API  0.9.6
 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:(CPRect)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:(CPRect)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:(CPRect)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:(CPSize)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 = null;
243  try { win = [self DOMWindow]; } catch (e) {}
244 
245  if (win && win.addEventListener)
246  {
247  var scrollEventHandler = function(anEvent)
248  {
249  var frameBounds = [self bounds],
250  frameCenter = CGPointMake(CGRectGetMidX(frameBounds), CGRectGetMidY(frameBounds)),
251  windowOrigin = [self convertPoint:frameCenter toView:nil],
252  globalOrigin = [[self window] convertBaseToBridge:windowOrigin];
253 
254  anEvent._overrideLocation = globalOrigin;
255  [[[self window] platformWindow] scrollEvent:anEvent];
256  };
257 
258  win.addEventListener("DOMMouseScroll", 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 = null;
279  try { win = [self DOMWindow]; } catch (e) {}
280 
281  if (win && win.document && win.document.body)
282  {
283  var width = win.document.body.scrollWidth,
284  height = win.document.body.scrollHeight;
285 
286  _iframe.setAttribute("width", width);
287  _iframe.setAttribute("height", height);
288 
289  [_frameView setFrameSize:CGSizeMake(width, height)];
290  }
291  else
292  {
293  // If we do have access to the content, it might be that the 'body' element simply hasn't loaded yet.
294  // The size will be updated by the content size timer in this case.
295  if (!win || !win.document)
296  {
297  CPLog.warn("using default size 800*1600");
298  [_frameView setFrameSize:CGSizeMake(800, 1600)];
299  }
300  }
301 
302  [_frameView scrollRectToVisible:visibleRect];
303  }
304 }
305 
317 - (void)setScrollMode:(int)aScrollMode
318 {
319  if (_scrollMode == aScrollMode)
320  return;
321 
322  _scrollMode = aScrollMode;
323 
324  [self _updateEffectiveScrollMode];
325 }
326 
336 - (int)effectiveScrollMode
337 {
338  return _effectiveScrollMode;
339 }
340 
341 - (void)_updateEffectiveScrollMode
342 {
343  var _newScrollMode = CPWebViewScrollAppKit;
344 
345  if (_scrollMode == CPWebViewScrollNative
346  || (_scrollMode == CPWebViewScrollAuto && !_contentIsAccessible)
348  {
349  _newScrollMode = CPWebViewScrollNative;
350  }
351  else if (_scrollMode == CPWebViewScrollAppKit && !_contentIsAccessible)
352  {
353  // Same behaviour as the previous case except that a warning is logged when AppKit
354  // scrollers can't be used.
355  CPLog.warn(self + " unable to use CPWebViewScrollAppKit scroll mode due to same origin policy.");
356  _newScrollMode = CPWebViewScrollNative;
357  }
358 
359  if (_newScrollMode !== _effectiveScrollMode)
360  [self _setEffectiveScrollMode:_newScrollMode];
361 }
362 
363 - (void)_setEffectiveScrollMode:(int)aScrollMode
364 {
365  _effectiveScrollMode = aScrollMode;
366 
367  _ignoreLoadStart = YES;
368  _ignoreLoadEnd = YES;
369 
370  var parent = _iframe.parentNode;
371  // FIXME "scrolling" can't be changed without readding the iframe. Unfortunately this causes a reload.
372  parent.removeChild(_iframe);
373 
374  if (_effectiveScrollMode === CPWebViewScrollAppKit)
375  {
376  [_scrollView setHasHorizontalScroller:YES];
377  [_scrollView setHasVerticalScroller:YES];
378 
379  _iframe.setAttribute("scrolling", "no");
380  }
381  else if (_effectiveScrollMode === CPWebViewScrollNone)
382  {
383  [_scrollView setHasHorizontalScroller:NO];
384  [_scrollView setHasVerticalScroller:NO];
385 
386  _iframe.setAttribute("scrolling", "no");
387  }
388  else
389  {
390  [_scrollView setHasHorizontalScroller:NO];
391  [_scrollView setHasVerticalScroller:NO];
392 
393  _iframe.setAttribute("scrolling", "auto");
394 
395  [_frameView setFrameSize:[_scrollView bounds].size];
396  }
397 
398  parent.appendChild(_iframe);
399  [self _applyBackgroundColor];
400 
401  [self _resizeWebFrame];
402 }
403 
404 - (void)_maybePollWebFrameSize
405 {
406  if (CPWebViewAppKitScrollMaxPollCount == 0 || _contentSizePollCount++ < CPWebViewAppKitScrollMaxPollCount)
407  [self _resizeWebFrame];
408  else
409  [_contentSizeCheckTimer invalidate];
410 }
411 
417 - (void)loadHTMLString:(CPString)aString
418 {
419  [self loadHTMLString:aString baseURL:nil];
420 }
421 
428 - (void)loadHTMLString:(CPString)aString baseURL:(CPURL)URL
429 {
430  // FIXME: do something with baseURL?
431  [_frameView setFrameSize:[_scrollView contentSize]];
432 
433  [self _startedLoading];
434 
435  _ignoreLoadStart = YES;
436 
437  _url = nil;
438  _html = aString;
439 
440  [self _load];
441 }
442 
443 - (void)_loadMainFrameURL
444 {
445  [self _startedLoading];
446 
447  _ignoreLoadStart = YES;
448 
449  _url = _mainFrameURL;
450  _html = nil;
451 
452  [self _load];
453 }
454 
455 - (void)_load
456 {
457  if (_url)
458  {
459  // Try to figure out if this URL will pass the same origin policy and hence allow us to potentially
460  // use appkit scrollbars.
461  var cpurl = [CPURL URLWithString:_url];
462  _contentIsAccessible = [cpurl _passesSameOriginPolicy];
463  [self _updateEffectiveScrollMode];
464 
465  _ignoreLoadEnd = NO;
466 
467  _iframe.src = _url;
468  }
469  else if (_html !== nil)
470  {
471  // clear the iframe
472  _iframe.src = "";
473 
474  _contentIsAccessible = YES;
475  [self _updateEffectiveScrollMode];
476 
477  _ignoreLoadEnd = NO;
478 
479  if (_loadHTMLStringTimer !== nil)
480  {
481  window.clearTimeout(_loadHTMLStringTimer);
482  _loadHTMLStringTimer = nil;
483  }
484 
485  // need to give the browser a chance to reset iframe, otherwise we'll be document.write()-ing the previous document
486  _loadHTMLStringTimer = window.setTimeout(function()
487  {
488  var win = [self DOMWindow];
489 
490  /*
491  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.
492  */
493  if (win)
494  win.document.write(_html || "<html><body></body></html>");
495 
496  window.setTimeout(_loadCallback, 1);
497  }, 0);
498  }
499 }
500 
501 - (void)_startedLoading
502 {
503  _isLoading = YES;
504 
505  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressStartedNotification object:self];
506 
507  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didStartProvisionalLoadForFrame:)])
508  [_frameLoadDelegate webView:self didStartProvisionalLoadForFrame:nil]; // FIXME: give this a frame somehow?
509 }
510 
511 - (void)_finishedLoading
512 {
513  _isLoading = NO;
514 
515  [self _resizeWebFrame];
516  [self _attachScrollEventIfNecessary];
517 
518  [self _scheduleContentSizeCheck];
519 
520  [[CPNotificationCenter defaultCenter] postNotificationName:CPWebViewProgressFinishedNotification object:self];
521 
522  if ([_frameLoadDelegate respondsToSelector:@selector(webView:didFinishLoadForFrame:)])
523  [_frameLoadDelegate webView:self didFinishLoadForFrame:nil]; // FIXME: give this a frame somehow?
524 }
525 
526 - (void)_scheduleContentSizeCheck
527 {
528  [_contentSizeCheckTimer invalidate];
529  if (_effectiveScrollMode == CPWebViewScrollAppKit)
530  {
531  /*
532  FIXME Need better method.
533  We don't know when the content of the iframe changes size (e.g. a
534  picture finishes loading, dynamic content is loaded). Often when a
535  page has initially 'loaded', it does not yet have its final size. In
536  lieu of any resize events we will simply check back in a few times
537  some time after loading.
538 
539  We run these checks only a limited number of times as to not deplete
540  battery life and slow down the software needlessly. This does mean
541  there are situations where the content changes size and the AppKit
542  scrollbars will be out of sync. Users who have dynamic content
543  in their web view will, for now, have to implement domain specific
544  fixes.
545  */
546 
547  _contentSizePollCount = 0;
548  _contentSizeCheckTimer = [CPTimer scheduledTimerWithTimeInterval:CPWebViewAppKitScrollPollInterval target:self selector:@selector(_maybePollWebFrameSize) userInfo:nil repeats:YES];
549  }
550 }
551 
556 - (BOOL)isLoading
557 {
558  return _isLoading;
559 }
560 
566 - (CPString)mainFrameURL
567 {
568  return _mainFrameURL;
569 }
570 
576 - (void)setMainFrameURL:(CPString)URLString
577 {
578  if (_mainFrameURL)
579  [_backwardStack addObject:_mainFrameURL];
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  _mainFrameURL = [_backwardStack lastObject];
598  [_backwardStack removeLastObject];
599 
600  [self _loadMainFrameURL];
601 
602  return YES;
603  }
604  return NO;
605 }
606 
612 - (BOOL)goForward
613 {
614  if (_forwardStack.length > 0)
615  {
616  if (_mainFrameURL)
617  [_backwardStack addObject:_mainFrameURL];
618  _mainFrameURL = [_forwardStack lastObject];
619  [_forwardStack removeLastObject];
620 
621  [self _loadMainFrameURL];
622 
623  return YES;
624  }
625  return NO;
626 }
627 
634 - (BOOL)canGoBack
635 {
636  return (_backwardStack.length > 0);
637 }
638 
645 - (BOOL)canGoForward
646 {
647  return (_forwardStack.length > 0);
648 }
649 
650 - (WebBackForwardList)backForwardList
651 {
652  // FIXME: return a real WebBackForwardList?
653  return { back: _backwardStack, forward: _forwardStack };
654 }
655 
660 - (void)close
661 {
662  _iframe.parentNode.removeChild(_iframe);
663 }
664 
670 - (DOMWindow)DOMWindow
671 {
672  return (_iframe.contentDocument && _iframe.contentDocument.defaultView) || _iframe.contentWindow;
673 }
674 
680 - (CPWebScriptObject)windowScriptObject
681 {
682  var win = [self DOMWindow];
683  if (!_wso || win != [_wso window])
684  {
685  if (win)
686  _wso = [[CPWebScriptObject alloc] initWithWindow:win];
687  else
688  _wso = nil;
689  }
690  return _wso;
691 }
692 
700 - (CPString)stringByEvaluatingJavaScriptFromString:(CPString)script
701 {
702  var result = [self objectByEvaluatingJavaScriptFromString:script];
703  return result ? String(result) : nil;
704 }
705 
712 - (JSObject)objectByEvaluatingJavaScriptFromString:(CPString)script
713 {
714  return [[self windowScriptObject] evaluateWebScript:script];
715 }
716 
724 - (DOMCSSStyleDeclaration)computedStyleForElement:(DOMElement)element pseudoElement:(CPString)pseudoElement
725 {
726  var win = [[self windowScriptObject] window];
727  if (win)
728  {
729  // FIXME: IE version?
730  return win.document.defaultView.getComputedStyle(element, pseudoElement);
731  }
732  return nil;
733 }
734 
735 
739 - (BOOL)drawsBackground
740 {
741  return _drawsBackground;
742 }
743 
755 - (void)setDrawsBackground:(BOOL)drawsBackground
756 {
757  if (drawsBackground == _drawsBackground)
758  return;
759  _drawsBackground = drawsBackground;
760 
761  [self _applyBackgroundColor];
762 }
763 
764 - (void)setBackgroundColor:(CPColor)aColor
765 {
766  [super setBackgroundColor:aColor];
767  [self _applyBackgroundColor];
768 }
769 
770 - (void)_applyBackgroundColor
771 {
772  if (_iframe)
773  {
774  var bgColor = [self backgroundColor] || [CPColor whiteColor];
775  _iframe.allowtransparency = !_drawsBackground;
776  _iframe.style.backgroundColor = _drawsBackground ? [bgColor cssString] : "transparent";
777  }
778 }
779 
780 // IBActions
781 
788 - (@action)takeStringURLFrom:(id)sender
789 {
790  [self setMainFrameURL:[sender stringValue]];
791 }
792 
798 - (@action)goBack:(id)sender
799 {
800  [self goBack];
801 }
802 
808 - (@action)goForward:(id)sender
809 {
810  [self goForward];
811 }
812 
818 - (@action)stopLoading:(id)sender
819 {
820  // FIXME: what to do?
821 }
822 
828 - (@action)reload:(id)sender
829 {
830  // If we're displaying pure HTML, redisplay it.
831  if (!_url && (_html !== nil))
832  [self loadHTMLString:_html];
833  else
834  [self _loadMainFrameURL];
835 }
836 
843 - (@action)print:(id)sender
844 {
845  try
846  {
847  [self DOMWindow].print();
848  }
849  catch (e)
850  {
851  alert('Please click the webpage and select "Print" from the "File" menu');
852  }
853 }
854 
855 
856 // Delegates:
857 
858 // FIXME: implement more delegates, though most of these will likely never work with the iframe implementation
859 
860 - (id)downloadDelegate
861 {
862  return _downloadDelegate;
863 }
864 - (void)setDownloadDelegate:(id)anObject
865 {
866  _downloadDelegate = anObject;
867 }
868 - (id)frameLoadDelegate
869 {
870  return _frameLoadDelegate;
871 }
872 - (void)setFrameLoadDelegate:(id)anObject
873 {
874  _frameLoadDelegate = anObject;
875 }
876 - (id)policyDelegate
877 {
878  return _policyDelegate;
879 }
880 - (void)setPolicyDelegate:(id)anObject
881 {
882  _policyDelegate = anObject;
883 }
884 - (id)resourceLoadDelegate
885 {
886  return _resourceLoadDelegate;
887 }
888 - (void)setResourceLoadDelegate:(id)anObject
889 {
890  _resourceLoadDelegate = anObject;
891 }
892 - (id)UIDelegate
893 {
894  return _UIDelegate;
895 }
896 - (void)setUIDelegate:(id)anObject
897 {
898  _UIDelegate = anObject;
899 }
900 
901 @end
902 
908 @implementation CPWebScriptObject : CPObject
909 {
910  Window _window;
911 }
912 
916 - (id)initWithWindow:(Window)aWindow
917 {
918  if (self = [super init])
919  {
920  _window = aWindow;
921  }
922  return self;
923 }
924 
931 - (id)callWebScriptMethod:(CPString)methodName withArguments:(CPArray)args
932 {
933  // Would using "with" be better here?
934  if (typeof _window[methodName] == "function")
935  {
936  try {
937  return _window[methodName].apply(args);
938  } catch (e) {
939  }
940  }
941  return undefined;
942 }
943 
950 - (id)evaluateWebScript:(CPString)script
951 {
952  try {
953  return _window.eval(script);
954  } catch (e) {
955  // FIX ME: if we fail inside here, shouldn't we return an exception?
956  }
957  return undefined;
958 }
959 
963 - (Window)window
964 {
965  return _window;
966 }
967 
968 @end
969 
970 
971 @implementation CPWebView (CPCoding)
972 
979 - (id)initWithCoder:(CPCoder)aCoder
980 {
981  self = [super initWithCoder:aCoder];
982 
983  if (self)
984  {
985  // FIXME: encode/decode these?
986  _mainFrameURL = nil;
987  _backwardStack = [];
988  _forwardStack = [];
989  _scrollMode = CPWebViewScrollAuto;
990 
991 #if PLATFORM(DOM)
992  [self _initDOMWithFrame:[self frame]];
993 #endif
994 
995  if (![self backgroundColor])
997 
998  [self _updateEffectiveScrollMode];
999  }
1000 
1001  return self;
1002 }
1003 
1009 - (void)encodeWithCoder:(CPCoder)aCoder
1010 {
1011  var actualSubviews = _subviews;
1012  _subviews = [];
1013  [super encodeWithCoder:aCoder];
1014  _subviews = actualSubviews;
1015 }
1016 
1017 @end
1018 
1019 @implementation CPURL(SOP)
1020 
1028 - (BOOL)_passesSameOriginPolicy
1029 {
1030  var documentURL = [CPURL URLWithString:window.location.href];
1031  if ([documentURL isFileURL] && CPFeatureIsCompatible(CPSOPDisabledFromFileURLs))
1032  return YES;
1033 
1034  // Relative URLs always pass the SOP.
1035  if (![self scheme] && ![self host] && ![self port])
1036  return YES;
1037 
1038  return ([documentURL scheme] == [self scheme] && [documentURL host] == [self host] && [documentURL port] == [self port]);
1039 }
1040 
1041 @end