API  0.9.8
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPTextField.j
Go to the documentation of this file.
1 /*
2  * CPTextField.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
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 @global CPApp
26 @global CPStringPboardType
27 @global CPCursor
28 
29 
31 
32 @end
33 
40 
41 
42 @typedef CPTextFieldBezelStyle
46 CPTextFieldDidFocusNotification = @"CPTextFieldDidFocusNotification";
47 CPTextFieldDidBlurNotification = @"CPTextFieldDidBlurNotification";
48 
62 
64 
65 
66 function CPTextFieldBlurFunction(anEvent, owner, domElement, inputElement, resigning, didBlurRef)
67 {
68  if (owner && domElement != inputElement.parentNode)
69  return;
70 
71  var ownerWindow = [owner window];
72 
73  if (!resigning && [ownerWindow isKeyWindow])
74  {
75  /*
76  Browsers blur text fields when a click occurs anywhere outside the text field. That is normal for browsers, but in Cocoa the key view retains focus unless the click target accepts first responder. So if we lost focus but were not told to resign and our window is still key, restore focus,
77  but only if the text field is completely within the browser window. If we restore focus when it
78  is off screen, the entire body scrolls out of our control.
79  */
80  if ([owner _isWithinUsablePlatformRect])
81  {
82  window.setTimeout(function()
83  {
84  inputElement.focus();
85  }, 0.0);
86  }
87  }
88 
89  CPTextFieldHandleBlur(anEvent, @ref(owner));
90  @deref(didBlurRef) = YES;
91 
92  return true;
93 }
94 
95 function CPTextFieldHandleBlur(anEvent, ownerRef)
96 {
97  @deref(ownerRef) = nil;
98 
99  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
100 }
101 
102 
104 
108 - (CPString)string
109 {
110  return self;
111 }
112 
113 @end
114 
115 CPTextFieldStateRounded = CPThemeState("rounded");
116 CPTextFieldStatePlaceholder = CPThemeState("placeholder");
117 
122 @implementation CPTextField : CPControl
123 {
124  BOOL _isEditing;
125 
126  BOOL _isEditable;
127  BOOL _isSelectable;
128  BOOL _isSecure;
129  BOOL _willBecomeFirstResponderByClick;
130  BOOL _invokedByUserEvent;
131 
132  BOOL _drawsBackground;
133 
134  CPColor _textFieldBackgroundColor;
135 
136  CPString _placeholderString;
137  CPString _stringValue;
138 
139  id <CPTextFieldDelegate> _delegate;
140  unsigned _implementedDelegateMethods;
141 
142  // NS-style Display Properties
143  CPTextFieldBezelStyle _bezelStyle;
144  BOOL _isBordered;
145  BOOL _usesSingleLineMode;
146  BOOL _wraps;
147  BOOL _scrolls;
148 }
149 
150 + (Class)_binderClassForBinding:(CPString)aBinding
151 {
152  if (aBinding === CPValueBinding)
153  return [_CPTextFieldValueBinder class];
154  else if ([aBinding hasPrefix:CPDisplayPatternValueBinding])
155  return [_CPTextFieldPatternValueBinder class];
156  else if ([aBinding hasPrefix:CPEditableBinding])
157  return [CPMultipleValueAndBinding class];
158 
159  return [super _binderClassForBinding:aBinding];
160 }
161 
162 + (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
163 {
164  return [self textFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
165 }
166 
167 + (CPTextField)textFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
168 {
169  var textField = [[self alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];
170 
171  [textField setTheme:aTheme];
172  [textField setStringValue:aStringValue];
173  [textField setPlaceholderString:aPlaceholder];
174  [textField setBordered:YES];
175  [textField setBezeled:YES];
176  [textField setEditable:YES];
177 
178  [textField sizeToFit];
179 
180  return textField;
181 }
182 
183 + (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth
184 {
185  return [self roundedTextFieldWithStringValue:aStringValue placeholder:aPlaceholder width:aWidth theme:[CPTheme defaultTheme]];
186 }
187 
188 + (CPTextField)roundedTextFieldWithStringValue:(CPString)aStringValue placeholder:(CPString)aPlaceholder width:(float)aWidth theme:(CPTheme)aTheme
189 {
190  var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0.0, 0.0, aWidth, 29.0)];
191 
192  [textField setTheme:aTheme];
193  [textField setStringValue:aStringValue];
194  [textField setPlaceholderString:aPlaceholder];
195  [textField setBezelStyle:CPTextFieldRoundedBezel];
196  [textField setBordered:YES];
197  [textField setBezeled:YES];
198  [textField setEditable:YES];
199 
200  [textField sizeToFit];
201 
202  return textField;
203 }
204 
205 + (CPTextField)labelWithTitle:(CPString)aTitle
206 {
207  return [self labelWithTitle:aTitle theme:[CPTheme defaultTheme]];
208 }
209 
210 + (CPTextField)labelWithTitle:(CPString)aTitle theme:(CPTheme)aTheme
211 {
212  var textField = [[self alloc] init];
213 
214  [textField setStringValue:aTitle];
215  [textField sizeToFit];
216 
217  return textField;
218 }
219 
220 + (CPString)defaultThemeClass
221 {
222  return "textfield";
223 }
224 
225 + (CPDictionary)themeAttributes
226 {
227  return @{
228  @"bezel-inset": CGInsetMakeZero(),
229  @"content-inset": CGInsetMake(1.0, 0.0, 0.0, 0.0),
230  @"bezel-color": [CPNull null],
231  };
232 }
233 
234 
235 #pragma mark -
236 #pragma mark Control Size
237 
238 - (void)setControlSize:(CPControlSize)aControlSize
239 {
240  [super setControlSize:aControlSize];
241 
242  if ([self isBezeled])
243  [self _sizeToControlSize];
244 }
245 
246 
247 #pragma mark -
248 
249 #if PLATFORM(DOM)
250 - (DOMElement)_inputElement
251 {
253  {
254  CPTextFieldDOMTextAreaElement = document.createElement("textarea");
255  CPTextFieldDOMTextAreaElement.style.position = "absolute";
256  CPTextFieldDOMTextAreaElement.style.border = "0px";
257  CPTextFieldDOMTextAreaElement.style.padding = "0px";
258  CPTextFieldDOMTextAreaElement.style.margin = "0px";
259  CPTextFieldDOMTextAreaElement.style.background = "transparent";
260  CPTextFieldDOMTextAreaElement.style.outline = "none";
261  CPTextFieldDOMTextAreaElement.style.resize = "none";
262  CPTextFieldDOMTextAreaElement.style.overflow = "hidden";
263  CPTextFieldDOMTextAreaElement.spellcheck = NO;
264  }
265 
267  {
268  CPTextFieldDOMStandardInputElement = document.createElement("input");
269  CPTextFieldDOMStandardInputElement.style.position = "absolute";
270  CPTextFieldDOMStandardInputElement.style.border = "0px";
271  CPTextFieldDOMStandardInputElement.style.padding = "0px";
272  CPTextFieldDOMStandardInputElement.style.margin = "0px";
273  CPTextFieldDOMStandardInputElement.style.whiteSpace = "pre";
274  CPTextFieldDOMStandardInputElement.style.background = "transparent";
275  CPTextFieldDOMStandardInputElement.style.outline = "none";
276  CPTextFieldDOMStandardInputElement.spellcheck = NO;
277  }
278 
280  {
281  CPTextFieldDOMPasswordInputElement = document.createElement("input");
282  CPTextFieldDOMPasswordInputElement.style.position = "absolute";
283  CPTextFieldDOMPasswordInputElement.style.border = "0px";
284  CPTextFieldDOMPasswordInputElement.style.padding = "0px";
285  CPTextFieldDOMPasswordInputElement.style.margin = "0px";
286  CPTextFieldDOMPasswordInputElement.style.whiteSpace = "pre";
287  CPTextFieldDOMPasswordInputElement.style.background = "transparent";
288  CPTextFieldDOMPasswordInputElement.style.outline = "none";
289  CPTextFieldDOMPasswordInputElement.type = "password";
290  CPTextFieldDOMPasswordInputElement.spellcheck = NO;
291  }
292 
294  {
295  CPTextFieldBlurHandler = function(anEvent)
296  {
298  anEvent,
300  CPTextFieldInputOwner ? CPTextFieldInputOwner._DOMElement : nil,
304  };
305  }
306 
308  {
310  return;
311 
312  CPTextFieldInputFunction = function(anEvent)
313  {
315  return;
316 
317  var cappEvent = [CPEvent keyEventWithType:CPKeyUp
318  location:CGPointMakeZero()
319  modifierFlags:0
321  windowNumber:[[CPApp keyWindow] windowNumber]
322  context:nil
323  characters:nil
325  isARepeat:NO
326  keyCode:nil];
327 
328  [CPTextFieldInputOwner keyUp:cappEvent];
329 
330  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
331  }
332 
335 
338 
341  }
342 
343 
344  if ([self isSecure])
346  else if (_usesSingleLineMode)
348  else
350 
352 }
353 #endif
354 
355 - (id)initWithFrame:(CGRect)aFrame
356 {
357  self = [super initWithFrame:aFrame];
358 
359  if (self)
360  {
361  [self setStringValue:@""];
362  [self setPlaceholderString:@""];
363 
364  _sendActionOn = CPKeyUpMask | CPKeyDownMask;
365 
366  [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
367  }
368 
369  return self;
370 }
371 
372 #pragma mark Controlling Editability and Selectability
373 
378 - (void)setEditable:(BOOL)shouldBeEditable
379 {
380  if (_isEditable === shouldBeEditable)
381  return;
382 
383  _isEditable = shouldBeEditable;
384 
385  if (shouldBeEditable)
386  _isSelectable = YES;
387 
388  if (_isEditable)
389  [self setThemeState:CPThemeStateEditable];
390  else
391  [self unsetThemeState:CPThemeStateEditable];
392 
393  // We only allow first responder status if the field is enable, and editable or selectable.
394  if (!(shouldBeEditable && ![self isSelectable]) && [[self window] firstResponder] === self)
395  [[self window] makeFirstResponder:nil];
396 
397  if (shouldBeEditable)
398  [self setThemeState:CPThemeStateEditable];
399  else
400  [self unsetThemeState:CPThemeStateEditable];
401 }
402 
406 - (BOOL)isEditable
407 {
408  return _isEditable;
409 }
410 
415 - (void)setEnabled:(BOOL)shouldBeEnabled
416 {
417  [super setEnabled:shouldBeEnabled];
418 
419  // We only allow first responder status if the field is enabled.
420  if (!shouldBeEnabled && [[self window] firstResponder] === self)
421  [[self window] makeFirstResponder:nil];
422 }
423 
428 - (void)setSelectable:(BOOL)aFlag
429 {
430  _isSelectable = aFlag;
431 }
432 
436 - (BOOL)isSelectable
437 {
438  return _isSelectable;
439 }
440 
445 - (void)setSecure:(BOOL)aFlag
446 {
447  _isSecure = aFlag;
448 }
449 
453 - (BOOL)isSecure
454 {
455  return _isSecure;
456 }
457 
458 // Setting the Bezel Style
463 - (void)setBezeled:(BOOL)shouldBeBezeled
464 {
465  if (shouldBeBezeled)
466  [self setThemeState:CPThemeStateBezeled];
467  else
468  [self unsetThemeState:CPThemeStateBezeled];
469 }
470 
474 - (BOOL)isBezeled
475 {
476  return [self hasThemeState:CPThemeStateBezeled];
477 }
478 
483 - (void)setBezelStyle:(CPTextFieldBezelStyle)aBezelStyle
484 {
485  var shouldBeRounded = aBezelStyle === CPTextFieldRoundedBezel;
486 
487  if (shouldBeRounded)
488  [self setThemeState:CPTextFieldStateRounded];
489  else
490  [self unsetThemeState:CPTextFieldStateRounded];
491 }
492 
496 - (CPTextFieldBezelStyle)bezelStyle
497 {
498  if ([self hasThemeState:CPTextFieldStateRounded])
500 
501  return CPTextFieldSquareBezel;
502 }
503 
508 - (void)setBordered:(BOOL)shouldBeBordered
509 {
510  if (shouldBeBordered)
511  [self setThemeState:CPThemeStateBordered];
512  else
513  [self unsetThemeState:CPThemeStateBordered];
514 }
515 
519 - (BOOL)isBordered
520 {
521  return [self hasThemeState:CPThemeStateBordered];
522 }
523 
528 - (void)setDrawsBackground:(BOOL)shouldDrawBackground
529 {
530  if (_drawsBackground == shouldDrawBackground)
531  return;
532 
533  _drawsBackground = shouldDrawBackground;
534 
535  [self setNeedsLayout];
536  [self setNeedsDisplay:YES];
537 }
538 
542 - (BOOL)drawsBackground
543 {
544  return _drawsBackground;
545 }
546 
551 - (void)setTextFieldBackgroundColor:(CPColor)aColor
552 {
553  if (_textFieldBackgroundColor == aColor)
554  return;
555 
556  _textFieldBackgroundColor = aColor;
557 
558  [self setNeedsLayout];
559  [self setNeedsDisplay:YES];
560 }
561 
565 - (CPColor)textFieldBackgroundColor
566 {
567  return _textFieldBackgroundColor;
568 }
569 
571 - (void)_setUsesSingleLineMode:(BOOL)aFlag
572 {
573  _usesSingleLineMode = aFlag;
574 }
575 
577 - (void)_setWraps:(BOOL)aFlag
578 {
579  _wraps = aFlag;
580 }
581 
583 - (void)_setScrolls:(BOOL)aFlag
584 {
585  _scrolls = aFlag;
586 }
587 
589 - (BOOL)acceptsFirstResponder
590 {
591  return [self isEnabled] && ([self isEditable] || [self isSelectable]) && [self _isWithinUsablePlatformRect];
592 }
593 
595 - (BOOL)becomeFirstResponder
596 {
597  if (![self isEnabled] || ![super becomeFirstResponder])
598  return NO;
599 
600  // As long as we are the first responder we need to monitor the key status of our window.
601  [self _setObserveWindowKeyNotifications:YES];
602 
603  _isEditing = NO;
604 
605  if ([[self window] isKeyWindow] && [self isEditable])
606  return [self _becomeFirstKeyResponder];
607 
608  return YES;
609 }
610 
611 /*
612  A text field can be the first responder without necessarily being the focus of keyboard input. For example, it might be the first responder of window A but window B is the main and key window. It's important we don't put a focused input field into a text field in a non-key window, even if that field is the first responder, because the key window might also have a first responder text field which the user will expect to receive keyboard input.
613 
614  Since a first responder but non-key window text field can't receive input it should not even look like an active text field (Cocoa has a "slightly active" text field look it uses when another window is the key window, but Cappuccino doesn't today.)
615 
616  It's also possible for a text field to be non-editable but selectable in which case it can also become the first responder -
617  this is what allows text to be copied from it.
618 */
619 - (BOOL)_becomeFirstKeyResponder
620 {
621  // If the text field is still not completely on screen, refuse to become
622  // first responder, because the browser will scroll it into view out of our control.
623  if (![self _isWithinUsablePlatformRect])
624  return NO;
625 
626  // A selectable but non-editable text field may be the first responder, but never the
627  // first key responder (first key responder indicating editability.)
628  if (![self isEditable])
629  return NO;
630 
631  [self setThemeState:CPThemeStateEditing];
632 
633  [self _updatePlaceholderState];
634 
635  [self setNeedsLayout];
636 
637  _stringValue = [self stringValue];
638 
639 #if PLATFORM(DOM)
640 
641  [self _setCSSStyleForInputElement];
642 
643  var element = [self _inputElement];
644  element.value = _stringValue;
645  _DOMElement.appendChild(element);
646 
648 
649  if (document.attachEvent)
650  {
651  CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
652  CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
653 
654  [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
655  [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
656  }
657 
658  CPTextFieldInputOwner = self;
659 
660  window.setTimeout(function()
661  {
662  /*
663  setTimeout handlers are not guaranteed to fire in the order they were initiated. This can cause a race condition when several windows with text fields are opened quickly, resulting in several instances of this timeout function being fired, perhaps out of order. So we have to check that by the time this function is fired, CPTextFieldInputOwner has not been changed to another text field in the meantime.
664  */
665  if (CPTextFieldInputOwner !== self)
666  return;
667 
668  element.focus();
669 
670  // Select the text if the textfield became first responder through keyboard interaction
671  if (!_willBecomeFirstResponderByClick)
672  [self _selectText:self immediately:YES];
673 
674  _willBecomeFirstResponderByClick = NO;
675 
676  [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
677  }, 0.0);
678 
679 #endif
680 
681  return YES;
682 }
683 
688 - (void)_setCSSStyleForInputElement
689 {
690 #if PLATFORM(DOM)
691 
692  var element = [self _inputElement],
693  font = [self currentValueForThemeAttribute:@"font"],
694  lineHeight = [font defaultLineHeightForFont],
695  contentRect = [self contentRectForBounds:[self bounds]],
696  verticalAlign = [self currentValueForThemeAttribute:"vertical-alignment"],
697  left = CGRectGetMinX(contentRect);
698 
699  // If the browser has a built in left padding, compensate for it. We need the input text to be exactly on top of the original text.
701  left -= 1;
702 
703  switch (verticalAlign)
704  {
706  var topPoint = CGRectGetMinY(contentRect) + "px";
707  break;
708 
710  var topPoint = (CGRectGetMidY(contentRect) - (lineHeight / 2)) + "px";
711  break;
712 
714  var topPoint = (CGRectGetMaxY(contentRect) - lineHeight) + "px";
715  break;
716 
717  default:
718  var topPoint = CGRectGetMinY(contentRect) + "px";
719  break;
720  }
721 
722  if ([self hasThemeState:CPTextFieldStatePlaceholder])
723  element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder] cssString];
724  else
725  element.style.color = [[self valueForThemeAttribute:@"text-color" inState:CPThemeStateEditing] cssString];
726 
727  switch ([self alignment])
728  {
730  element.style.textAlign = "center";
731  break;
732 
734  element.style.textAlign = "right";
735  break;
736 
737  default:
738  element.style.textAlign = "left";
739  }
740 
741  var isTextArea = element.nodeName.toUpperCase() == "TEXTAREA";
742 
743  element.style.zIndex = 1000;
744  element.style.top = topPoint;
745  element.style.lineHeight = ROUND(lineHeight) + "px";
746  element.style.height = isTextArea ? CGRectGetHeight(contentRect) + "px" : ROUND(lineHeight) + "px";;
747  element.style.width = CGRectGetWidth(contentRect) + "px";
748  element.style.left = left + "px";
749  element.style.verticalAlign = "top";
750  element.style.cursor = "auto";
751  element.style.font = [font cssString];
752 
753  if (isTextArea)
754  element.style.whiteSpace = _wraps ? "pre" : "nowrap";
755 
756 #endif
757 }
758 
760 - (BOOL)resignFirstResponder
761 {
762 #if PLATFORM(DOM)
763  // We might have been the first responder without actually editing.
764  if (_isEditing && CPTextFieldInputOwner === self)
765  {
766  var element = [self _inputElement],
767  newValue = element.value,
768  error = @"";
769 
770  if (newValue !== _stringValue)
771  {
772  [self _setStringValue:newValue];
773  }
774 
775  // If there is a formatter, always give it a chance to reject the resignation,
776  // even if the value has not changed.
777  if ([self _valueIsValid:newValue] === NO)
778  {
779  element.focus();
780  return NO;
781  }
782  }
783 #endif
784 
785  // When we are no longer the first responder we don't worry about the key status of our window anymore.
786  [self _setObserveWindowKeyNotifications:NO];
787 
788  [self _resignFirstKeyResponder];
789 
790  _isEditing = NO;
791  if ([self isEditable])
792  {
793  [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
794 
795  if ([self sendsActionOnEndEditing])
796  [self sendAction:[self action] to:[self target]];
797  }
798 
799  [self textDidBlur:[CPNotification notificationWithName:CPTextFieldDidBlurNotification object:self userInfo:nil]];
800 
801  return YES;
802 }
803 
804 - (void)_resignFirstKeyResponder
805 {
806  [self unsetThemeState:CPThemeStateEditing];
807 
808  // Cache the formatted string
809  _stringValue = [self stringValue];
810 
811  _willBecomeFirstResponderByClick = NO;
812 
813  [self _updatePlaceholderState];
814  [self setNeedsLayout];
815 
816 #if PLATFORM(DOM)
817 
818  var element = [self _inputElement];
819 
821 
823  element.blur();
824 
827 
830 
831  if (element.parentNode == _DOMElement)
832  element.parentNode.removeChild(element);
833 
835 
836  if (document.attachEvent)
837  {
838  [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
839  [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;
840 
843  }
844 
845 #endif
846 }
847 
848 - (void)_setObserveWindowKeyNotifications:(BOOL)shouldObserve
849 {
850  if (shouldObserve)
851  {
852  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidResignKey:) name:CPWindowDidResignKeyNotification object:[self window]];
853  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_windowDidBecomeKey:) name:CPWindowDidBecomeKeyNotification object:[self window]];
854  }
855  else
856  {
857  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidResignKeyNotification object:[self window]];
858  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPWindowDidBecomeKeyNotification object:[self window]];
859  }
860 }
861 
862 - (void)_removeObservers
863 {
864  if (!_isObserving)
865  return;
866 
867  [super _removeObservers];
868  [self _setObserveWindowKeyNotifications:NO];
869 }
870 
871 - (void)_addObservers
872 {
873  if (_isObserving)
874  return;
875 
876  [super _addObservers];
877 
878  if ([[self window] firstResponder] === self)
879  [self _setObserveWindowKeyNotifications:YES];
880 }
881 
882 - (void)_windowDidResignKey:(CPNotification)aNotification
883 {
884  if (![[self window] isKeyWindow])
885  [self _resignFirstKeyResponder];
886 }
887 
888 - (void)_windowDidBecomeKey:(CPNotification)aNotification
889 {
890  if (!([self isEnabled] && [self isEditable]))
891  return;
892 
893  var wind = [self window];
894 
895  if ([wind isKeyWindow] && [wind firstResponder] === self)
896  if (![self _becomeFirstKeyResponder])
897  [wind makeFirstResponder:nil];
898 }
899 
900 - (BOOL)_valueIsValid:(CPString)aValue
901 {
902 #if PLATFORM(DOM)
903 
904  var error = @"";
905 
906  if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
907  {
908  var acceptInvalidValue = NO;
909 
911  acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
912 
913  if (acceptInvalidValue === NO)
914  return NO;
915  }
916 
917 #endif
918 
919  return YES;
920 }
921 
926 - (BOOL)needsPanelToBecomeKey
927 {
928  return [self acceptsFirstResponder];
929 }
930 
934 - (BOOL)acceptsFirstMouse:(CPEvent)anEvent
935 {
936  return [self acceptsFirstResponder];
937 }
938 
939 - (void)_didEdit
940 {
941  if (!_isEditing)
942  {
943  _isEditing = YES;
944  [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
945  }
946 
947  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
948 }
949 
950 - (void)mouseMoved:(CPEvent)anEvent
951 {
952  [super mouseMoved:anEvent];
953  [self _updateCursorForEvent:anEvent];
954 }
955 
956 - (void)mouseDown:(CPEvent)anEvent
957 {
958  // Don't track! (ever?)
959  if ([self isEditable] && [self isEnabled])
960  {
961  _willBecomeFirstResponderByClick = YES;
962  [[self window] makeFirstResponder:self];
963  }
964  else if ([self isSelectable])
965  {
966  if (document.attachEvent)
967  {
968  CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
969  CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
970 
971  [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
972  [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
973  }
974  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
975  }
976  else
977  return [[self nextResponder] mouseDown:anEvent];
978 }
979 
980 - (void)mouseUp:(CPEvent)anEvent
981 {
982  if (![self isEnabled] || !([self isSelectable] || [self isEditable]))
983  [[self nextResponder] mouseUp:anEvent];
984  else if ([self isSelectable])
985  {
986  if (document.attachEvent)
987  {
988  [[self window] platformWindow]._DOMBodyElement.ondrag = CPTextFieldCachedDragFunction;
989  [[self window] platformWindow]._DOMBodyElement.onselectstart = CPTextFieldCachedSelectStartFunction;
990 
993  }
994 
995  // TODO clickCount === 2 should select the clicked word.
996 
997  if ([[CPApp currentEvent] clickCount] === 3)
998  {
999  [self selectText:nil];
1000  return;
1001  }
1002 
1003  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
1004  }
1005 }
1006 
1007 - (void)mouseDragged:(CPEvent)anEvent
1008 {
1009  if (![self isEnabled] || !([self isSelectable] || [self isEditable]))
1010  [[self nextResponder] mouseDragged:anEvent];
1011  else if ([self isSelectable])
1012  return [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:YES];
1013 }
1014 
1015 - (void)keyUp:(CPEvent)anEvent
1016 {
1017  if (!([self isEnabled] && [self isEditable]))
1018  return;
1019 
1020 #if PLATFORM(DOM)
1021  var newValue = [self _inputElement].value;
1022 
1023  if (newValue !== _stringValue)
1024  {
1025  [self _setStringValue:newValue];
1026  [self _didEdit];
1027  }
1028 
1029  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1030 #endif
1031 }
1032 
1033 - (void)keyDown:(CPEvent)anEvent
1034 {
1035  if (!([self isEnabled] && [self isEditable]))
1036  return;
1037 
1038  // CPTextField uses an HTML input element to take the input so we need to
1039  // propagate the dom event so the element is updated. This has to be done
1040  // before interpretKeyEvents: though so individual commands have a chance
1041  // to override this (escape to clear the text in a search field for example).
1042  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1043 
1044  // Set a flag so that key handling methods (such as deleteBackward:)
1045  // know they were invoked from a user event.
1046  _invokedByUserEvent = !!anEvent._DOMEvent;
1047  [self interpretKeyEvents:[anEvent]];
1048  _invokedByUserEvent = NO;
1049 
1050  [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode];
1051 }
1052 
1062 - (void)doCommandBySelector:(SEL)aSelector
1063 {
1064  if ([self respondsToSelector:aSelector])
1065  [self performSelector:aSelector];
1066 }
1067 
1068 - (void)insertNewline:(id)sender
1069 {
1070  if (!([self isEnabled] && [self isEditable]))
1071  return;
1072 
1073  var newValue = [self _inputElement].value;
1074 
1075  if (newValue !== _stringValue)
1076  {
1077  [self _setStringValue:newValue];
1078  [self _didEdit];
1079  }
1080 
1081  if ([self _valueIsValid:_stringValue])
1082  {
1083  // If _isEditing == YES then the target action can also be called via
1084  // resignFirstResponder, and it is possible that the target action
1085  // itself will change this textfield's responder status, so start by
1086  // setting the _isEditing flag to NO to prevent the target action being
1087  // called twice (once below and once from resignFirstResponder).
1088  if (_isEditing)
1089  {
1090  _isEditing = NO;
1091  [self textDidEndEditing:[CPNotification notificationWithName:CPControlTextDidEndEditingNotification object:self userInfo:@{"CPTextMovement": [self _currentTextMovement]}]];
1092  }
1093 
1094  // If there is no target action, or the sendAction call returns
1095  // success.
1096  if (![self action] || [self sendAction:[self action] to:[self target]])
1097  {
1098  [self selectAll:nil];
1099  }
1100  }
1101 
1102  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
1103 }
1104 
1105 - (void)insertNewlineIgnoringFieldEditor:(id)sender
1106 {
1107  [self _insertCharacterIgnoringFieldEditor:CPNewlineCharacter];
1108 }
1109 
1110 - (void)insertTabIgnoringFieldEditor:(id)sender
1111 {
1112  [self _insertCharacterIgnoringFieldEditor:CPTabCharacter];
1113 }
1114 
1115 - (void)_insertCharacterIgnoringFieldEditor:(CPString)aCharacter
1116 {
1117  if (!([self isEnabled] && [self isEditable]))
1118  return;
1119 
1120 #if PLATFORM(DOM)
1121 
1122  var oldValue = _stringValue,
1123  range = [self selectedRange],
1124  element = [self _inputElement];
1125 
1126  // we don't need to do this in case of textarea
1127  // or we will end up with 2 carriage returns
1128  if (aCharacter != CPNewlineCharacter || element.nodeName.toUpperCase() != "TEXTAREA" || !CPFeatureIsCompatible(CPAltEnterTextAreaFeature))
1129  element.value = [element.value stringByReplacingCharactersInRange:[self selectedRange] withString:aCharacter];
1130 
1131  [self _setStringValue:element.value];
1132 
1133  // NOTE: _stringValue is now the current input element value
1134  if (oldValue !== _stringValue)
1135  {
1136  [self _didEdit];
1137  }
1138 
1139 #endif
1140 }
1141 
1142 - (void)textDidBlur:(CPNotification)note
1143 {
1144  // this looks to prevent false propagation of notifications for other objects
1145  if ([note object] != self)
1146  return;
1147 
1148  if (_implementedDelegateMethods & CPTextFieldDelegate_controlTextDidBlur_)
1149  [_delegate controlTextDidBlur:note];
1150 
1152 }
1153 
1154 - (void)textDidFocus:(CPNotification)note
1155 {
1156  // this looks to prevent false propagation of notifications for other objects
1157  if ([note object] != self)
1158  return;
1159 
1160  if (_implementedDelegateMethods & CPTextFieldDelegate_controlTextDidFocus_)
1161  [_delegate controlTextDidFocus:note];
1162 
1164 }
1165 
1166 - (void)textDidChange:(CPNotification)note
1167 {
1168  if ([note object] !== self)
1169  return;
1170 
1171  [self _continuouslyReverseSetBinding];
1172 
1173  if (_implementedDelegateMethods & CPTextFieldDelegate_controlTextDidChange_)
1174  [_delegate controlTextDidChange:note];
1175 
1176  [super textDidChange:note];
1177 }
1178 
1179 - (void)textDidBeginEditing:(CPNotification)note
1180 {
1181  //this looks to prevent false propagation of notifications for other objects
1182  if ([note object] != self)
1183  return;
1184 
1185  if (_implementedDelegateMethods & CPTextFieldDelegate_controlTextDidBeginEditing_)
1186  [_delegate controlTextDidBeginEditing:[[CPNotification alloc] initWithName:CPControlTextDidBeginEditingNotification object:self userInfo:@{"CPFieldEditor": [note object]}]]
1187 
1188  [super textDidBeginEditing:note];
1189 }
1190 
1191 - (void)textDidEndEditing:(CPNotification)note
1192 {
1193  //this looks to prevent false propagation of notifications for other objects
1194  if ([note object] != self)
1195  return;
1196 
1197  [super textDidEndEditing:note];
1198 
1199  if (_implementedDelegateMethods & CPTextFieldDelegate_controlTextDidEndEditing_)
1200  [_delegate controlTextDidEndEditing:note];
1201 }
1202 
1203 - (void)_updateCursorForEvent:(CPEvent)anEvent
1204 {
1205  var frame = CGRectMakeCopy([self frame]),
1206  contentInset = [self currentValueForThemeAttribute:@"content-inset"];
1207 
1208  frame = [[self superview] convertRectToBase:CGRectInsetByInset(frame, contentInset)];
1209 
1210  if ([self isEnabled] && ([self isSelectable] || [self isEditable]) && CGRectContainsPoint(frame, [anEvent locationInWindow]))
1211  {
1212 #if PLATFORM(DOM)
1213  self._DOMElement.style.cursor = "text";
1214 #endif
1215  }
1216  else
1217  {
1218 #if PLATFORM(DOM)
1219  self._DOMElement.style.cursor = "default";
1220 #endif
1221  }
1222 }
1223 
1227 - (id)objectValue
1228 {
1229  return [super objectValue];
1230 }
1231 
1232 /*
1233  Sets the internal string value without updating the value in the input element.
1234  This should only be invoked when the underlying text element's value has changed.
1235 */
1236 - (BOOL)_setStringValue:(CPString)aValue
1237 {
1238  return [self _setStringValue:aValue isNewValue:YES errorDescription:nil];
1239 }
1240 
1245 - (BOOL)_setStringValue:(CPString)aValue isNewValue:(BOOL)isNewValue errorDescription:(CPStringRef)anError
1246 {
1247  _stringValue = aValue;
1248 
1249  var objectValue = aValue,
1250  formatter = [self formatter],
1251  result = YES;
1252 
1253  if (formatter)
1254  {
1255  var object = nil;
1256 
1257  if ([formatter getObjectValue:@ref(object) forString:aValue errorDescription:anError])
1258  objectValue = object;
1259  else
1260  {
1261  objectValue = undefined; // Mark the value as invalid
1262  result = NO;
1263  }
1264 
1265  isNewValue |= objectValue !== [super objectValue];
1266  }
1267 
1268  if (isNewValue)
1269  {
1270  [self willChangeValueForKey:@"objectValue"];
1271  [super setObjectValue:objectValue];
1272  [self _updatePlaceholderState];
1273  [self didChangeValueForKey:@"objectValue"];
1274  }
1275 
1276  return result;
1277 }
1278 
1279 - (void)setObjectValue:(id)aValue
1280 {
1281  [self _setObjectValue:aValue useFormatter:YES];
1282 }
1283 
1284 - (void)_setObjectValue:(id)aValue useFormatter:(BOOL)useFormatter
1285 {
1286  [super setObjectValue:aValue];
1287 
1288  var formatter = [self formatter];
1289 
1290  if (useFormatter && formatter)
1291  {
1292  // If there is a formatter, make sure the object value can be formatted successfully
1293  var formattedString = [self hasThemeState:CPThemeStateEditing] ? [formatter editingStringForObjectValue:aValue] : [formatter stringForObjectValue:aValue];
1294 
1295  if (formattedString === nil)
1296  {
1297  var value = nil;
1298 
1299  // Formatting failed, get an "empty" object by formatting an empty string.
1300  // If that fails, the value is undefined.
1301  if ([formatter getObjectValue:@ref(value) forString:@"" errorDescription:nil] === NO)
1302  value = undefined;
1303 
1304  [super setObjectValue:value];
1305  _stringValue = (value === nil || value === undefined) ? @"" : String(value);
1306  }
1307  else
1308  _stringValue = formattedString;
1309  }
1310  else
1311  _stringValue = [self stringValue];
1312 
1313 #if PLATFORM(DOM)
1314 
1315  if ((CPTextFieldInputOwner === self || [[self window] firstResponder] === self) && [[self window] isKeyWindow])
1316  [self _inputElement].value = _stringValue;
1317 
1318 #endif
1319 
1320  [self _updatePlaceholderState];
1321 }
1322 
1323 - (void)_updatePlaceholderState
1324 {
1325  if (!_stringValue || _stringValue.length === 0)
1326  [self setThemeState:CPTextFieldStatePlaceholder];
1327  else
1328  [self unsetThemeState:CPTextFieldStatePlaceholder];
1329 }
1330 
1335 - (void)setPlaceholderString:(CPString)aStringValue
1336 {
1337  if (_placeholderString === aStringValue)
1338  return;
1339 
1340  _placeholderString = aStringValue;
1341 
1342  // Only update things if we need to show the placeholder
1343  if ([self hasThemeState:CPTextFieldStatePlaceholder])
1344  {
1345  [self setNeedsLayout];
1346  [self setNeedsDisplay:YES];
1347  }
1348 }
1349 
1353 - (CPString)placeholderString
1354 {
1355  return _placeholderString;
1356 }
1357 
1378 - (void)sizeToFit
1379 {
1380  [self setFrameSize:[self _minimumFrameSize]];
1381 }
1382 
1383 - (CGSize)_minimumFrameSize
1384 {
1385  var frameSize = [self frameSize],
1386  contentInset = [self currentValueForThemeAttribute:@"content-inset"],
1387  minSize = [self currentValueForThemeAttribute:@"min-size"],
1388  maxSize = [self currentValueForThemeAttribute:@"max-size"],
1389  lineBreakMode = [self lineBreakMode],
1390  text = (_stringValue || @" "),
1391  textSize = CGSizeMakeCopy(frameSize),
1392  font = [self currentValueForThemeAttribute:@"font"];
1393 
1394  textSize.width -= contentInset.left + contentInset.right;
1395  textSize.height -= contentInset.top + contentInset.bottom;
1396 
1397  if (frameSize.width !== 0 &&
1398  ![self isBezeled] &&
1399  (lineBreakMode === CPLineBreakByWordWrapping || lineBreakMode === CPLineBreakByCharWrapping))
1400  {
1401  textSize = [text sizeWithFont:font inWidth:textSize.width];
1402  }
1403  else
1404  {
1405  textSize = [text sizeWithFont:font];
1406 
1407  // Account for possible fractional pixels at right edge
1408  textSize.width += 1;
1409  }
1410 
1411  // Account for possible fractional pixels at bottom edge
1412  textSize.height += 1;
1413 
1414  frameSize.height = textSize.height + contentInset.top + contentInset.bottom;
1415 
1416  if ([self isBezeled])
1417  {
1418  frameSize.height = MAX(frameSize.height, minSize.height);
1419 
1420  if (maxSize.width > 0.0)
1421  frameSize.width = MIN(frameSize.width, maxSize.width);
1422 
1423  if (maxSize.height > 0.0)
1424  frameSize.height = MIN(frameSize.height, maxSize.height);
1425  }
1426  else
1427  frameSize.width = textSize.width + contentInset.left + contentInset.right;
1428 
1429  frameSize.width = MAX(frameSize.width, minSize.width);
1430 
1431  return frameSize;
1432 }
1433 
1437 - (void)selectText:(id)sender
1438 {
1439  [self _selectText:sender immediately:NO];
1440 }
1441 
1442 - (void)_selectText:(id)sender immediately:(BOOL)immediately
1443 {
1444  // Selecting the text in a field makes it the first responder
1445  if ([self isEditable] || [self isSelectable])
1446  {
1447  var wind = [self window];
1448 
1449 #if PLATFORM(DOM)
1450  if ([self isEditable])
1451  {
1452  var element = [self _inputElement];
1453 
1454  if ([wind firstResponder] === self)
1455  {
1456  if (immediately)
1457  element.select();
1458  else
1459  window.setTimeout(function() { element.select(); }, 0);
1460  }
1461  else if (wind !== nil && [wind makeFirstResponder:self])
1462  [self _selectText:sender immediately:immediately];
1463  }
1464  else
1465  {
1466  [self setSelectedRange:CPMakeRange(0, _stringValue.length)];
1467  }
1468 #else
1469  // Even if we can't actually select the text we need to preserve the first
1470  // responder side effect.
1471  if (wind !== nil && [wind firstResponder] !== self)
1472  [wind makeFirstResponder:self];
1473 #endif
1474  }
1475 
1476 }
1477 
1478 - (void)copy:(id)sender
1479 {
1480  // First write to the Cappuccino clipboard.
1481  var stringToCopy = nil;
1482 
1483  if ([self isEditable])
1484  {
1485  var selectedRange = [self selectedRange];
1486 
1487  if (selectedRange.length < 1)
1488  return;
1489 
1490  stringToCopy = [_stringValue substringWithRange:selectedRange];
1491  }
1492  else
1493  {
1494  // selectedRange won't work if we're displaying our text using a <div>. Instead we have to ask the browser
1495  // what's selected and hope it's right in a Cappuccino context as well.
1496 #if PLATFORM(DOM)
1497  stringToCopy = [[[self window] platformWindow] _selectedText];
1498 #endif
1499  }
1500 
1501  var pasteboard = [CPPasteboard generalPasteboard];
1502 
1503  [pasteboard declareTypes:[CPStringPboardType] owner:nil];
1504  [pasteboard setString:stringToCopy forType:CPStringPboardType];
1505 
1506  if ([CPPlatform isBrowser])
1507  {
1508  // Then also allow the browser to capture the copied text into the system clipboard.
1509  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1510  }
1511 }
1512 
1513 - (void)cut:(id)sender
1514 {
1515  if (![self isEnabled])
1516  return;
1517 
1518  [self copy:sender];
1519 
1520  if (![self isEditable])
1521  return;
1522 
1523  if (![[CPApp currentEvent] _platformIsEffectingCutOrPaste])
1524  {
1525  [self deleteBackward:sender];
1526  }
1527  else
1528  {
1529  // Allow the browser's standard cut handling. This should also result in the deleteBackward: happening.
1530  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1531 
1532  // If we don't have an oninput listener, we won't detect the change made by the cut and need to fake a key up "soon".
1534  [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
1535  }
1536 }
1537 
1538 - (void)paste:(id)sender
1539 {
1540  if (!([self isEnabled] && [self isEditable]))
1541  return;
1542 
1543  if (![[CPApp currentEvent] _platformIsEffectingCutOrPaste])
1544  {
1545  var pasteboard = [CPPasteboard generalPasteboard];
1546 
1547  if (![[pasteboard types] containsObject:CPStringPboardType])
1548  return;
1549 
1550  [self deleteBackward:sender];
1551 
1552  var selectedRange = [self selectedRange],
1553  pasteString = [pasteboard stringForType:CPStringPboardType],
1554  newValue = [_stringValue stringByReplacingCharactersInRange:selectedRange withString:pasteString];
1555 
1556  [self setStringValue:newValue];
1557  [self _didEdit];
1558  [self setSelectedRange:CPMakeRange(selectedRange.location + pasteString.length, 0)];
1559  }
1560  // If we don't have an oninput listener, we won't detect the change made by the cut and need to fake a key up "soon".
1561  else
1562  {
1563  // Allow the browser's standard paste handling.
1564  [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
1565 
1567  [CPTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(keyUp:) userInfo:nil repeats:NO];
1568  }
1569 }
1570 
1571 - (CPRange)selectedRange
1572 {
1573  // TODO Need a way to figure out the selected range if we're not using an input. Need
1574  // to get whole document selection and somehow see which part is inside of this text field.
1575  if ([[self window] firstResponder] !== self)
1576  return CPMakeRange(0, 0);
1577 
1578 #if PLATFORM(DOM)
1579 
1580  // we wrap this in try catch because firefox will throw an exception in certain instances
1581  try
1582  {
1583  var inputElement = [self _inputElement],
1584  selectionStart = inputElement.selectionStart,
1585  selectionEnd = inputElement.selectionEnd;
1586 
1587  if ([selectionStart isKindOfClass:CPNumber])
1588  return CPMakeRange(selectionStart, selectionEnd - selectionStart);
1589 
1590  // browsers which don't support selectionStart/selectionEnd (aka IE).
1591  var theDocument = inputElement.ownerDocument || inputElement.document,
1592  selectionRange = theDocument.selection.createRange(),
1593  range = inputElement.createTextRange();
1594 
1595  if (range.inRange(selectionRange))
1596  {
1597  range.setEndPoint('EndToStart', selectionRange);
1598  return CPMakeRange(range.text.length, selectionRange.text.length);
1599  }
1600  }
1601  catch (e)
1602  {
1603  // fall through to the return
1604  }
1605 
1606 #endif
1607 
1608  return CPMakeRange(0, 0);
1609 }
1610 
1611 - (void)setSelectedRange:(CPRange)aRange
1612 {
1613  if (![[self window] firstResponder] === self)
1614  return;
1615 
1616 #if PLATFORM(DOM)
1617 
1618  if (![self isEditable])
1619  {
1620  // No input element - selectable text field only.
1621  var contentView = [self layoutEphemeralSubviewNamed:@"content-view"
1622  positioned:CPWindowAbove
1623  relativeToEphemeralSubviewNamed:@"bezel-view"];
1624 
1625  if (contentView)
1626  [contentView setSelectedRange:aRange];
1627  }
1628  else
1629  {
1630  // Input element
1631  var inputElement = [self _inputElement];
1632 
1633  try
1634  {
1635  if ([inputElement.selectionStart isKindOfClass:CPNumber])
1636  {
1637  inputElement.selectionStart = aRange.location;
1638  inputElement.selectionEnd = CPMaxRange(aRange);
1639  }
1640  else
1641  {
1642  // browsers which don't support selectionStart/selectionEnd (aka IE).
1643  var theDocument = inputElement.ownerDocument || inputElement.document,
1644  existingRange = theDocument.selection.createRange(),
1645  range = inputElement.createTextRange();
1646 
1647  if (range.inRange(existingRange))
1648  {
1649  range.collapse(true);
1650  range.move('character', aRange.location);
1651  range.moveEnd('character', aRange.length);
1652  range.select();
1653  }
1654  }
1655  }
1656  catch (e)
1657  {
1658  }
1659  }
1660 #endif
1661 }
1662 
1663 - (void)selectAll:(id)sender
1664 {
1665  [self selectText:sender];
1666 }
1667 
1668 - (void)deleteBackward:(id)sender
1669 {
1670  if (!([self isEnabled] && [self isEditable]))
1671  return;
1672 
1673  var selectedRange = [self selectedRange];
1674 
1675  if (selectedRange.length === 0)
1676  {
1677  if (selectedRange.location < 1)
1678  return;
1679 
1680  // Delete a single element backward from the insertion point if there's no selection.
1681  selectedRange.location -= 1;
1682  selectedRange.length += 1;
1683  }
1684 
1685  [self _replaceCharactersInRange:selectedRange withCharacters:@""];
1686 }
1687 
1688 - (void)delete:(id)sender
1689 {
1690  if (!([self isEnabled] && [self isEditable]))
1691  return;
1692 
1693  // delete: only works when there's a selection (as opposed to deleteForward: and deleteBackward:).
1694  var selectedRange = [self selectedRange];
1695 
1696  if (selectedRange.length < 1)
1697  return;
1698 
1699  [self _replaceCharactersInRange:selectedRange withCharacters:@""];
1700 }
1701 
1702 - (void)deleteForward:(id)sender
1703 {
1704  if (!([self isEnabled] && [self isEditable]))
1705  return;
1706 
1707  var selectedRange = [self selectedRange];
1708 
1709  if (selectedRange.length === 0)
1710  {
1711  if (selectedRange.location >= _stringValue.length)
1712  return;
1713 
1714  // Delete a single element forward from the insertion point if there's no selection.
1715  selectedRange.length += 1;
1716  }
1717 
1718  [self _replaceCharactersInRange:selectedRange withCharacters:@""];
1719 }
1720 
1721 - (void)_replaceCharactersInRange:(CPRange)range withCharacters:(CPString)characters
1722 {
1723  var newValue = [_stringValue stringByReplacingCharactersInRange:range withString:characters];
1724 
1725  if (_invokedByUserEvent)
1726  {
1727  [self _setStringValue:newValue];
1728  }
1729  else
1730  {
1731  [self _setObjectValue:newValue useFormatter:NO];
1732  [self setSelectedRange:CPMakeRange(range.location, 0)];
1733 
1734 #if PLATFORM(DOM)
1735  // Since we just performed the deletion manually, we don't need the browser to do anything else.
1736  [[[self window] platformWindow] _propagateCurrentDOMEvent:NO];
1737 #endif
1738  }
1739 
1740  [self _didEdit];
1741 }
1742 
1743 #pragma mark Setting the Delegate
1744 
1745 - (void)setDelegate:(id <CPTextFieldDelegate>)aDelegate
1746 {
1747  if (_delegate === aDelegate)
1748  return;
1749 
1750  _delegate = aDelegate;
1751  _implementedDelegateMethods = 0;
1752 
1753  if ([_delegate respondsToSelector:@selector(control:didFailToFormatString:errorDescription:)])
1755 
1756  if ([_delegate respondsToSelector:@selector(controlTextDidBeginEditing:)])
1757  _implementedDelegateMethods |= CPTextFieldDelegate_controlTextDidBeginEditing_;
1758 
1759  if ([_delegate respondsToSelector:@selector(controlTextDidChange:)])
1760  _implementedDelegateMethods |= CPTextFieldDelegate_controlTextDidChange_;
1761 
1762  if ([_delegate respondsToSelector:@selector(controlTextDidEndEditing:)])
1763  _implementedDelegateMethods |= CPTextFieldDelegate_controlTextDidEndEditing_;
1764 
1765  if ([_delegate respondsToSelector:@selector(controlTextDidFocus:)])
1766  _implementedDelegateMethods |= CPTextFieldDelegate_controlTextDidFocus_;
1767 
1768  if ([_delegate respondsToSelector:@selector(controlTextDidBlur:)])
1769  _implementedDelegateMethods |= CPTextFieldDelegate_controlTextDidBlur_;
1770 }
1771 
1773 {
1774  return _delegate;
1775 }
1776 
1777 - (CGRect)contentRectForBounds:(CGRect)bounds
1778 {
1779  var contentInset = [self currentValueForThemeAttribute:@"content-inset"];
1780 
1781  return CGRectInsetByInset(bounds, contentInset);
1782 }
1783 
1784 - (CGRect)bezelRectForBounds:(CGRect)bounds
1785 {
1786  var bezelInset = [self currentValueForThemeAttribute:@"bezel-inset"];
1787 
1788  return CGRectInsetByInset(bounds, bezelInset);
1789 }
1790 
1791 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
1792 {
1793  if (aName === "bezel-view")
1794  return [self bezelRectForBounds:[self bounds]];
1795 
1796  else if (aName === "content-view")
1797  return [self contentRectForBounds:[self bounds]];
1798 
1799  return [super rectForEphemeralSubviewNamed:aName];
1800 }
1801 
1802 - (CPView)createEphemeralSubviewNamed:(CPString)aName
1803 {
1804  if (aName === "bezel-view")
1805  {
1806  var view = [[CPView alloc] initWithFrame:CGRectMakeZero()];
1807 
1808  [view setHitTests:NO];
1809 
1810  return view;
1811  }
1812  else
1813  {
1814  var view = [[_CPImageAndTextView alloc] initWithFrame:CGRectMakeZero()];
1815 
1816  [view setHitTests:NO];
1817 
1818  return view;
1819  }
1820 
1821  return [super createEphemeralSubviewNamed:aName];
1822 }
1823 
1824 - (void)layoutSubviews
1825 {
1826  var bezelView = [self layoutEphemeralSubviewNamed:@"bezel-view"
1827  positioned:CPWindowBelow
1828  relativeToEphemeralSubviewNamed:@"content-view"];
1829 
1830  if (bezelView)
1831  [bezelView setBackgroundColor:[self currentValueForThemeAttribute:@"bezel-color"]];
1832 
1833  var contentView = [self layoutEphemeralSubviewNamed:@"content-view"
1834  positioned:CPWindowAbove
1835  relativeToEphemeralSubviewNamed:@"bezel-view"];
1836 
1837  if (contentView)
1838  {
1839  [contentView setHidden:(_stringValue && _stringValue.length > 0) && [self hasThemeState:CPThemeStateEditing]];
1840 
1841  var string = "";
1842 
1843  if ([self hasThemeState:CPTextFieldStatePlaceholder])
1844  string = [self placeholderString];
1845  else
1846  {
1847  string = _stringValue;
1848 
1849  if ([self isSecure])
1850  string = secureStringForString(string);
1851  }
1852 
1853  [contentView setText:string];
1854 
1855  [contentView setTextColor:[self currentValueForThemeAttribute:@"text-color"]];
1856  [contentView setFont:[self currentValueForThemeAttribute:@"font"]];
1857  [contentView setAlignment:[self currentValueForThemeAttribute:@"alignment"]];
1858  [contentView setVerticalAlignment:[self currentValueForThemeAttribute:@"vertical-alignment"]];
1859  [contentView setLineBreakMode:[self currentValueForThemeAttribute:@"line-break-mode"]];
1860  [contentView setTextShadowColor:[self currentValueForThemeAttribute:@"text-shadow-color"]];
1861  [contentView setTextShadowOffset:[self currentValueForThemeAttribute:@"text-shadow-offset"]];
1862  }
1863 
1864  if (_isEditing)
1865  [self _setCSSStyleForInputElement];
1866 }
1867 
1868 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
1869 {
1870  var count = objects.length,
1871  value = [objects[0] valueForKeyPath:aKeyPath];
1872 
1873  [self setStringValue:value];
1874  [self setPlaceholderString:@""];
1875 
1876  while (count-- > 1)
1877  if (value !== [objects[count] valueForKeyPath:aKeyPath])
1878  {
1879  [self setPlaceholderString:@"Multiple Values"];
1880  [self setStringValue:@""];
1881  }
1882 }
1883 
1884 #pragma mark Overrides
1885 
1891 - (void)setTextColor:(CPColor)aColor
1892 {
1893  // We don't want to change the text-color of the placeHolder of the textField
1894  var placeholderColor = [self valueForThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder];
1895 
1896  [super setTextColor:aColor];
1897  [self setValue:placeholderColor forThemeAttribute:@"text-color" inState:CPTextFieldStatePlaceholder];
1898 }
1899 
1900 - (void)viewDidHide
1901 {
1902  [super viewDidHide];
1903 
1904  if ([[self window] firstResponder] === self)
1905  [self _resignFirstKeyResponder];
1906 }
1907 
1908 - (void)viewDidUnhide
1909 {
1910  [super viewDidUnhide];
1911 
1912  if ([self isEditable] && [[self window] firstResponder] === self)
1913  [self _becomeFirstKeyResponder];
1914 }
1915 
1916 - (BOOL)validateUserInterfaceItem:(id /*<CPValidatedUserInterfaceItem>*/)anItem
1917 {
1918  var theAction = [anItem action];
1919 
1920  if (![self isEditable] && (theAction == @selector(cut:) || theAction == @selector(paste:) || theAction == @selector(delete:)))
1921  return NO;
1922 
1923  // FIXME - [self selectedRange] is always empty if we're not an editable field, so we must assume yes here.
1924  if (![self isEditable])
1925  return YES;
1926 
1927  if (theAction == @selector(copy:) || theAction == @selector(cut:) || theAction == @selector(delete:))
1928  return [self selectedRange].length;
1929 
1930  return YES;
1931 }
1932 
1933 #pragma mark Private
1934 
1935 - (BOOL)_isWithinUsablePlatformRect
1936 {
1937  // Make sure the text field is completely within the platform window
1938  // so the browser will not scroll it into view.
1939 
1940  var wind = [self window];
1941 
1942  // If the field is not yet within a window, it can't be first responder
1943  if (!wind)
1944  return NO;
1945 
1946  var frame = [self convertRectToBase:[self contentRectForBounds:[self bounds]]],
1947  usableRect = [[wind platformWindow] usableContentFrame];
1948 
1949  frame.origin = [wind convertBaseToGlobal:frame.origin];
1950 
1951  return (CGRectGetMinX(frame) >= CGRectGetMinX(usableRect) &&
1952  CGRectGetMaxX(frame) <= CGRectGetMaxX(usableRect) &&
1953  CGRectGetMinY(frame) >= CGRectGetMinY(usableRect) &&
1954  CGRectGetMaxY(frame) <= CGRectGetMaxY(usableRect));
1955 }
1956 
1957 @end
1958 
1959 var secureStringForString = function(aString)
1960 {
1961  // This is true for when aString === "" and null/undefined.
1962  if (!aString)
1963  return "";
1964 
1965  return Array(aString.length + 1).join(CPSecureTextFieldCharacter);
1966 };
1967 
1968 
1969 var CPTextFieldIsEditableKey = "CPTextFieldIsEditableKey",
1970  CPTextFieldIsSelectableKey = "CPTextFieldIsSelectableKey",
1971  CPTextFieldIsBorderedKey = "CPTextFieldIsBorderedKey",
1972  CPTextFieldIsBezeledKey = "CPTextFieldIsBezeledKey",
1973  CPTextFieldBezelStyleKey = "CPTextFieldBezelStyleKey",
1974  CPTextFieldDrawsBackgroundKey = "CPTextFieldDrawsBackgroundKey",
1975  CPTextFieldLineBreakModeKey = "CPTextFieldLineBreakModeKey",
1976  CPTextFieldAlignmentKey = "CPTextFieldAlignmentKey",
1977  CPTextFieldBackgroundColorKey = "CPTextFieldBackgroundColorKey",
1978  CPTextFieldPlaceholderStringKey = "CPTextFieldPlaceholderStringKey",
1979  CPTextFieldUsesSingleLineMode = "CPTextFieldUsesSingleLineMode",
1980  CPTextFieldWraps = "CPTextFieldWraps",
1981  CPTextFieldScrolls = "CPTextFieldScrolls";
1982 
1983 
1984 @implementation CPTextField (CPCoding)
1985 
1991 - (id)initWithCoder:(CPCoder)aCoder
1992 {
1993  self = [super initWithCoder:aCoder];
1994 
1995  if (self)
1996  {
1997  [self setEditable:[aCoder decodeBoolForKey:CPTextFieldIsEditableKey]];
1998  [self setSelectable:[aCoder decodeBoolForKey:CPTextFieldIsSelectableKey]];
1999 
2000  [self setDrawsBackground:[aCoder decodeBoolForKey:CPTextFieldDrawsBackgroundKey]];
2001 
2002  [self setTextFieldBackgroundColor:[aCoder decodeObjectForKey:CPTextFieldBackgroundColorKey]];
2003 
2004  [self setLineBreakMode:[aCoder decodeIntForKey:CPTextFieldLineBreakModeKey]];
2005  [self setAlignment:[aCoder decodeIntForKey:CPTextFieldAlignmentKey]];
2006 
2007  [self setPlaceholderString:[aCoder decodeObjectForKey:CPTextFieldPlaceholderStringKey]];
2008 
2009  [self _setUsesSingleLineMode:[aCoder decodeBoolForKey:CPTextFieldUsesSingleLineMode]];
2010  [self _setWraps:[aCoder decodeBoolForKey:CPTextFieldWraps]];
2011  [self _setScrolls:[aCoder decodeBoolForKey:CPTextFieldScrolls]];
2012  }
2013 
2014  return self;
2015 }
2016 
2021 - (void)encodeWithCoder:(CPCoder)aCoder
2022 {
2023  [super encodeWithCoder:aCoder];
2024 
2025  [aCoder encodeBool:_isEditable forKey:CPTextFieldIsEditableKey];
2026  [aCoder encodeBool:_isSelectable forKey:CPTextFieldIsSelectableKey];
2027 
2028  [aCoder encodeBool:_drawsBackground forKey:CPTextFieldDrawsBackgroundKey];
2029 
2030  [aCoder encodeObject:_textFieldBackgroundColor forKey:CPTextFieldBackgroundColorKey];
2031 
2032  [aCoder encodeInt:[self lineBreakMode] forKey:CPTextFieldLineBreakModeKey];
2033  [aCoder encodeInt:[self alignment] forKey:CPTextFieldAlignmentKey];
2034 
2035  [aCoder encodeObject:_placeholderString forKey:CPTextFieldPlaceholderStringKey];
2036 
2037  [aCoder encodeBool:_usesSingleLineMode forKey:CPTextFieldUsesSingleLineMode];
2038  [aCoder encodeBool:_wraps forKey:CPTextFieldWraps];
2039  [aCoder encodeBool:_scrolls forKey:CPTextFieldScrolls];
2040 }
2041 
2042 @end
2043 @implementation _CPTextFieldValueBinder : CPBinder
2044 {
2045  id __doxygen__;
2046 }
2047 
2048 - (void)_updatePlaceholdersWithOptions:(CPDictionary)options forBinding:(CPString)aBinding
2049 {
2050  [super _updatePlaceholdersWithOptions:options];
2051 
2052  [self _setPlaceholder:@"Multiple Values" forMarker:CPMultipleValuesMarker isDefault:YES];
2053  [self _setPlaceholder:@"No Selection" forMarker:CPNoSelectionMarker isDefault:YES];
2054  [self _setPlaceholder:@"Not Applicable" forMarker:CPNotApplicableMarker isDefault:YES];
2055  [self _setPlaceholder:@"" forMarker:CPNullMarker isDefault:YES];
2056 }
2057 
2058 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
2059 {
2060  [_source setPlaceholderString:aValue];
2061  [_source setObjectValue:nil];
2062 }
2063 
2064 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
2065 {
2066  if (!aValue || (aValue.isa && [aValue isMemberOfClass:CPNull]))
2067  [_source setPlaceholderString:[self _placeholderForMarker:CPNullMarker]];
2068 
2069  [_source setObjectValue:aValue];
2070 }
2071 
2072 - (void)reverseSetValueFor:(CPString)aBinding
2073 {
2074  var destination = [_info objectForKey:CPObservedObjectKey],
2075  keyPath = [_info objectForKey:CPObservedKeyPathKey],
2076  options = [_info objectForKey:CPOptionsKey],
2077  newValue = [self valueForBinding:aBinding],
2078  value = [destination valueForKeyPath:keyPath];
2079 
2080  if (CPIsControllerMarker(value) && newValue === nil) return;
2081 
2082  newValue = [self reverseTransformValue:newValue withOptions:options];
2083 
2084  [self suppressSpecificNotificationFromObject:destination keyPath:keyPath];
2085  [destination setValue:newValue forKeyPath:keyPath];
2086  [self unsuppressSpecificNotificationFromObject:destination keyPath:keyPath];
2087 }
2088 
2089 @end
2090 @implementation _CPTextFieldPatternValueBinder : CPValueWithPatternBinding
2091 {
2092  id __doxygen__;
2093 }
2094 
2095 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
2096 {
2097  [_source setPlaceholderString:aValue];
2098  [_source setObjectValue:nil];
2099 }
2100 
2101 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
2102 {
2103  if (!aValue || (aValue.isa && [aValue isMemberOfClass:CPNull]))
2104  [_source setPlaceholderString:[self _placeholderForMarker:CPNullMarker]];
2105 
2106  [_source setObjectValue:aValue];
2107 }
2108 
2109 @end