mirror of
https://github.com/leanote/leanote-ios.git
synced 2025-10-15 15:40:44 +00:00
2263 lines
74 KiB
Objective-C
Executable File
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
|