Files
leanote-ios/Leanote/editor/EditorClasses/WPEditorView.m
2015-08-21 23:49:41 +08:00

2263 lines
74 KiB
Objective-C
Executable File

#import "WPEditorView.h"
#import "UIWebView+GUIFixes.h"
#import "HRColorUtil.h"
#import "WPEditorField.h"
#import "WPImageMeta.h"
#import "ZSSTextView.h"
#import "WPDeviceIdentification.h"
#import <WordPress-iOS-Shared/WPFontManager.h>
#import <WordPress-iOS-Shared/WPStyleGuide.h>
// life
#import "NSURLProtocolCustom.h"
typedef void(^WPEditorViewCallbackParameterProcessingBlock)(NSString* parameterName, NSString* parameterValue);
typedef void(^WPEditorViewNoParamsCompletionBlock)();
static NSString* const kDefaultCallbackParameterSeparator = @"~";
static NSString* const kDefaultCallbackParameterComponentSeparator = @"=";
static NSString* const kWPEditorViewFieldTitleId = @"zss_field_title";
static NSString* const kWPEditorViewFieldContentId = @"zss_field_content";
static const CGFloat UITextFieldLeftRightInset = 15.5f;
static const CGFloat iPadUITextFieldLeftRightInset = 90.0f;
static const CGFloat UITextFieldFieldHeight = 44.0f;
static const CGFloat HTMLViewTopInset = 15.0f;
static const CGFloat HTMLViewLeftRightInset = 10.0f;
static const CGFloat iPadHTMLViewLeftRightInset = 85.0f;
static NSString* const WPEditorViewWebViewContentSizeKey = @"contentSize";
@interface WPEditorView () <UITextViewDelegate, UIWebViewDelegate, UITextFieldDelegate>
#pragma mark - Cached caret & line data
@property (nonatomic, strong, readwrite) NSNumber *caretYOffset;
@property (nonatomic, strong, readwrite) NSNumber *lineHeight;
#pragma mark - Editor height
@property (nonatomic, assign, readwrite) NSInteger lastEditorHeight;
#pragma mark - Editing state
@property (nonatomic, assign, readwrite, getter = isEditing) BOOL editing;
#pragma mark - Selection
@property (nonatomic, assign, readwrite) NSRange selectionBackup;
@property (nonatomic, strong, readwrite) NSString *selectedLinkURL;
@property (nonatomic, strong, readwrite) NSString *selectedLinkTitle;
@property (nonatomic, strong, readwrite) NSString *selectedImageURL;
@property (nonatomic, strong, readwrite) NSString *selectedImageAlt;
#pragma mark - Subviews
@property (nonatomic, strong, readwrite) UITextField *sourceViewTitleField;
@property (nonatomic, strong, readonly) UIView *sourceContentDividerView;
@property (nonatomic, strong, readwrite) ZSSTextView *sourceView;
@property (nonatomic, strong, readonly) UIWebView* webView;
#pragma mark - Editor loading support
@property (nonatomic, copy, readwrite) NSString* preloadedHTML;
#pragma mark - Fields
@property (nonatomic, weak, readwrite) WPEditorField* focusedField;
@property BOOL isMarkdown;
@property BOOL keyboardIsShow;
@property BOOL isEditorMode;
@end
@implementation WPEditorView
#pragma mark - NSObject
- (void)dealloc
{
[self stopObservingKeyboardNotifications];
[self stopObservingWebViewContentSizeChanges];
}
#pragma mark - UIView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
self.isEditorMode = YES;
if (self) {
CGRect childFrame = frame;
childFrame.origin = CGPointZero;
[self createSourceTitleViewWithFrame: childFrame];
[self createSourceDividerViewWithFrame:CGRectMake(0.0f, CGRectGetMaxY(self.sourceViewTitleField.frame), CGRectGetWidth(childFrame), 1.0f)];
CGRect sourceViewFrame = CGRectMake(0.0f,
CGRectGetMaxY(self.sourceContentDividerView.frame),
CGRectGetWidth(childFrame),
CGRectGetHeight(childFrame)-CGRectGetHeight(self.sourceViewTitleField.frame)-CGRectGetHeight(self.sourceContentDividerView.frame));
[self createSourceViewWithFrame:sourceViewFrame];
[self createWebViewWithFrame:childFrame];
[self setupHTMLEditor];
}
return self;
}
// life
- (instancetype)initWithFrame:(CGRect)frame isMarkdown:(BOOL)isMarkdown
{
self = [super initWithFrame:frame];
self.isMarkdown = isMarkdown;
self.isEditorMode = YES;
if (self) {
CGRect childFrame = frame;
childFrame.origin = CGPointZero;
[self createSourceTitleViewWithFrame: childFrame];
[self createSourceDividerViewWithFrame:CGRectMake(0.0f, CGRectGetMaxY(self.sourceViewTitleField.frame), CGRectGetWidth(childFrame), 1.0f)];
CGRect sourceViewFrame = CGRectMake(0.0f,
CGRectGetMaxY(self.sourceContentDividerView.frame),
CGRectGetWidth(childFrame),
CGRectGetHeight(childFrame)-CGRectGetHeight(self.sourceViewTitleField.frame)-CGRectGetHeight(self.sourceContentDividerView.frame));
[self createSourceViewWithFrame:sourceViewFrame];
[self createWebViewWithFrame:childFrame];
[self setupHTMLEditor];
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
if (!newSuperview) {
[self stopObservingKeyboardNotifications];
} else {
[self startObservingKeyboardNotifications];
}
}
#pragma mark - Init helpers
- (void)createSourceTitleViewWithFrame:(CGRect)frame
{
NSAssert(!_sourceViewTitleField, @"The source view title field must not exist when this method is called!");
if (IS_IPAD) {
CGFloat textWidth = CGRectGetWidth(frame) - (2 * iPadUITextFieldLeftRightInset);
_sourceViewTitleField = [[UITextField alloc] initWithFrame:CGRectMake(iPadUITextFieldLeftRightInset, 5.0f, textWidth, UITextFieldFieldHeight)];
} else {
CGFloat textWidth = CGRectGetWidth(frame) - (2 * UITextFieldLeftRightInset);
_sourceViewTitleField = [[UITextField alloc] initWithFrame:CGRectMake(UITextFieldLeftRightInset, 5.0f, textWidth, UITextFieldFieldHeight)];
}
_sourceViewTitleField.hidden = YES;
_sourceViewTitleField.font = [WPFontManager merriweatherBoldFontOfSize:18.0f];
_sourceViewTitleField.autocapitalizationType = UITextAutocapitalizationTypeWords;
_sourceViewTitleField.autocorrectionType = UITextAutocorrectionTypeYes;
_sourceViewTitleField.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_sourceViewTitleField.delegate = self;
_sourceViewTitleField.accessibilityLabel = NSLocalizedString(@"Title", @"Post title");
_sourceViewTitleField.returnKeyType = UIReturnKeyNext;
[self addSubview:_sourceViewTitleField];
}
- (void)createSourceDividerViewWithFrame:(CGRect)frame
{
NSAssert(!_sourceContentDividerView, @"The source divider view must not exist when this method is called!");
if (IS_IPAD) {
CGFloat lineWidth = CGRectGetWidth(frame) - (2 * iPadUITextFieldLeftRightInset);
_sourceContentDividerView = [[UIView alloc] initWithFrame:CGRectMake(iPadUITextFieldLeftRightInset, CGRectGetMaxY(frame), lineWidth, CGRectGetHeight(frame))];
} else {
CGFloat lineWidth = CGRectGetWidth(frame) - (2 * UITextFieldLeftRightInset);
_sourceContentDividerView = [[UIView alloc] initWithFrame:CGRectMake(UITextFieldLeftRightInset, CGRectGetMaxY(frame), lineWidth, CGRectGetHeight(frame))];
}
_sourceContentDividerView.backgroundColor = [WPStyleGuide greyLighten30];
_sourceContentDividerView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
_sourceContentDividerView.hidden = YES;
[self addSubview:_sourceContentDividerView];
}
- (void)createSourceViewWithFrame:(CGRect)frame
{
NSAssert(!_sourceView, @"The source view must not exist when this method is called!");
_sourceView = [[ZSSTextView alloc] initWithFrame:frame];
_sourceView.autocapitalizationType = UITextAutocapitalizationTypeNone;
_sourceView.autocorrectionType = UITextAutocorrectionTypeNo;
_sourceView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_sourceView.autoresizesSubviews = YES;
if (IS_IPAD) {
_sourceView.textContainerInset = UIEdgeInsetsMake(HTMLViewTopInset, iPadHTMLViewLeftRightInset, 0, iPadHTMLViewLeftRightInset);
} else {
_sourceView.textContainerInset = UIEdgeInsetsMake(HTMLViewTopInset, HTMLViewLeftRightInset, 0.0f, HTMLViewLeftRightInset);
}
_sourceView.delegate = self;
[self addSubview:_sourceView];
}
- (void)createWebViewWithFrame:(CGRect)frame
{
NSAssert(!_webView, @"The web view must not exist when this method is called!");
_webView = [[UIWebView alloc] initWithFrame:frame];
// 就算frame高度>实现的高度, 通过autoresizingMask也可以resize到真实的高度
_webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_webView.delegate = self;
_webView.scalesPageToFit = YES;
_webView.dataDetectorTypes = UIDataDetectorTypeNone;
_webView.backgroundColor = [UIColor clearColor];
_webView.opaque = NO;
_webView.scrollView.bounces = NO;
_webView.usesGUIFixes = YES;
_webView.keyboardDisplayRequiresUserAction = NO;
_webView.scrollView.bounces = YES;
_webView.allowsInlineMediaPlayback = YES;
[self startObservingWebViewContentSizeChanges];
[self addSubview:_webView];
}
// 加载editor.html
- (void)setupHTMLEditor
{
// 设置自定义协议
[NSURLProtocol registerClass:[NSURLProtocolCustom class]];
// 普通编辑器
if(!self.isMarkdown) {
NSBundle * bundle = [NSBundle bundleForClass:[WPEditorView class]];
NSURL * editorURL = [bundle URLForResource:@"editor.min" withExtension:@"html"];
[self.webView loadRequest:[NSURLRequest requestWithURL:editorURL]];
}
// markdown编辑器
else {
// life
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"editor-mobile.min"
ofType:@"html"
inDirectory:@"MarkdownAssets" ];
NSString *html = [NSString stringWithContentsOfFile:htmlPath
encoding:NSUTF8StringEncoding
error:nil];
[self.webView loadHTMLString:html
baseURL:[NSURL fileURLWithPath:
[NSString stringWithFormat:@"%@/MarkdownAssets",
[[NSBundle mainBundle] bundlePath]]]];
}
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// IMPORTANT: WORKAROUND: the following code is a fix to prevent the web view from thinking it's
// taller than it really is. The problem we were having is that when we were switching the
// focus from the title field to the content field, the web view was trying to scroll down, and
// jumping back up.
//
// The reason behind the sizing issues is that the web view doesn't really like having insets
// and wants it's body and content to be as tall as possible.
//
// Ref bug: https://github.com/wordpress-mobile/WordPress-iOS-Editor/issues/324
//
if (object == self.webView.scrollView) {
if ([keyPath isEqualToString:WPEditorViewWebViewContentSizeKey]) {
NSValue *newValue = change[NSKeyValueChangeNewKey];
CGSize newSize = [newValue CGSizeValue];
if (newSize.height != self.lastEditorHeight) {
// First make sure that the content size is not changed without us recalculating it.
//
self.webView.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.frame), self.lastEditorHeight);
[self workaroundBrokenWebViewRendererBug];
// Then recalculate it asynchronously so the UIWebView doesn't break.
//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self refreshVisibleViewportAndContentSize];
});
}
}
}
}
// 监听scrollView的contentSize的变化
- (void)startObservingWebViewContentSizeChanges
{
[_webView.scrollView addObserver:self
forKeyPath:WPEditorViewWebViewContentSizeKey
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)stopObservingWebViewContentSizeChanges
{
[self.webView.scrollView removeObserver:self
forKeyPath:WPEditorViewWebViewContentSizeKey];
}
#pragma mark - Bug Workarounds
/**
* @brief Redraws the web view, since [webView setNeedsDisplay] doesn't seem to work.
*/
- (void)redrawWebView
{
NSArray *views = self.webView.scrollView.subviews;
for(int i = 0; i< views.count; i++){
UIView *view = views[i];
[view setNeedsDisplay];
}
}
/**
* @brief Works around a problem caused by another workaround we're using, that's causing the
* web renderer to be interrupted before finishing.
* @details When we know of a contentSize change in the web view's scroll view, we override the
* operation to manually calculate the proper new size and set it. This is causing the
* web renderer to fail and interrupt. Drawing doesn't finish properly. This method
* offers a sort of forced redraw mechanism after a very short delay.
*/
- (void)workaroundBrokenWebViewRendererBug
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self redrawWebView];
});
}
#pragma mark - Keyboard notifications
- (void)startObservingKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)stopObservingKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}
#pragma mark - Keyboard status
- (void)keyboardDidShow:(NSNotification *)notification
{
BOOL isiOSVersionEarlierThan8 = [WPDeviceIdentification isiOSVersionEarlierThan8];
if (isiOSVersionEarlierThan8) {
// PROBLEM: under iOS 7, it seems that setting the proper insets in keyboardWillShow: is not
// enough. We were having trouble when adding images, where the keyboard would show but the
// insets would be reset to {0, 0, 0, 0} between keyboardWillShow: and keyboardDidShow:
//
// HOW TO TEST:
//
// - launch the WPiOS app under iOS 7.
// - set a title
// - make sure the virtual keyboard is up
// - add some text and on the same line add an image
// - once the image is added tap once on the content field to make the keyboard come back up
// (do this before the upload finishes).
//
// WORKAROUND: we just set the insets again in keyboardDidShow: for iOS 7
//
[self refreshKeyboardInsetsWithShowNotification:notification];
}
[self scrollToCaretAnimated:NO];
}
- (void)keyboardWillShow:(NSNotification *)notification
{
[self refreshKeyboardInsetsWithShowNotification:notification];
self.keyboardIsShow = YES;
NSLog(@"keyboardIsShow");
}
- (void)keyboardWillHide:(NSNotification *)notification
{
// WORKAROUND: sometimes the input accessory view is not taken into account and a
// keyboardWillHide: call is triggered instead. Since there's no way for the source view now
// to have focus, we'll just make sure the inputAccessoryView is taken into account when
// hiding the keyboard.
//
CGFloat vOffset = self.sourceView.inputAccessoryView.frame.size.height; // 40
UIEdgeInsets insets = UIEdgeInsetsMake(0.0f, 0.0f, vOffset, 0.0f);
// for test
insets = UIEdgeInsetsMake(0.0f, 0.0f, 0, 0.0f);
self.webView.scrollView.contentInset = insets;
self.webView.scrollView.scrollIndicatorInsets = insets;
self.sourceView.contentInset = insets;
self.sourceView.scrollIndicatorInsets = insets;
NSLog(@"keyboardIsShow NO");
self.keyboardIsShow = NO;
if(self.isMarkdown) {
// for markdown
[self scrollToCaretAnimated:NO];
}
// markdown编辑器会触发两次
// 为markdown
// NSString *js = [NSString stringWithFormat:@"LEAMD.keyboardHide()"];
// [self.webView stringByEvaluatingJavaScriptFromString:js];
}
#pragma mark - Keyboard Misc.
/**
* @brief Takes care of calculating and setting the proper insets when the keyboard is shown.
* @details This method can be called from both keyboardWillShow: and keyboardDidShow:.
*
* @param notification The notification containing the size info for the keyboard.
* Cannot be nil.
*/
- (void)refreshKeyboardInsetsWithShowNotification:(NSNotification*)notification
{
NSParameterAssert([notification isKindOfClass:[NSNotification class]]);
NSDictionary *info = notification.userInfo;
CGRect keyboardEnd = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect localizedKeyboardEnd = [self convertRect:keyboardEnd fromView:nil];
CGPoint keyboardOrigin = localizedKeyboardEnd.origin;
// Orientation
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if (keyboardOrigin.y > 0) {
// for rich
CGFloat vOffset = CGRectGetHeight(self.frame) - keyboardOrigin.y; // 除去keyboard的高度
// padding padding-bottom
UIEdgeInsets insets = UIEdgeInsetsMake(0.0f, 0.0f, vOffset, 0.0f);
self.webView.scrollView.contentInset = insets;
self.webView.scrollView.scrollIndicatorInsets = insets;
self.sourceView.contentInset = insets;
self.sourceView.scrollIndicatorInsets = insets;
if(self.isMarkdown) {
CGFloat keyboardHeight = UIInterfaceOrientationIsLandscape(orientation) ? ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.000000) ? keyboardEnd.size.height : keyboardEnd.size.width : keyboardEnd.size.height;
// NSLog(@"webView height %f", self.webView.frame.size.height);
NSString *js = [NSString stringWithFormat:@"LEAMD.keyboardShow(%f, %f);", keyboardHeight, self.webView.frame.size.height];
[self.webView stringByEvaluatingJavaScriptFromString:js];
}
}
}
- (void)refreshVisibleViewportAndContentSize
{
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.refreshVisibleViewportSize();"];
#ifdef DEBUG
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.logMainElementSizes();"];
#endif
NSString* newHeightString = [self.webView stringByEvaluatingJavaScriptFromString:@"$(document.body).height();"];
NSInteger newHeight = [newHeightString integerValue];
self.lastEditorHeight = newHeight;
// 我靠, 重新resize啊
// contentSize是scrollview可以滚动的区域, 所以, contentSize就是内容的高度
self.webView.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.frame), newHeight);
}
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = [request URL];
BOOL shouldLoad = NO;
if (navigationType != UIWebViewNavigationTypeLinkClicked) {
BOOL handled = [self handleWebViewCallbackURL:url];
shouldLoad = !handled;
}
return shouldLoad;
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if ([self.delegate respondsToSelector:@selector(editorViewDidFinishLoading:)]) {
[self.delegate editorViewDidFinishLoading:self];
}
}
#pragma mark - Handling callbacks
/**
* @brief Handles UIWebView callbacks.
*
* @param url The url for the callback. Cannot be nil.
*
* @returns YES if the callback was handled, NO otherwise.
*/
- (BOOL)handleWebViewCallbackURL:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
BOOL handled = NO;
NSString *scheme = [url scheme];
// DDLogDebug(@"WebEditor callback received: %@", url);
if (scheme) {
if ([self isFocusInScheme:scheme]) {
[self handleFocusInCallback:url];
handled = YES;
} else if ([self isFocusOutScheme:scheme]) {
[self handleFocusOutCallback:url];
handled = YES;
} else if ([self isInputCallbackScheme:scheme]) {
[self handleInputCallback:url];
handled = YES;
// callback-link-tap
// callback-link-tap:id=zss_field_conten~turl=http%3A%2F%2Flife.leanote.com%2F~title=%E6%80%8E%E4%B9%88%E6%90%9E%E7%9A%84%3F%20
} else if ([self isLinkTappedScheme:scheme]) {
[self handleLinkTappedCallback:url];
handled = YES;
}
// 点击图片
// @"callback-image-tap:id=1~url=leanote%3A%2F%2FgetImage%3FfileId%3D5581a29f38f41130230006d7~meta=%7B%22align%22%3A%22%22%2C%22alt%22%3A%22%22%2C%22attachment_id%22%3A%22%22%2C%22caption%22%3A%22%22%2C%22captionClassName%22%3A%22%22%2C%22captionId%22%3A%22%22%2C%22classes%22%3A%22%22%2C%22height%22%3A888%2C%22linkClassName%22%3A%22%22%2C%22linkRel%22%3A%22%22%2C%22linkTargetBlank%22%3Afalse%2C%22linkUrl%22%3A%22%22%2C%22size%22%3A%22custom%22%2C%22src%22%3A%22leanote%3A%2F%2FgetImage%3FfileId%3D5581a29f38f41130230006d7%22%2C%22title%22%3A%22%22%2C%22width%22%3A574%2C%22naturalWidth%22%3A574%2C%22naturalHeight%22%3A888%7D"
else if ([self isImageTappedScheme:scheme]) {
[self handleImageTappedCallback:url];
handled = YES;
} else if ([self isVideoTappedScheme:scheme]) {
[self handleVideoTappedCallback:url];
handled = YES;
} else if ([self isLogCallbackScheme:scheme]){
[self handleLogCallbackScheme:url];
handled = YES;
} else if ([self isLogErrorCallbackScheme:scheme]){
[self handleLogErrorCallbackScheme:url];
handled = YES;
} else if ([self isNewFieldCallbackScheme:scheme]) {
[self handleNewFieldCallback:url];
handled = YES;
} else if ([self isSelectionChangedCallbackScheme:scheme]){
[self handleSelectionChangedCallback:url];
handled = YES;
} else if ([self isSelectionStyleScheme:scheme]) {
[self handleSelectionStyleCallback:url];
handled = YES;
} else if ([self isDOMLoadedScheme:scheme]) {
[self handleDOMLoadedCallback:url];
handled = YES;
} else if ([self isImageReplacedScheme:scheme]) {
[self handleImageReplacedCallback:url];
handled = YES;
} else if ([self isVideoReplacedScheme:scheme]) {
[self handleVideoReplacedCallback:url];
handled = YES;
} else if ([self isVideoFullScreenStartedScheme:scheme]) {
[self handleVideoFullScreenStartedCallback:url];
handled = YES;
} else if ([self isVideoFullScreenEndedScheme:scheme]) {
[self handleVideoFullScreenEndedCallback:url];
handled = YES;
} else if ([self isVideoPressInfoRequestScheme:scheme]) {
[self handleVideoPressInfoRequestCallback:url];
handled = YES;
} else if ([self isMediaRemovedScheme:scheme]) {
[self handleMediaRemovedCallback:url];
handled = YES;
}
}
return handled;
}
/**
* @brief Handles a DOM loaded callback.
*
* @param url The url with all the callback information.
*/
- (void)handleDOMLoadedCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
[self.titleField handleDOMLoaded];
[self.contentField handleDOMLoaded];
if ([self.delegate respondsToSelector:@selector(editorViewDidFinishLoadingDOM:)]) {
[self.delegate editorViewDidFinishLoadingDOM:self];
}
}
- (void)handleFocusInCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kFieldIdParameterName = @"id";
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kFieldIdParameterName]) {
if ([parameterValue isEqualToString:kWPEditorViewFieldTitleId]) {
self.focusedField = self.titleField;
} else if ([parameterValue isEqualToString:kWPEditorViewFieldContentId]) {
self.focusedField = self.contentField;
}
self.webView.customInputAccessoryView = self.focusedField.inputAccessoryView;
}
} onComplete:^{
[self callDelegateFieldFocused:self.focusedField];
}];
}
/**
* @brief Handles a focus out callback.
*
* @param url The url with all the callback information.
*/
- (void)handleFocusOutCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
self.focusedField = nil;
[self callDelegateFieldFocused:self.focusedField];
}
/**
* @brief Handles a key pressed callback.
*
* @param url The url with all the callback information.
*/
- (void)handleInputCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kFieldIdParameterName = @"id";
static NSString* const kYOffsetParameterName = @"yOffset";
static NSString* const kLineHeightParameterName = @"height";
self.caretYOffset = nil;
self.lineHeight = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kFieldIdParameterName]) {
if ([parameterValue isEqualToString:kWPEditorViewFieldTitleId]) {
[self callDelegateEditorTitleDidChange];
} else if ([parameterValue isEqualToString:kWPEditorViewFieldContentId]) {
[self callDelegateEditorTextDidChange];
}
self.webView.customInputAccessoryView = self.focusedField.inputAccessoryView;
} else if ([parameterName isEqualToString:kYOffsetParameterName]) {
self.caretYOffset = @([parameterValue floatValue]);
NSLog(@"caretYOffset = %@", self.caretYOffset);
} else if ([parameterName isEqualToString:kLineHeightParameterName]) {
self.lineHeight = @([parameterValue floatValue]);
}
} onComplete:^() {
// WORKAROUND: it seems that without this call, typing doesn't always follow the caret
// position.
//
// HOW TO TEST THIS: disable the following line, and run the demo... type in the contents
// field while also showing the virtual keyboard. You'll notice the caret can, at times,
// go behind the virtual keyboard. 确实如此
//
[self refreshVisibleViewportAndContentSize];
[self scrollToCaretAnimated:NO];
}];
}
/**
* @brief Handles a link tapped callback.
*
* @param url The url with all the callback information.
*/
- (void)handleLinkTappedCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kTappedUrlParameterName = @"url";
static NSString* const kTappedUrlTitleParameterName = @"title";
__block NSURL* tappedUrl = nil;
__block NSString* tappedUrlTitle = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kTappedUrlParameterName]) {
tappedUrl = [NSURL URLWithString:[self stringByDecodingURLFormat:parameterValue]];
} else if ([parameterName isEqualToString:kTappedUrlTitleParameterName]) {
tappedUrlTitle = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:linkTapped:title:)]) {
[self.delegate editorView:self linkTapped:tappedUrl title:tappedUrlTitle];
}
}];
}
/**
* @brief Handles a image tapped callback.
*
* @param url The url with all the callback information.
*/
- (void)handleImageTappedCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kTappedUrlParameterName = @"url";
static NSString *const kTappedIdParameterName = @"id";
static NSString *const kTappedMetaName = @"meta";
__block NSURL *tappedUrl = nil;
__block NSString *tappedId = nil;
__block NSString *tappedMeta = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kTappedUrlParameterName]) {
tappedUrl = [NSURL URLWithString:[self stringByDecodingURLFormat:parameterValue]];
} else if ([parameterName isEqualToString:kTappedIdParameterName]) {
tappedId = [self stringByDecodingURLFormat:parameterValue];
} else if ([parameterName isEqualToString:kTappedMetaName]) {
tappedMeta = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:imageTapped:url:imageMeta:)]) {
WPImageMeta *imageMeta = [WPImageMeta imageMetaFromJSONString:tappedMeta];
[self.delegate editorView:self imageTapped:tappedId url:tappedUrl imageMeta:imageMeta];
}
}];
}
/**
* @brief Handles a video tapped callback.
*
* @param url The url with all the callback information.
*/
- (void)handleVideoTappedCallback:(NSURL *)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kTappedUrlParameterName = @"url";
static NSString *const kTappedIdParameterName = @"id";
__block NSURL *tappedUrl = nil;
__block NSString *tappedId = nil;
[self parseParametersFromCallbackURL:url andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue) {
if ([parameterName isEqualToString:kTappedUrlParameterName]) {
tappedUrl = [NSURL URLWithString:[self stringByDecodingURLFormat:parameterValue]];
} else if ([parameterName isEqualToString:kTappedIdParameterName]) {
tappedId = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:videoTapped:url:)]) {
[self.delegate editorView:self videoTapped:tappedId url:tappedUrl];
}
}];
}
/**
* @brief Handles a video entered fullscreen callback
*
* @param url The url with all the callback information.
*/
- (void)handleVideoFullScreenStartedCallback:(NSURL *)aURL
{
NSParameterAssert([aURL isKindOfClass:[NSURL class]]);
[self saveSelection];
// FIXME: SergioEstevao 2015/03/25 - It looks there is a bug on iOS 8 that makes
// the keyboard not to be hidden when a video is made to run in full screen inside a webview.
// this workaround searches for the first responder and dismisses it
UIView *firstResponder = [self findFirstResponder:self];
[firstResponder resignFirstResponder];
}
/**
* Finds the first responder in the view hierarchy starting from the currentView
*
* @param currentView the view to start looking for the first responder.
*
* @return the view that is the current first responder nil if none was found.
*/
- (UIView *)findFirstResponder:(UIView *)currentView
{
if (currentView.isFirstResponder) {
[currentView resignFirstResponder];
return currentView;
}
for (UIView *subView in currentView.subviews) {
UIView *result = [self findFirstResponder:subView];
if (result) {
return result;
}
}
return nil;
}
/**
* @brief Handles a video ended fullscreen callback.
*
* @param url The url with all the callback information.
*/
- (void)handleVideoFullScreenEndedCallback:(NSURL *)aURL
{
NSParameterAssert([aURL isKindOfClass:[NSURL class]]);
[self restoreSelection];
}
/**
* @brief Handles a image replaced callback.
*
* @param url The url with all the callback information.
*/
- (void)handleImageReplacedCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kImagedIdParameterName = @"id";
__block NSString *imageId = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kImagedIdParameterName]) {
imageId = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:imageReplaced:)]) {
[self.delegate editorView:self imageReplaced:imageId];
}
}];
}
/**
* @brief Handles a video replaced callback.
*
* @param url The url with all the callback information.
*/
- (void)handleVideoReplacedCallback:(NSURL *)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kVideoIdParameterName = @"id";
__block NSString *videoId = nil;
[self parseParametersFromCallbackURL:url andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kVideoIdParameterName]) {
videoId = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:videoReplaced:)]) {
[self.delegate editorView:self videoReplaced:videoId];
}
}];
}
- (void)handleVideoPressInfoRequestCallback:(NSURL *)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kVideoIdParameterName = @"id";
__block NSString *videoId = nil;
[self parseParametersFromCallbackURL:url andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kVideoIdParameterName]) {
videoId = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:videoPressInfoRequest:)]) {
[self.delegate editorView:self videoPressInfoRequest:videoId];
}
}];
}
- (void)handleMediaRemovedCallback:(NSURL *)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString *const kMediaIdParameterName = @"id";
__block NSString *mediaId = nil;
[self parseParametersFromCallbackURL:url andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kMediaIdParameterName]) {
mediaId = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
if ([self.delegate respondsToSelector:@selector(editorView:mediaRemoved:)]) {
[self.delegate editorView:self mediaRemoved:mediaId];
}
}];
}
/**
* @brief Handles a log callback.
*
* @param url The url with all the callback information.
*/
- (void)handleLogCallbackScheme:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kMessageParameterName = @"msg";
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kMessageParameterName]) {
NSLog(@"WebEditor log: %@", [self stringByDecodingURLFormat:parameterValue]);
}
} onComplete:nil];
}
/**
* @brief Handles a log error callback.
*
* @param url The url with all the callback information.
*/
- (void)handleLogErrorCallbackScheme:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kLineParameterName = @"line";
static NSString* const kMessageParameterName = @"msg";
static NSString* const kURLParameterName = @"url";
__block NSString *line = nil;
__block NSString *message = nil;
__block NSString *errorUrl = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kLineParameterName]) {
line = [self stringByDecodingURLFormat:parameterValue];
} else if ([parameterName isEqualToString:kMessageParameterName]) {
message = [self stringByDecodingURLFormat:parameterValue];
} else if ([parameterName isEqualToString:kURLParameterName]) {
errorUrl = [self stringByDecodingURLFormat:parameterValue];
}
} onComplete:^{
static NSString* const ErrorFormat = @"WebEditor error:\r\n In file: %@\r\n In line: %@\r\n %@";
DDLogError(ErrorFormat, errorUrl, line, message);
}];
}
/**
* @brief Handles a new field callback.
*
* @param url The url with all the callback information.
*/
- (void)handleNewFieldCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kFieldIdParameterName = @"id";
__block NSString* fieldId = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kFieldIdParameterName]) {
NSAssert([parameterValue isKindOfClass:[NSString class]],
@"We're expecting a non-nil NSString object here.");
fieldId = parameterValue;
}
} onComplete:^{
WPEditorField* newField = [self createFieldWithId:fieldId];
[self callDelegateFieldCreated:newField];
}];
}
/**
* @brief Handles a selection changed callback.
*
* @param url The url with all the callback information.
*/
- (void)handleSelectionChangedCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
static NSString* const kYOffsetParameterName = @"yOffset";
static NSString* const kLineHeightParameterName = @"height";
self.caretYOffset = nil;
self.lineHeight = nil;
[self parseParametersFromCallbackURL:url
andExecuteBlockForEachParameter:^(NSString *parameterName, NSString *parameterValue)
{
if ([parameterName isEqualToString:kYOffsetParameterName]) {
self.caretYOffset = @([parameterValue floatValue]);
} else if ([parameterName isEqualToString:kLineHeightParameterName]) {
self.lineHeight = @([parameterValue floatValue]);
}
} onComplete:^() {
[self scrollToCaretAnimated:NO];
}];
}
- (void)handleSelectionStyleCallback:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
NSString* styles = [[url resourceSpecifier] stringByReplacingOccurrencesOfString:@"//" withString:@""];
[self processStyles:styles];
}
#pragma mark - Handling callbacks: identifying schemes
- (BOOL)isDOMLoadedScheme:(NSString*)scheme
{
static NSString* const kCallbackScheme = @"callback-dom-loaded";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isFocusInScheme:(NSString*)scheme
{
static NSString* const kCallbackScheme = @"callback-focus-in";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isFocusOutScheme:(NSString*)scheme
{
static NSString* const kCallbackScheme = @"callback-focus-out";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isLinkTappedScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-link-tap";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isImageTappedScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-image-tap";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isImageReplacedScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-image-replaced";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isVideoTappedScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-video-tap";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isVideoReplacedScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-video-replaced";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isLogCallbackScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-log";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isLogErrorCallbackScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-log-error";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isNewFieldCallbackScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-new-field";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isSelectionChangedCallbackScheme:(NSString*)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString* const kCallbackScheme = @"callback-selection-changed";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isVideoFullScreenStartedScheme:(NSString *)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString *const kCallbackScheme = @"callback-video-fullscreen-started";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isVideoFullScreenEndedScheme:(NSString *)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString *const kCallbackScheme = @"callback-video-fullscreen-ended";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isVideoPressInfoRequestScheme:(NSString *)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString *const kCallbackScheme = @"callback-videopress-info-request";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isMediaRemovedScheme:(NSString *)scheme
{
NSAssert([scheme isKindOfClass:[NSString class]],
@"We're expecting a non-nil string object here.");
static NSString *const kCallbackScheme = @"callback-media-removed";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isSelectionStyleScheme:(NSString*)scheme
{
static NSString* const kCallbackScheme = @"callback-selection-style";
return [scheme isEqualToString:kCallbackScheme];
}
- (BOOL)isInputCallbackScheme:(NSString*)scheme
{
static NSString* const kCallbackScheme = @"callback-input";
return [scheme isEqualToString:kCallbackScheme];
}
- (void)processStyles:(NSString *)styles
{
NSArray *styleStrings = [styles componentsSeparatedByString:kDefaultCallbackParameterSeparator];
NSMutableArray *itemsModified = [[NSMutableArray alloc] init];
self.selectedImageURL = nil;
self.selectedImageAlt = nil;
self.selectedLinkURL = nil;
self.selectedLinkTitle = nil;
for (NSString *styleString in styleStrings) {
NSString *updatedItem = styleString;
if ([styleString hasPrefix:@"link:"]) {
updatedItem = @"link";
self.selectedLinkURL = [self stringByDecodingURLFormat:[styleString stringByReplacingOccurrencesOfString:@"link:" withString:@""]];
} else if ([styleString hasPrefix:@"link-title:"]) {
self.selectedLinkTitle = [self stringByDecodingURLFormat:[styleString stringByReplacingOccurrencesOfString:@"link-title:" withString:@""]];
} else if ([styleString hasPrefix:@"image:"]) {
updatedItem = @"image";
self.selectedImageURL = [styleString stringByReplacingOccurrencesOfString:@"image:" withString:@""];
} else if ([styleString hasPrefix:@"image-alt:"]) {
self.selectedImageAlt = [self stringByDecodingURLFormat:[styleString stringByReplacingOccurrencesOfString:@"image-alt:" withString:@""]];
}
[itemsModified addObject:updatedItem];
}
styleStrings = [NSArray arrayWithArray:itemsModified];
if ([self.delegate respondsToSelector:@selector(editorView:stylesForCurrentSelection:)])
{
[self.delegate editorView:self stylesForCurrentSelection:styleStrings];
}
}
#pragma mark - Viewport rect
/**
* @brief Obtain the current viewport.
*
* @returns The current viewport.
*/
- (CGRect)viewport
{
UIScrollView* scrollView = self.webView.scrollView;
CGRect viewport;
viewport.origin = scrollView.contentOffset;
viewport.size = scrollView.bounds.size;
viewport.size.height -= (scrollView.contentInset.top + scrollView.contentInset.bottom);
viewport.size.width -= (scrollView.contentInset.left + scrollView.contentInset.right);
return viewport;
}
#pragma mark - Callback parsing
/**
* @brief Extract the components that make up a parameter.
* @details Should always be two (for example: 'value=65' would return @['value', '65']).
*
* @param parameter The string parameter to parse. Cannot be nil.
*
* @returns An array containing each component.
*/
- (NSArray*)componentsFromParameter:(NSString*)parameter
{
NSParameterAssert([parameter isKindOfClass:[NSString class]]);
NSRange range = [parameter rangeOfString:kDefaultCallbackParameterComponentSeparator];
NSString* parameterName = [parameter substringToIndex:range.location];
NSString* parameterValue = [parameter substringFromIndex:range.location + range.length];
NSArray* components = @[parameterName, parameterValue];
NSAssert([components count] == 2,
@"We're expecting exactly two components here.");
return components;
}
/**
* @brief This is a very helpful method for parsing through a callback's parameters and
* performing custom processing when each parameter and value is identified.
*
* @param url The callback URL to process. Cannot be nil.
* @param block Will be executed one time for each parameter identified by the
* parser. Cannot be nil.
* @param onCompleteBlock The block to execute when the parsing finishes. Can be nil.
*/
- (void)parseParametersFromCallbackURL:(NSURL*)url
andExecuteBlockForEachParameter:(WPEditorViewCallbackParameterProcessingBlock)block
onComplete:(WPEditorViewNoParamsCompletionBlock)onCompleteBlock
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
NSParameterAssert(block);
NSArray* parameters = [self parametersFromCallbackURL:url];
for (NSString* parameter in parameters) {
NSAssert([parameter isKindOfClass:[NSString class]],
@"We're expecting to have a non-nil NSString object here.");
NSArray* components = [self componentsFromParameter:parameter];
NSAssert([components count] == 2,
@"We're expecting exactly two components here.");
block([components objectAtIndex:0], [components objectAtIndex:1]);
}
if (onCompleteBlock) {
onCompleteBlock();
}
}
/**
* @brief Extract the parameters that make up a callback URL.
*
* @param url The callback URL to parse. Cannot be nil.
*
* @returns An array containing each parameter.
*/
- (NSArray*)parametersFromCallbackURL:(NSURL*)url
{
NSParameterAssert([url isKindOfClass:[NSURL class]]);
NSArray* parameters = [[url resourceSpecifier] componentsSeparatedByString:kDefaultCallbackParameterSeparator];
return parameters;
}
#pragma mark - Fields
/**
* @brief Creates a field for the specified id.
* @todo At some point it would be nice to have WPEditorView be able to handle a custom list
* of fields, instead of expecting the HTML page to only have a title and a content
* field.
*
* @param fieldId The id of the field to create. This is the id of the html node that
* our new field will wrap. Cannot be nil.
*
* @returns The newly created field.
*/
- (WPEditorField*)createFieldWithId:(NSString*)fieldId
{
NSAssert([fieldId isKindOfClass:[NSString class]],
@"We're expecting a non-nil NSString object here.");
WPEditorField* newField = nil;
if ([fieldId isEqualToString:kWPEditorViewFieldTitleId]) {
NSAssert(!_titleField,
@"We should never have to set this twice.");
_titleField = [[WPEditorField alloc] initWithId:fieldId webView:self.webView];
newField = self.titleField;
} else if ([fieldId isEqualToString:kWPEditorViewFieldContentId]) {
NSAssert(!_contentField,
@"We should never have to set this twice.");
_contentField = [[WPEditorField alloc] initWithId:fieldId webView:self.webView];
newField = self.contentField;
}
NSAssert([newField isKindOfClass:[WPEditorField class]],
@"A new field should've been created here.");
return newField;
}
#pragma mark - URL & HTML utilities
/**
* @brief Adds slashes to the specified HTML string, to prevent injections when calling JS
* code.
*
* @param html The HTML string to add slashes to. Cannot be nil.
*
* @returns The HTML string with the added slashes.
*/
- (NSString *)addSlashes:(NSString *)html
{
html = [html stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
html = [html stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
html = [html stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
html = [html stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
return html;
}
- (NSString *)stringByDecodingURLFormat:(NSString *)string
{
NSString *result = [string stringByReplacingOccurrencesOfString:@"+" withString:@" "];
result = [result stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
return result;
}
#pragma mark - Interaction
// undo, redo没用, 奇怪
- (void)undo
{
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.undo();"];
[self callDelegateEditorTextDidChange];
}
- (void)redo
{
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.redo();"];
[self callDelegateEditorTextDidChange];
}
#pragma mark - Text Access
- (NSString*)contents
{
NSString* contents = nil;
if ([self isInVisualMode]) {
contents = [self.contentField html];
} else {
contents = self.sourceView.text;
}
return contents;
}
- (NSString*)title
{
NSString* title = nil;
if ([self isInVisualMode]) {
title = [self.titleField strippedHtml];
} else {
title = self.sourceViewTitleField.text;
}
return title;
}
#pragma mark - Scrolling support
// 键盘显示, handleInputCallback, handleSelectionCallback 都会调用, 滚动到caret
/**
* @brief Scrolls to a position where the caret is visible. This uses the values stored in caretYOffest and lineHeight properties.
* @param animated If the scrolling shoud be animated The offset to show.
*/
- (void)scrollToCaretAnimated:(BOOL)animated
{
BOOL notEnoughInfoToScroll = self.caretYOffset == nil || self.lineHeight == nil;
if (notEnoughInfoToScroll && self.isMarkdown) {
self.lineHeight = 0;
if(self.keyboardIsShow) {
NSLog(@"scrollToCaretAnimated keyboardIsShow");
NSString *y = [self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.getCaretYPosition();"];
self.caretYOffset = [NSNumber numberWithFloat:[y floatValue]];
}
else {
// 如果keyboard is hide, 则用之前的y
NSLog(@"scrollToCaretAnimated keyboardIsHide");
}
// return;
}
CGRect viewport = [self viewport];
CGFloat caretYOffset = [self.caretYOffset floatValue];
CGFloat lineHeight = [self.lineHeight floatValue];
CGFloat offsetBottom = caretYOffset + lineHeight;
BOOL mustScroll = (caretYOffset < viewport.origin.y
|| offsetBottom > viewport.origin.y + CGRectGetHeight(viewport));
if (mustScroll) {
// DRM: by reducing the necessary height we avoid an issue that moves the caret out
// of view.
//
CGFloat necessaryHeight = viewport.size.height / 2;
// DRM: just make sure we don't go out of bounds with the desired yOffset.
//
caretYOffset = MIN(caretYOffset,
self.webView.scrollView.contentSize.height - necessaryHeight);
CGRect targetRect = CGRectMake(0.0f,
caretYOffset,
CGRectGetWidth(viewport),
necessaryHeight);
[self.webView.scrollView scrollRectToVisible:targetRect animated:animated];
}
}
#pragma mark - Selection
- (void)restoreSelection
{
if (self.isInVisualMode) {
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.restoreRange();"];
} else {
[self.sourceView select:self];
[self.sourceView setSelectedRange:self.selectionBackup];
self.selectionBackup = NSMakeRange(0, 0);
}
}
- (void)saveSelection
{
if (self.isInVisualMode) {
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.backupRange();"];
} else {
self.selectionBackup = self.sourceView.selectedRange;
}
}
- (NSString*)selectedText
{
NSString* selectedText;
if (self.isInVisualMode) {
selectedText = [self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.getSelectedText();"];
} else {
NSRange range = [self.sourceView selectedRange];
selectedText = [self.sourceView.text substringWithRange:range];
}
return selectedText;
}
/*
// life, 没用
// 为什么很难判断isContentDirty ? click ? keydown ? 因为还有insertImage
- (NSString*)contentIsDirty
{
NSString *isDirty;
isDirty = [self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.contentIsDirty();"];
return isDirty;
}
*/
- (void)setSelectedColor:(UIColor*)color tag:(int)tag
{
CGFloat r, g, b, a;
[color getRed: &r green:&g blue:&b alpha:&a];
NSString *hex = [NSString stringWithFormat:@"rgba(%d, %d, %d, %f)", (int)(r * 255), (int)(g * 255), (int)(b * 255), a];
NSString *trigger;
if (tag == 1) {
trigger = [NSString stringWithFormat:@"ZSSEditor.setTextColor(\"%@\");", hex];
} else if (tag == 2) {
trigger = [NSString stringWithFormat:@"ZSSEditor.setBackgroundColor(\"%@\");", hex];
}
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
#pragma mark - Images
- (void)insertLocalImage:(NSString*)url uniqueId:(NSString*)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertLocalImage(\"%@\", \"%@\");", uniqueId, url];
// NSLog(trigger);
// NSString *trigger2 = @"ZSSEditor.insertImage(\"http://leanote.com/images/logo.png\", \"xx\");";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
// [self.webView stringByEvaluatingJavaScriptFromString:trigger2];
}
// url = ['a','b']
- (void)insertImage:(NSString *)url alt:(NSString *)alt
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertImage(%@, \"%@\");", url, alt];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)replaceLocalImageWithRemoteImage:(NSString*)url uniqueId:(NSString*)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.replaceLocalImageWithRemoteImage(\"%@\", \"%@\");", uniqueId, url];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)updateImage:(NSString *)url alt:(NSString *)alt
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.updateImage(\"%@\", \"%@\");", url, alt];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)updateCurrentImageMeta:(WPImageMeta *)imageMeta
{
NSString *jsonString = [imageMeta jsonStringRepresentation];
jsonString = [self addSlashes:jsonString];
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.updateCurrentImageMeta(\"%@\");", jsonString];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)setProgress:(double) progress onImage:(NSString*)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.setProgressOnImage(\"%@\", %f);", uniqueId, progress];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)markImage:(NSString *)uniqueId failedUploadWithMessage:(NSString*) message;
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.markImageUploadFailed(\"%@\", \"%@\");", uniqueId, message];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)unmarkImageFailedUpload:(NSString *)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.unmarkImageUploadFailed(\"%@\");", uniqueId];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)removeImage:(NSString*)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.removeImage(\"%@\");", uniqueId];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
#pragma mark - Videos
- (void)insertVideo:(NSString *)videoURL posterImage:(NSString *)posterImageURL alt:(NSString *)alt
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertVideo(\"%@\", \"%@\", \"%@\");", videoURL, posterImageURL, alt];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)insertInProgressVideoWithID:(NSString *)uniqueId
usingPosterImage:(NSString *)posterImageURL
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertInProgressVideoWithIDUsingPosterImage(\"%@\", \"%@\");", uniqueId, posterImageURL];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)setProgress:(double)progress onVideo:(NSString *)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.setProgressOnVideo(\"%@\", %f);", uniqueId, progress];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)replaceLocalVideoWithID:(NSString *)uniqueID
forRemoteVideo:(NSString *)videoURL
remotePoster:(NSString *)posterURL
videoPress:(NSString *)videoPressID
{
NSString * videoPressSafeID = videoPressID;
if (!videoPressSafeID) {
videoPressSafeID = @"";
}
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.replaceLocalVideoWithRemoteVideo(\"%@\", \"%@\", \"%@\", \"%@\");", uniqueID, videoURL, posterURL, videoPressSafeID];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)markVideo:(NSString *)uniqueId failedUploadWithMessage:(NSString*) message;
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.markVideoUploadFailed(\"%@\", \"%@\");", uniqueId, message];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)unmarkVideoFailedUpload:(NSString *)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.unmarkVideoUploadFailed(\"%@\");", uniqueId];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)removeVideo:(NSString*)uniqueId
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.removeVideo(\"%@\");", uniqueId];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)setVideoPress:(NSString *)videoPressID source:(NSString *)videoURL poster:(NSString *)posterURL
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.setVideoPressLinks(\"%@\", \"%@\", \"%@\");", videoPressID, videoURL, posterURL];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
- (void)pauseAllVideos
{
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.pauseAllVideos();"];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
#pragma mark - URL normalization
- (NSString*)normalizeURL:(NSString*)url
{
static NSString* const kDefaultScheme = @"http://";
static NSString* const kURLSchemePrefix = @"://";
NSString* normalizedURL = url;
NSRange substringRange = [url rangeOfString:kURLSchemePrefix];
if (substringRange.length == 0) {
normalizedURL = [kDefaultScheme stringByAppendingString:url];
}
return normalizedURL;
}
#pragma mark - Links
- (void)insertLink:(NSString *)url
title:(NSString*)title
{
NSParameterAssert([url isKindOfClass:[NSString class]]);
NSParameterAssert([title isKindOfClass:[NSString class]]);
url = [self normalizeURL:url];
if (self.isInVisualMode) {
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertLink(\"%@\",\"%@\");", url, title];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
NSString *aTagText = [NSString stringWithFormat:@"<a href=\"%@\">%@</a>", url, title];
[self.sourceView insertText:aTagText];
[self.sourceView becomeFirstResponder];
}
[self callDelegateEditorTextDidChange];
}
- (BOOL)isSelectionALink
{
return self.selectedLinkURL != nil;
}
- (void)updateLink:(NSString *)url
title:(NSString*)title
{
NSAssert(self.isInVisualMode, @"Editor must be in visual mode when calling this method.");
NSParameterAssert([url isKindOfClass:[NSString class]]);
NSParameterAssert([title isKindOfClass:[NSString class]]);
url = [self normalizeURL:url];
if (self.isInVisualMode) {
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.updateLink(\"%@\",\"%@\");", url, title];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
[self callDelegateEditorTextDidChange];
}
- (void)removeLink
{
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.unlink();"];
}
- (void)quickLink
{
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.quickLink();"];
}
#pragma mark - Editor: HTML interaction
// Inserts HTML at the caret position
- (void)insertHTML:(NSString *)html
{
NSString *cleanedHTML = [self addSlashes:html];
NSString *trigger = [NSString stringWithFormat:@"ZSSEditor.insertHTML(\"%@\");", cleanedHTML];
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
#pragma mark - Editing
- (void)wrapSourceViewSelectionWithTag:(NSString *)tag
{
NSParameterAssert([tag isKindOfClass:[NSString class]]);
NSRange range = self.sourceView.selectedRange;
NSString *selection = [self.sourceView.text substringWithRange:range];
NSString *prefix, *suffix;
if ([tag isEqualToString:@"more"]) {
prefix = @"<!--more-->";
suffix = @"\n";
} else if ([tag isEqualToString:@"blockquote"]) {
prefix = [NSString stringWithFormat:@"\n<%@>", tag];
suffix = [NSString stringWithFormat:@"</%@>\n", tag];
} else {
prefix = [NSString stringWithFormat:@"<%@>", tag];
suffix = [NSString stringWithFormat:@"</%@>", tag];
}
NSString *replacement = [NSString stringWithFormat:@"%@%@%@",prefix,selection,suffix];
[self.sourceView insertText:replacement];
}
- (void)endEditing;
{
[self.webView endEditing:YES];
[self.sourceView endEditing:YES];
}
#pragma mark - Editor mode
- (BOOL)isInVisualMode
{
return self.isEditorMode;
return !self.webView.hidden;
}
// 显示html源码
- (void)showHTMLSource
{
self.isEditorMode = NO;
// 如果是markdown, 则显示预览
if(self.isMarkdown) {
[self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.togglePreview();"];
}
else {
self.sourceView.text = [self.contentField html];
self.sourceView.hidden = NO;
self.sourceViewTitleField.text = [self.titleField strippedHtml];
self.sourceViewTitleField.hidden = NO;
self.sourceContentDividerView.hidden = NO;
self.webView.hidden = YES;
[self.sourceView becomeFirstResponder];
UITextPosition* position = [self.sourceView positionFromPosition:[self.sourceView beginningOfDocument]
offset:0];
[self.sourceView setSelectedTextRange:[self.sourceView textRangeFromPosition:position toPosition:position]];
}
}
// ? 这里有问题 self.sourceViewTitleField.text ? 导致内容消失!
- (void)showVisualEditor
{
self.isEditorMode = YES;
if(self.isMarkdown) {
// [self.webView stringByEvaluatingJavaScriptFromString:@"ZSSEditor.toggleWrite();"];
}
else {
[self.contentField setHtml:self.sourceView.text];
self.sourceView.hidden = YES;
[self.titleField setHtml:self.sourceViewTitleField.text];
self.sourceViewTitleField.hidden = YES;
self.sourceContentDividerView.hidden = YES;
self.webView.hidden = NO;
[self.contentField focus];
}
}
#pragma mark - Editing lock
// 关闭编辑
- (void)disableEditing
{
// 关闭编辑, 如果之前是source view, 则切换到editor view
if (!self.sourceView.hidden) {
[self showVisualEditor];
}
[self.titleField disableEditing];
[self.contentField disableEditing];
[self.sourceViewTitleField setEnabled:NO];
[self.sourceView setEditable:NO];
}
- (void)enableEditing
{
[self.titleField enableEditing];
[self.contentField enableEditing];
[self.sourceViewTitleField setEnabled:YES];
[self.sourceView setEditable:YES];
}
#pragma mark - Styles
- (void)alignLeft
{
NSString *trigger = @"ZSSEditor.setJustifyLeft();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)alignCenter
{
NSString *trigger = @"ZSSEditor.setJustifyCenter();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)alignRight
{
NSString *trigger = @"ZSSEditor.setJustifyRight();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)alignFull
{
NSString *trigger = @"ZSSEditor.setJustifyFull();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)setBold
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setBold();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"b"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setBlockQuote
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setBlockquote();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"blockquote"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setItalic
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setItalic();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"i"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setSubscript
{
NSString *trigger = @"ZSSEditor.setSubscript();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)setUnderline
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setUnderline();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"u"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setSuperscript
{
NSString *trigger = @"ZSSEditor.setSuperscript();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)setStrikethrough
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setStrikeThrough();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"del"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setUnorderedList
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setUnorderedList();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"ul"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setOrderedList
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setOrderedList();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"ol"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setHR
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setHorizontalRule();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
} else {
[self wrapSourceViewSelectionWithTag:@"hr"];
}
[self callDelegateEditorTextDidChange];
}
- (void)setIndent
{
NSString *trigger = @"ZSSEditor.setIndent();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)setOutdent
{
NSString *trigger = @"ZSSEditor.setOutdent();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)heading1
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setHeading('h1');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
else {
[self wrapSourceViewSelectionWithTag:@"h1"];
}
[self callDelegateEditorTextDidChange];
}
- (void)heading2
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setHeading('h2');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
else {
[self wrapSourceViewSelectionWithTag:@"h2"];
}
[self callDelegateEditorTextDidChange];
}
- (void)heading3
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setHeading('h3');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
else {
[self wrapSourceViewSelectionWithTag:@"h3"];
}
[self callDelegateEditorTextDidChange];
}
- (void)heading4
{
if (self.isInVisualMode) {
NSString *trigger = @"ZSSEditor.setHeading('h4');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
}
else {
[self wrapSourceViewSelectionWithTag:@"h4"];
}
[self callDelegateEditorTextDidChange];
}
- (void)heading5
{
NSString *trigger = @"ZSSEditor.setHeading('h5');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)heading6
{
NSString *trigger = @"ZSSEditor.setHeading('h6');";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)newline
{
NSString *trigger = @"ZSSEditor.newline()";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
- (void)removeFormat
{
NSString *trigger = @"ZSSEditor.removeFormating();";
[self.webView stringByEvaluatingJavaScriptFromString:trigger];
[self callDelegateEditorTextDidChange];
}
#pragma mark - UITextViewDelegate
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
[self callDelegateSourceFieldFocused:textView];
return YES;
}
- (void)textViewDidChange:(UITextView *)textView
{
[self callDelegateEditorTextDidChange];
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
return YES;
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
[self callDelegateSourceFieldFocused:textField];
return YES;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
textField.text = [textField.text stringByReplacingCharactersInRange:range withString:string];
[self callDelegateEditorTitleDidChange];
return NO;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self.sourceView becomeFirstResponder];
return NO;
}
#pragma mark - Delegate calls
/**
* @brief Call's the delegate editorTextDidChange: method.
*/
- (void)callDelegateEditorTextDidChange
{
if ([self.delegate respondsToSelector: @selector(editorTextDidChange:)]) {
[self.delegate editorTextDidChange:self];
}
}
/**
* @brief Call's the delegate editorTitleDidChange: method.
*/
- (void)callDelegateEditorTitleDidChange
{
if ([self.delegate respondsToSelector: @selector(editorTitleDidChange:)]) {
[self.delegate editorTitleDidChange:self];
}
}
/**
* @brief Call's the delegate editorView:fieldCreated: method.
*/
- (void)callDelegateFieldCreated:(WPEditorField*)field
{
NSParameterAssert([field isKindOfClass:[WPEditorField class]]);
if ([self.delegate respondsToSelector:@selector(editorView:fieldCreated:)]) {
[self.delegate editorView:self fieldCreated:field];
}
}
/**
* @brief Call's the delegate editorView:fieldFocused: method.
*/
// 调用WPEditorViewController中的
- (void)callDelegateFieldFocused:(WPEditorField*)field
{
if ([self.delegate respondsToSelector:@selector(editorView:fieldFocused:)]) {
[self.delegate editorView:self fieldFocused:field];
}
}
/**
* @brief Call's the delegate editorView:sourceFieldFocused: method.
*/
- (void)callDelegateSourceFieldFocused:(UIView*)view
{
if ([self.delegate respondsToSelector:@selector(editorView:sourceFieldFocused:)]) {
[self.delegate editorView:self sourceFieldFocused:view];
}
}
@end