API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPWebView.j
Go to the documentation of this file.
1 /*
2  * CPWebView.j
3  * AppKit
4  *
5  * Created by Thomas Robinson.
6  * Copyright 2008, 280 North, Inc.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21  */
22 
23 
24 
25 // FIXME: implement these where possible:
26 /*
27 CPWebViewDidBeginEditingNotification = "CPWebViewDidBeginEditingNotification";
28 CPWebViewDidChangeNotification = "CPWebViewDidChangeNotification";
29 CPWebViewDidChangeSelectionNotification = "CPWebViewDidChangeSelectionNotification";
30 CPWebViewDidChangeTypingStyleNotification = "CPWebViewDidChangeTypingStyleNotification";
31 CPWebViewDidEndEditingNotification = "CPWebViewDidEndEditingNotification";
32 CPWebViewProgressEstimateChangedNotification = "CPWebViewProgressEstimateChangedNotification";
33 */
34 CPWebViewProgressStartedNotification = "CPWebViewProgressStartedNotification";
35 CPWebViewProgressFinishedNotification = "CPWebViewProgressFinishedNotification";
36 
58 
64 
74 
89 @implementation CPWebView : CPView
90 {
91  CPScrollView _scrollView;
92  CPView _frameView;
93 
94  IFrame _iframe;
95  CPString _mainFrameURL;
96  CPArray _backwardStack;
97  CPArray _forwardStack;
98 
99  BOOL _ignoreLoadStart;
100  BOOL _ignoreLoadEnd;
101  BOOL _isLoading;
102 
103  id _downloadDelegate;
104  id _frameLoadDelegate;
105  id _policyDelegate;
106  id _resourceLoadDelegate;
107  id _UIDelegate;
108 
109  CPWebScriptObject _wso;
110 
111  CPString _url;
112  CPString _html;
113 
114  Function _loadCallback;
115 
116  int _scrollMode;
117  int _effectiveScrollMode;
118  BOOL _contentIsAccessible;
119  CPTimer _contentSizeCheckTimer;
120  int _contentSizePollCount;
121 
122  int _loadHTMLStringTimer;
123 
124  BOOL _drawsBackground;
125 }
126 
127 - (id)initWithFrame:(CGRect)frameRect frameName:(CPString)frameName groupName:(CPString)groupName
128 {
129  if (self = [self initWithFrame:frameRect])
130  {
131  _iframe.name = frameName;
132  }
133 
134  return self;
135 }
136 
137 - (id)initWithFrame:(CGRect)aFrame
138 {
139  if (self = [super initWithFrame:aFrame])
140  {
141  _mainFrameURL = nil;
142  _backwardStack = [];
143  _forwardStack = [];
144  _scrollMode = CPWebViewScrollAuto;
145  _contentIsAccessible = YES;
146  _isLoading = NO;
147 
148  _drawsBackground = YES;
149 
151 
152  [self _initDOMWithFrame:aFrame];
153  }
154 
155  return self;
156 }
157 
158 - (id)_initDOMWithFrame:(CGRect)aFrame
159 {
160  _ignoreLoadStart = YES;
161  _ignoreLoadEnd = YES;
162 
163  _iframe = document.createElement("iframe");
164  _iframe.name = "iframe_" + FLOOR(RAND() * 10000);
165  _iframe.style.width = "100%";
166  _iframe.style.height = "100%";
167  _iframe.style.borderWidth = "0px";
168  _iframe.frameBorder = "0";
169 
170  [self _applyBackgroundColor];
171 
172  _loadCallback = function()
173  {
174  // HACK: this block handles the case where we don't know about loads initiated by the user clicking a link
175  if (!_ignoreLoadStart)
176  {
177  // post the start load notification
178  [self _startedLoading];
179 
180  if (_mainFrameURL)
181  [_backwardStack addObject:_mainFrameURL];
182 
183  // FIXME: this doesn't actually get the right URL for different domains. Not possible due to browser security restrictions.
184  _mainFrameURL = _iframe.src;
185 
186  // clear the forward
187  [_forwardStack removeAllObjects];
188  }
189  else
190  _ignoreLoadStart = NO;
191 
192  if (!_ignoreLoadEnd)
193  {
194  [self _finishedLoading];
195  }
196  else
197  _ignoreLoadEnd = NO;
198 
199  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
200  };
201 
202  if (_iframe.addEventListener)
203  _iframe.addEventListener("load", _loadCallback, false);
204  else if (_iframe.attachEvent)
205  _iframe.attachEvent("onload", _loadCallback);
206 
207  _frameView = [[CPView alloc] initWithFrame:[self bounds]];
208  [_frameView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
209 
210  _scrollView = [[CPScrollView alloc] initWithFrame:[self bounds]];
211  [_scrollView setAutohidesScrollers:YES];
212  [_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
213  [_scrollView setDocumentView:_frameView];
214 
215  _frameView._DOMElement.appendChild(_iframe);
216 
217  [self _updateEffectiveScrollMode];
218 
219  [self addSubview:_scrollView];
220 }
221 
222 - (void)setFrameSize:(CGSize)aSize
223 {
224  [super setFrameSize:aSize];
225  [self _resizeWebFrame];
226 }
227 
228 - (void)viewDidUnhide
229 {
230  // Sizing cannot properly happen while we're hidden because the iframe is inaccessible.
231  // So now that it is accessible again, make sure to catch up.
232  [_frameView setFrameSize:[_scrollView contentSize]];
233  [self _resizeWebFrame];
234  [self _scheduleContentSizeCheck];
235 }
236 
237 - (void)_attachScrollEventIfNecessary
238 {
239  if (_effectiveScrollMode !== CPWebViewScrollAppKit)
240  return;
241 
242  var win = [self DOMWindow];
243 
244  if (win && win.addEventListener)
245  {
246  var scrollEventHandler = function(anEvent)
247  {
248  var frameBounds = [self bounds],
249  frameCenter = CGPointMake(CGRectGetMidX(frameBounds), CGRectGetMidY(frameBounds)),
250  windowOrigin = [self convertPoint:frameCenter toView:nil],
251  globalOrigin = [[self window] convertBaseToBridge:windowOrigin];
252 
253  anEvent._overrideLocation = globalOrigin;
254  [[[self window] platformWindow] scrollEvent:anEvent];
255  };
256 
257  win.addEventListener("DOMMouseScroll", scrollEventHandler, false);
258  win.addEventListener("wheel", scrollEventHandler, false);
259  }
260 }
261 
262 - (void)_resizeWebFrame
263 {
264  // When a webview is not in the DOM we can't inspect its contents for sizing information.
265  // If we try, we might end up setting the fallback frame size which will then become
266  // somewhat sticky.
267  if (![self _isVisible])
268  {
269  return;
270  }
271 
272  if (_effectiveScrollMode === CPWebViewScrollAppKit)
273  {
274  var visibleRect = [_frameView visibleRect];
275  [_frameView setFrameSize:CGSizeMake(CGRectGetMaxX(visibleRect), CGRectGetMaxY(visibleRect))];
276 
277  // try to get the document size so we can correctly set the frame
278  var win = [self DOMWindow];
279 
280  if (win && win.document && win.document.body)
281  {
282  var width = win.document.body.scrollWidth,
283  height = win.document.body.scrollHeight;
284 
285  _iframe.setAttribute("width", width);
286  _iframe.setAttribute("height", height);
287 
288  [_frameView setFrameSize:CGSizeMake(width, height)];
289  }
290  else
291  {
292  // If we do have access to the content, it might be that the 'body' element simply hasn't loaded yet.
293  // The size will be updated by the content size timer in this case.
294  if (!win || !win.document)
295  {
296  CPLog.warn("using default size 800*1600");
297  [_frameView setFrameSize:CGSizeMake(800, 1600)];
298  }
299  }
300 
301  [_frameView scrollRectToVisible:visibleRect];
302  }
303 }
304 
316 - (void)setScrollMode:(int)aScrollMode
317 {
318  if (_scrollMode == aScrollMode)
319  return;
320 
321  _scrollMode = aScrollMode;
322 
323  [self _updateEffectiveScrollMode];
324 }
325 
335 - (int)effectiveScrollMode
336 {
337  return _effectiveScrollMode;
338 }
339 
340 - (void)_updateEffectiveScrollMode
341 {
342  var _newScrollMode = CPWebViewScrollAppKit;
343 
344  if (_scrollMode == CPWebViewScrollNative
345  || (_scrollMode == CPWebViewScrollAuto && !_contentIsAccessible)
347  {
348  _newScrollMode = CPWebViewScrollNative;
349  }
350  else if (_scrollMode == CPWebViewScrollAppKit && !_contentIsAccessible)
351  {
352  // Same behaviour as the previous case except that a warning is logged when AppKit
353  // scrollers can't be used.
354  CPLog.warn(self + " unable to use CPWebViewScrollAppKit scroll mode due to same origin policy.");
355  _newScrollMode = CPWebViewScrollNative;
356  }
357 
358  if (_newScrollMode !== _effectiveScrollMode)
359  [self _setEffectiveScrollMode:_newScrollMode];
360 }
361 
362 - (void)_setEffectiveScrollMode:(int)aScrollMode
363 {
364  _effectiveScrollMode = aScrollMode;
365 
366  _ignoreLoadStart = YES;
367  _ignoreLoadEnd = YES;
368 
369  var parent = _iframe.parentNode;
370  // FIXME "scrolling" can't be changed without readding the iframe. Unfortunately this causes a reload.
371  parent.removeChild(_iframe);
372 
373  if (_effectiveScrollMode === CPWebViewScrollAppKit)
374  {
375  [_scrollView setHasHorizontalScroller:YES];
376  [_scrollView setHasVerticalScroller:YES];
377 
378  _iframe.setAttribute("scrolling", "no");
379  }
380  else if (_effectiveScrollMode === CPWebViewScrollNone)
381  {
382  [_scrollView setHasHorizontalScroller:NO];
383  [_scrollView setHasVerticalScroller:NO];
384 
385  _iframe.setAttribute("scrolling", "no");
386  }
387  else
388  {
389  [_scrollView setHasHorizontalScroller:NO];
390  [_scrollView setHasVerticalScroller:NO];
391 
392  _iframe.setAttribute("scrolling", "auto");
393 
394  [_frameView setFrameSize:[_scrollView bounds].size];
395  }
396 
397  parent.appendChild(_iframe);
398  [self _applyBackgroundColor];
399 
400  [self _resizeWebFrame];
401 }
402 
403 - (void)_maybePollWebFrameSize
404 {
405  if (CPWebViewAppKitScrollMaxPollCount == 0 || _contentSizePollCount++ < CPWebViewAppKitScrollMaxPollCount)
406  [self _resizeWebFrame];
407  else
408  [_contentSizeCheckTimer invalidate];
409 }
410 
416 - (void)loadHTMLString:(CPString)aString
417 {
418  [self loadHTMLString:aString baseURL:nil];
419 }
420 
427 - (void)loadHTMLString:(CPString)aString baseURL:(CPURL)URL
428 {
429  // FIXME: do something with baseURL?
430  [_frameView setFrameSize:[_scrollView contentSize]];
431 
432  [self _startedLoading];
433 
434  _ignoreLoadStart = YES;
435 
436  _url = nil;
437  _html = aString;
438 
439  [self _load];
440 }
441 
442 - (void)_loadMainFrameURL
443 {
444  [self _startedLoading];
445 
446  _ignoreLoadStart = YES;
447 
448  _url = _mainFrameURL;
449  _html = nil;
450 
451  [self _load];
452 }
453 
454 - (void)_load
455 {
456  if (_url)
457  {
458  // Try to figure out if this URL will pass the same origin policy and hence allow us to potentially
459  // use appkit scrollbars.
460  var cpurl = [CPURL URLWithString:_url];
461  _contentIsAccessible = [cpurl _passesSameOriginPolicy];
462  [self _updateEffectiveScrollMode];
463 
464  _ignoreLoadEnd = NO;
465 
466  _iframe.src = _url;
467  }
468  else if (_html !== nil)
469  {
470  // clear the iframe
471  _iframe.src = "";
472 
473  _contentIsAccessible = YES;
474  [self _updateEffectiveScrollMode];
475 
476  _ignoreLoadEnd = NO;
477 
478  if (_loadHTMLStringTimer !== nil)
479  {
480  window.clearTimeout(_loadHTMLStringTimer);
481  _loadHTMLStringTimer = nil;
482  }
483 
484  // need to give the browser a chance to reset iframe, otherwise we'll be document.write()-ing the previous document
485  _loadHTMLStringTimer = window.setTimeout(function()
486  {
487  var win = [self DOMWindow];
488 
489  /*
490  If _html is the empty string, subtitute in an empty HTML structure. Just leaving the contents entirely empty prompts the browser to subtitute in a white page which would interfere with any custom background colours in use by this web view.
491  */
492  if (win)
493  win.document.write(_html || "<html><body></body></html>");
494 
495  window.setTimeout(_loadCallback, 1);
496  }, 0);
497  }
498 }
499 
500 - (void)_startedLoading
501 {
502  _isLoading = YES;
503 
504  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressStartedNotification object:self];
505 
506  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didStartProvisionalLoadForFrame:)])
507  [_frameLoadDelegate webView:self didStartProvisionalLoadForFrame:nil]; // FIXME: give this a frame somehow?
508 }
509 
510 - (void)_finishedLoading
511 {
512  _isLoading = NO;
513 
514  [self _resizeWebFrame];
515  [self _attachScrollEventIfNecessary];
516 
517  [self _scheduleContentSizeCheck];
518 
519  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressFinishedNotification object:self];
520 
521  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didFinishLoadForFrame:)])
522  [_frameLoadDelegate webView:self didFinishLoadForFrame:nil]; // FIXME: give this a frame somehow?
523 }
524 
525 - (void)_scheduleContentSizeCheck
526 {
527  [_contentSizeCheckTimer invalidate];
528  if (_effectiveScrollMode == CPWebViewScrollAppKit)
529  {
530  /*
531  FIXME Need better method.
532  We don't know when the content of the iframe changes size (e.g. a
533  picture finishes loading, dynamic content is loaded). Often when a
534  page has initially 'loaded', it does not yet have its final size. In
535  lieu of any resize events we will simply check back in a few times
536  some time after loading.
537 
538  We run these checks only a limited number of times as to not deplete
539  battery life and slow down the software needlessly. This does mean
540  there are situations where the content changes size and the AppKit
541  scrollbars will be out of sync. Users who have dynamic content
542  in their web view will, for now, have to implement domain specific
543  fixes.
544  */
545 
546  _contentSizePollCount = 0;
547  _contentSizeCheckTimer = [CPTimer scheduledTimerWithTimeInterval:CPWebViewAppKitScrollPollInterval target:self selector:@selector(_maybePollWebFrameSize) userInfo:nil repeats:YES];
548  }
549 }
550 
555 - (BOOL)isLoading
556 {
557  return _isLoading;
558 }
559 
565 - (CPString)mainFrameURL
566 {
567  return _mainFrameURL;
568 }
569 
575 - (void)setMainFrameURL:(CPString)URLString
576 {
577  if (_mainFrameURL)
578  [_backwardStack addObject:_mainFrameURL];
579 
580  _mainFrameURL = URLString;
581  [_forwardStack removeAllObjects];
582 
583  [self _loadMainFrameURL];
584 }
585 
591 - (BOOL)goBack
592 {
593  if (_backwardStack.length > 0)
594  {
595  if (_mainFrameURL)
596  [_forwardStack addObject:_mainFrameURL];
597 
598  _mainFrameURL = [_backwardStack lastObject];
599  [_backwardStack removeLastObject];
600 
601  [self _loadMainFrameURL];
602 
603  return YES;
604  }
605 
606  return NO;
607 }
608 
614 - (BOOL)goForward
615 {
616  if (_forwardStack.length > 0)
617  {
618  if (_mainFrameURL)
619  [_backwardStack addObject:_mainFrameURL];
620 
621  _mainFrameURL = [_forwardStack lastObject];
622  [_forwardStack removeLastObject];
623 
624  [self _loadMainFrameURL];
625 
626  return YES;
627  }
628 
629  return NO;
630 }
631 
638 - (BOOL)canGoBack
639 {
640  return (_backwardStack.length > 0);
641 }
642 
649 - (BOOL)canGoForward
650 {
651  return (_forwardStack.length > 0);
652 }
653 
654 - (WebBackForwardList)backForwardList
655 {
656  // FIXME: return a real WebBackForwardList?
657  return { back: _backwardStack, forward: _forwardStack };
658 }
659 
664 - (void)close
665 {
666  _iframe.parentNode.removeChild(_iframe);
667 }
668 
674 - (DOMWindow)DOMWindow
675 {
676  try
677  {
678  return (_iframe.contentDocument && _iframe.contentDocument.defaultView) || _iframe.contentWindow;
679  }
680  catch (e)
681  {
682  return nil;
683  }
684 }
685 
691 - (CPWebScriptObject)windowScriptObject
692 {
693  var win = [self DOMWindow];
694 
695  if (!_wso || win != [_wso window])
696  {
697  if (win)
698  _wso = [[CPWebScriptObject alloc] initWithWindow:win];
699  else
700  _wso = nil;
701  }
702 
703  return _wso;
704 }
705 
713 - (CPString)stringByEvaluatingJavaScriptFromString:(CPString)script
714 {
715  var result = [self objectByEvaluatingJavaScriptFromString:script];
716  return result ? String(result) : nil;
717 }
718 
725 - (JSObject)objectByEvaluatingJavaScriptFromString:(CPString)script
726 {
727  return [[self windowScriptObject] evaluateWebScript:script];
728 }
729 
737 - (DOMCSSStyleDeclaration)computedStyleForElement:(DOMElement)element pseudoElement:(CPString)pseudoElement
738 {
739  var win = [[self windowScriptObject] window];
740 
741  if (win)
742  {
743  // FIXME: IE version?
744  return win.document.defaultView.getComputedStyle(element, pseudoElement);
745  }
746  return nil;
747 }
748 
749 
753 - (BOOL)drawsBackground
754 {
755  return _drawsBackground;
756 }
757 
769 - (void)setDrawsBackground:(BOOL)drawsBackground
770 {
771  if (drawsBackground == _drawsBackground)
772  return;
773 
774  _drawsBackground = drawsBackground;
775 
776  [self _applyBackgroundColor];
777 }
778 
779 - (void)setBackgroundColor:(CPColor)aColor
780 {
781  [super setBackgroundColor:aColor];
782  [self _applyBackgroundColor];
783 }
784 
785 - (void)_applyBackgroundColor
786 {
787  if (_iframe)
788  {
789  var bgColor = [self backgroundColor] || [CPColor whiteColor];
790  _iframe.allowtransparency = !_drawsBackground;
791  _iframe.style.backgroundColor = _drawsBackground ? [bgColor cssString] : "transparent";
792  }
793 }
794 
795 // IBActions
796 
803 - (@action)takeStringURLFrom:(id)sender
804 {
805  [self setMainFrameURL:[sender stringValue]];
806 }
807 
813 - (@action)goBack:(id)sender
814 {
815  [self goBack];
816 }
817 
823 - (@action)goForward:(id)sender
824 {
825  [self goForward];
826 }
827 
833 - (@action)stopLoading:(id)sender
834 {
835  // FIXME: what to do?
836 }
837 
843 - (@action)reload:(id)sender
844 {
845  // If we're displaying pure HTML, redisplay it.
846  if (!_url && (_html !== nil))
847  [self loadHTMLString:_html];
848  else
849  [self _loadMainFrameURL];
850 }
851 
858 - (@action)print:(id)sender
859 {
860  try
861  {
862  [self DOMWindow].print();
863  }
864  catch (e)
865  {
866  alert('Please click the webpage and select "Print" from the "File" menu');
867  }
868 }
869 
870 
871 // Delegates:
872 
873 // FIXME: implement more delegates, though most of these will likely never work with the iframe implementation
874 
875 - (id)downloadDelegate
876 {
877  return _downloadDelegate;
878 }
879 
880 - (void)setDownloadDelegate:(id)anObject
881 {
882  _downloadDelegate = anObject;
883 }
884 
885 - (id)frameLoadDelegate
886 {
887  return _frameLoadDelegate;
888 }
889 
890 - (void)setFrameLoadDelegate:(id)anObject
891 {
892  _frameLoadDelegate = anObject;
893 }
894 
895 - (id)policyDelegate
896 {
897  return _policyDelegate;
898 }
899 
900 - (void)setPolicyDelegate:(id)anObject
901 {
902  _policyDelegate = anObject;
903 }
904 
905 - (id)resourceLoadDelegate
906 {
907  return _resourceLoadDelegate;
908 }
909 
910 - (void)setResourceLoadDelegate:(id)anObject
911 {
912  _resourceLoadDelegate = anObject;
913 }
914 
915 - (id)UIDelegate
916 {
917  return _UIDelegate;
918 }
919 
920 - (void)setUIDelegate:(id)anObject
921 {
922  _UIDelegate = anObject;
923 }
924 
925 @end
926 
932 @implementation CPWebScriptObject : CPObject
933 {
934  Window _window;
935 }
936 
940 - (id)initWithWindow:(Window)aWindow
941 {
942  if (self = [super init])
943  {
944  _window = aWindow;
945  }
946 
947  return self;
948 }
949 
956 - (id)callWebScriptMethod:(CPString)methodName withArguments:(CPArray)args
957 {
958  // Would using "with" be better here?
959  if (typeof _window[methodName] == "function")
960  {
961  try
962  {
963  return _window[methodName].apply(_window, args);
964  }
965  catch (e)
966  {
967  }
968  }
969  return undefined;
970 }
971 
978 - (id)evaluateWebScript:(CPString)script
979 {
980  try
981  {
982  return _window.eval(script);
983  }
984  catch (e)
985  {
986  // FIX ME: if we fail inside here, shouldn't we return an exception?
987  }
988 
989  return undefined;
990 }
991 
995 - (Window)window
996 {
997  return _window;
998 }
999 
1000 @end
1001 
1002 
1003 @implementation CPWebView (CPCoding)
1004 
1011 - (id)initWithCoder:(CPCoder)aCoder
1012 {
1013  self = [super initWithCoder:aCoder];
1014 
1015  if (self)
1016  {
1017  // FIXME: encode/decode these?
1018  _mainFrameURL = nil;
1019  _backwardStack = [];
1020  _forwardStack = [];
1021  _scrollMode = CPWebViewScrollAuto;
1022 
1023 #if PLATFORM(DOM)
1024  [self _initDOMWithFrame:[self frame]];
1025 #endif
1026 
1027  if (![self backgroundColor])
1029 
1030  [self _updateEffectiveScrollMode];
1031  }
1032 
1033  return self;
1034 }
1035 
1041 - (void)encodeWithCoder:(CPCoder)aCoder
1042 {
1043  var actualSubviews = _subviews;
1044  _subviews = [];
1045  [super encodeWithCoder:aCoder];
1046  _subviews = actualSubviews;
1047 }
1048 
1049 @end
1050 
1051 @implementation CPURL(SOP)
1052 
1060 - (BOOL)_passesSameOriginPolicy
1061 {
1062  var documentURL = [CPURL URLWithString:window.location.href];
1063 
1064  if ([documentURL isFileURL] && CPFeatureIsCompatible(CPSOPDisabledFromFileURLs))
1065  return YES;
1066 
1067  // Relative URLs always pass the SOP.
1068  if (![self scheme] && ![self host] && ![self port])
1069  return YES;
1070 
1071  return ([documentURL scheme] == [self scheme] && [documentURL host] == [self host] && [documentURL port] == [self port]);
1072 }
1073 
1074 @end