@breakside/jskit 2023.13.0 → 2023.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/Frameworks/DOM.jsframework/Info.json +2 -2
  2. package/Frameworks/DOM.jsframework/io.breakside.JSKit.DOM-bundle.js +2 -2
  3. package/Frameworks/FontKit.jsframework/Info.json +2 -2
  4. package/Frameworks/FontKit.jsframework/io.breakside.JSKit.FontKit-bundle.js +2 -2
  5. package/Frameworks/Foundation.jsframework/Info.json +2 -2
  6. package/Frameworks/Foundation.jsframework/io.breakside.JSKit.Foundation-bundle.js +2 -2
  7. package/Frameworks/SecurityKit.jsframework/Info.json +2 -2
  8. package/Frameworks/SecurityKit.jsframework/io.breakside.JSKit.SecurityKit-bundle.js +2 -2
  9. package/Info.json +2 -2
  10. package/Node/io.breakside.jskit-bundle.js +2 -2
  11. package/Root/Frameworks/APIKit/Info.yaml +1 -1
  12. package/Root/Frameworks/APIKitTesting/Info.yaml +1 -1
  13. package/Root/Frameworks/AuthKit/Info.yaml +1 -1
  14. package/Root/Frameworks/CSSOM/Info.yaml +1 -1
  15. package/Root/Frameworks/ChartKit/Info.yaml +1 -1
  16. package/Root/Frameworks/ConferenceKit/Info.yaml +1 -1
  17. package/Root/Frameworks/DBKit/Info.yaml +1 -1
  18. package/Root/Frameworks/DOM/Info.yaml +1 -1
  19. package/Root/Frameworks/Dispatch/Info.yaml +1 -1
  20. package/Root/Frameworks/FontKit/Info.yaml +1 -1
  21. package/Root/Frameworks/Foundation/Info.yaml +1 -1
  22. package/Root/Frameworks/ImageKit/Info.yaml +1 -1
  23. package/Root/Frameworks/MediaKit/Info.yaml +1 -1
  24. package/Root/Frameworks/MediaKitUI/Info.yaml +1 -1
  25. package/Root/Frameworks/NotificationKit/Info.yaml +1 -1
  26. package/Root/Frameworks/PDFKit/Info.yaml +1 -1
  27. package/Root/Frameworks/QRKit/Info.yaml +1 -1
  28. package/Root/Frameworks/SearchKit/Info.yaml +1 -1
  29. package/Root/Frameworks/SecurityKit/Info.yaml +1 -1
  30. package/Root/Frameworks/ServerKit/Info.yaml +1 -1
  31. package/Root/Frameworks/ServerKitTesting/Info.yaml +1 -1
  32. package/Root/Frameworks/TestKit/Info.yaml +1 -1
  33. package/Root/Frameworks/UIKit/Info.yaml +1 -1
  34. package/Root/Frameworks/UIKit/UICollectionView.js +1 -0
  35. package/Root/Frameworks/UIKit/UIHTMLContentEditableTextInputManager.js +164 -151
  36. package/Root/Frameworks/UIKit/UIHTMLTextField.js +2 -1
  37. package/Root/Frameworks/UIKit/UIHTMLTextLine.js +45 -25
  38. package/Root/Frameworks/UIKit/UIHTMLTextRun.js +7 -2
  39. package/Root/Frameworks/UIKit/UIHTMLTextTypesetter.js +9 -3
  40. package/Root/Frameworks/UIKit/UIHTMLWindowServer.js +1 -0
  41. package/Root/Frameworks/UIKit/UIListView.js +1 -0
  42. package/Root/Frameworks/UIKit/UITextEditor.js +31 -8
  43. package/Root/Frameworks/UIKit/UITextField.js +1 -0
  44. package/Root/Frameworks/UIKit/UITokenField.js +1 -1
  45. package/Root/Frameworks/UIKit/UIToolbar.js +17 -9
  46. package/Root/Frameworks/UIKit/UIWindow.js +21 -7
  47. package/Root/Frameworks/UIKit/UIWindowServer.js +1 -1
  48. package/Root/Frameworks/UIKitTesting/Info.yaml +1 -1
  49. package/Root/Templates/html/${PROJECT_NAME}/conf/debug/nginx.conf +2 -0
  50. package/Root/Templates/html/${PROJECT_NAME}/conf/release/nginx.conf +2 -0
  51. package/package.json +1 -1
@@ -15,7 +15,7 @@
15
15
 
16
16
  // #import "UITextInputManager.js"
17
17
  // #feature 'key' in KeyboardEvent.prototype
18
- /* global UIHTMLTextInputManager */
18
+ /* global UIHTMLTextInputManager, UIDevice, UIView, UIUserInterface */
19
19
  // jshint browser: true
20
20
  'use strict';
21
21
 
@@ -29,7 +29,10 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
29
29
  domWindow: null,
30
30
  domDocument: null,
31
31
  rootElement: null,
32
+ textInputLayer: JSDynamicProperty("_textInputLayer", null),
33
+ layoutManager: JSDynamicProperty("_layoutManager", null),
32
34
  editableElement: JSDynamicProperty("_editableElement", null),
35
+ editableContext: JSDynamicProperty("_editableContext", null),
33
36
  styleElement: null,
34
37
  supportsBeforeinputEvent: false,
35
38
 
@@ -38,9 +41,9 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
38
41
  this.rootElement = rootElement;
39
42
  this.domDocument = this.rootElement.ownerDocument;
40
43
  this.domWindow = this.domDocument.defaultView;
41
- // NOTE: onbeforeinput insn't exposed in Chrome even though it supports the beforeinput event
44
+ // NOTE: onbeforeinput insn't exposed in older Chrome versions even though it supports the beforeinput event
42
45
  this.supportsBeforeinputEvent = ("onbeforeinput" in HTMLElement.prototype) || userAgentKnownToSupportBeforeInput();
43
- this.canLayoutDuringComposition = !userAgentIsKnownToFailCompositionIfDOMChanges();
46
+ this.shouldPreventSpaceInput = !userAgentKnownToRequireSpaceInput();
44
47
  this.styleElement = this.domDocument.createElement("style");
45
48
  this.styleElement.type = "text/css";
46
49
  this.domDocument.head.appendChild(this.styleElement);
@@ -51,21 +54,29 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
51
54
  windowDidChangeResponder: function(window){
52
55
  UIHTMLTextInputManager.$super.windowDidChangeResponder.call(this, window);
53
56
  if (this.textInputClient === null){
54
- this.editableElement = null;
57
+ this.textInputLayer = null;
58
+ this.layoutManager = null;
55
59
  }else{
56
- var layer = null;
57
- var context = null;
58
- if (this.textInputClient.textInputLayer){
59
- layer = this.textInputClient.textInputLayer();
60
- if (layer !== null){
61
- context = this.windowServer.displayServer.contextForLayer(layer);
62
- }
63
- }
64
- if (context !== null){
65
- this.editableElement = context.element;
66
- }else{
67
- this.editableElement = null;
68
- }
60
+ this.textInputLayer = this.textInputClient.textInputLayer ? this.textInputClient.textInputLayer() : null;
61
+ this.layoutManager = this.textInputClient.textInputLayoutManager ? this.textInputClient.textInputLayoutManager() : null;
62
+ }
63
+ },
64
+
65
+ setTextInputLayer: function(layer){
66
+ this._textInputLayer = layer;
67
+ if (this._textInputLayer !== null){
68
+ this.editableContext = this.windowServer.displayServer.contextForLayer(this._textInputLayer);
69
+ }else{
70
+ this.editableContext = null;
71
+ }
72
+ },
73
+
74
+ setEditableContext: function(context){
75
+ this._editableContext = context;
76
+ if (this._editableContext !== null){
77
+ this.editableElement = this._editableContext.element;
78
+ }else{
79
+ this.editableElement = null;
69
80
  }
70
81
  },
71
82
 
@@ -99,16 +110,12 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
99
110
  },
100
111
 
101
112
  textFrameForSelections: function(){
102
- if (this.textInputClient === null){
103
- return null;
104
- }
105
- if (!this.textInputClient.textInputLayoutManager){
113
+ if (this._layoutManager === null){
106
114
  return null;
107
115
  }
108
116
  if (!this.textInputClient.textInputSelections){
109
117
  return null;
110
118
  }
111
- var layoutManager = this.textInputClient.textInputLayoutManager();
112
119
  var selections = this.textInputClient.textInputSelections();
113
120
  var selection;
114
121
  var range;
@@ -128,8 +135,8 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
128
135
  return null;
129
136
  }
130
137
  selection = selections[0];
131
- container1 = layoutManager.textContainerForCharacterAtIndex(selection.startLocation);
132
- container2 = layoutManager.textContainerForCharacterAtIndex(selection.insertionLocation);
138
+ container1 = this._layoutManager.textContainerForCharacterAtIndex(selection.startLocation);
139
+ container2 = this._layoutManager.textContainerForCharacterAtIndex(selection.insertionLocation);
133
140
  if (container1 && container2){
134
141
  if (container1 === container2){
135
142
  return container1.textFrame;
@@ -376,7 +383,7 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
376
383
  logger.warn("dom selection point is not in a text frame");
377
384
  return null;
378
385
  }
379
- return +element.dataset.rangeLocation + element.dataset.rangeLength;
386
+ return +element.dataset.rangeLocation + (+element.dataset.rangeLength);
380
387
  },
381
388
 
382
389
  keydown: function(e){
@@ -406,7 +413,7 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
406
413
  this.windowServer._createKeyEventFromDOMEvent(e, UIEvent.Type.keyDown, preventDefault);
407
414
  if (!this.supportsBeforeinputEvent && this.textInputClient !== null && text !== null && text !== ""){
408
415
  e.preventDefault();
409
- this.insertText(text);
416
+ this.textClient.insertText(text);
410
417
  }
411
418
  },
412
419
 
@@ -419,17 +426,16 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
419
426
  var selections = this.selectionsForDOMRanges(domRanges);
420
427
  var inputClientSelections = [];
421
428
  // logger.debug("beforeinput %{public}: type=%{public}, data=%{public} dataTransfer=%{public}", selections, e.inputType, e.data, e.dataTransfer !== null ? e.dataTransfer.getData("text/plain") : null);
422
- if (domRanges.length > 0){
423
- // logger.debug(" range start : %{public} @ %d", domRanges[0].startContainer.nodeValue, domRanges[0].startOffset);
424
- // logger.debug(" range end : %{public} @ %d", domRanges[0].endContainer.nodeValue, domRanges[0].endOffset);
425
- }
426
- if (domSelection.anchorNode){
427
- // logger.debug(" selection anchor: %{public} @ %d", domSelection.anchorNode.nodeValue, domSelection.anchorOffset);
428
- // logger.debug(" range focus : %{public} @ %d", domSelection.focusNode.nodeValue, domSelection.focusOffset);
429
- }
429
+ // if (domRanges.length > 0){
430
+ // logger.debug(" range start : %{public} @ %d", domRanges[0].startContainer.nodeValue, domRanges[0].startOffset);
431
+ // logger.debug(" range end : %{public} @ %d", domRanges[0].endContainer.nodeValue, domRanges[0].endOffset);
432
+ // }
433
+ // if (domSelection.anchorNode){
434
+ // logger.debug(" selection anchor: %{public} @ %d", domSelection.anchorNode.nodeValue, domSelection.anchorOffset);
435
+ // logger.debug(" range focus : %{public} @ %d", domSelection.focusNode.nodeValue, domSelection.focusOffset);
436
+ // }
430
437
  switch (e.inputType){
431
438
  case "insertText":
432
- e.preventDefault();
433
439
  // Chrome will use insertText when it really should use replaceText, so
434
440
  // we could look for situations that seem more like a replacement, when
435
441
  // the event selections are different from our current selections.
@@ -446,6 +452,9 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
446
452
  // }
447
453
  // }
448
454
  // }
455
+ if (this.shouldPreventSpaceInput || e.data !== " "){
456
+ e.preventDefault();
457
+ }
449
458
  this.textInputClient.insertText(e.data);
450
459
  this.scheduleDocumentSelectionUpdate();
451
460
  break;
@@ -548,56 +557,26 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
548
557
  this.scheduleDocumentSelectionUpdate();
549
558
  }
550
559
  break;
551
- case "deleteByComposition":
552
- // 2021-03-26 not supported by any browser
553
- // - should be called before the first insertCompositionText
554
- // if there's a current selection
555
- // - UITextEditor will clear the text automatically if necessary
556
- // e.preventDefault();
557
- // this.textInputClient.replaceText(selections, "");
558
- break;
559
560
  case "insertCompositionText":
560
561
  // not cancelable
561
- // browser will insert the new composing text 1
562
- if (this.compositionstartHasData){
563
- // When compositionstart has data, it means some text will
564
- // be replaced. The range of that text is not available
565
- // until the subsequent beforeinput event.
566
- // In such a scenario, Firefox will fire compositionupdate
567
- // before the beforeinput event, which gets things out of
568
- // order, so we check for that and queue the update data
569
- // to be handled here.
570
- if (this.textInputClient.replaceText){
571
- this.textInputClient.replaceText(selections, "");
572
- }else{
573
- logger.warn("UITextInputClient missing implementation of replaceText()");
574
- }
575
- this.compositionstartHasData = false;
576
- if (this.compositionupdateData){
577
- if (this.textInputClient.setMarkedText){
578
- this.textInputClient.setMarkedText(this.compositionupdateData);
579
- }else{
580
- logger.warn("UITextInput missing implementation of setMarkedText()");
581
- }
582
- this.compositionupdateData = null;
583
- }
562
+ // browser will insert the new composing text
563
+ if (this.textInputClient.replaceText){
564
+ this.textInputClient.replaceText(selections, e.data);
584
565
  }
585
566
  break;
586
567
  case "deleteCompositionText":
587
- // 2021-03-26 only supported by safari
588
- // not cancelable
589
- // - called before insertFromComposition to indicate the
590
- // compsed text will be deleted
591
- // - we'll handle this in compositionend instead
568
+ // Removed from input events spec, but Safari implemented it,
569
+ // so it may still fire. Need to handle.
570
+ if (this.textInputClient.replaceText){
571
+ this.textInputClient.replaceText(selections, "");
572
+ }
592
573
  break;
593
574
  case "insertFromComposition":
594
- // 2021-03-26 only supported by safari
595
- // - we'll handle this in compositionend instead
596
- // - preventing default so input doesn't fire and DOM isn't updated
597
- // NOTE:
598
- // Safari calls beforeinput, input, then compositionend
599
- // Spec says beforeinput, compositionend, input
600
- e.preventDefault();
575
+ // Removed from input events spec, but Safari implemented it,
576
+ // so it may still fire. Need to handle.
577
+ if (this.textInputClient.replaceText){
578
+ this.textInputClient.replaceText(selections, e.data);
579
+ }
601
580
  break;
602
581
  case "deleteByDrag":
603
582
  case "deleteByCut":
@@ -631,6 +610,7 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
631
610
  break;
632
611
  default:
633
612
  // anything else, not allowed
613
+ logger.warn("contenteditable received unknown beforeinput %{public}", e.inputType);
634
614
  e.preventDefault();
635
615
  break;
636
616
  }
@@ -639,76 +619,132 @@ JSClass('UIHTMLContentEditableTextInputManager', UITextInputManager, {
639
619
  input: function(e){
640
620
  // logger.debug("input: type=%{public}, data=%{public} dataTransfer=%{public}", e.inputType, e.data, e.dataTransfer !== null ? e.dataTransfer.getData("text/plain") : null);
641
621
  // logger.debug("%{public}", this._editableElement.innerHTML);
642
- var layoutManager = this.textInputClient.textInputLayoutManager ? this.textInputClient.textInputLayoutManager() : null;
643
622
  switch (e.inputType){
644
623
  case "insertCompositionText":
645
- // Firefox calls this after compositionend, making it an
646
- // unreliable signal for marked text updates.
647
- // We'll use compositionupdate instead, which is fired correctly.
648
- // Setting the marked text (in compositionupdate) will trigger a layout, which will
649
- // update DOM. Safari and Firefox are fine, but Chrome doesn't
650
- // appreciate such an edit and will fail to complete the composition.
651
- // So instead:
652
- // - tell the layout manager it doesn't need an update after all
653
- // - update the text frame to match the edited HTML
654
- if (!this.canLayoutDuringComposition){
655
- if (layoutManager !== null){
656
- layoutManager.updateHTMLFrameRangesAfterEdit();
657
- }
658
- }
624
+ // Cannot be canceled during beforeinput, so we expect this
625
+ // input event to run. Allow the browser to do its thing here.
659
626
  break;
660
627
  case "deleteCompositionText":
661
- // Only safari supports this, and we don't really care because
662
- // we can do what we need in compositionend instead, which
663
- // all browsers support
628
+ case "insertFromComposition":
629
+ // Removed from input events spec, but Safari implemented it,
630
+ // so it may still fire. No need to log a warning.
631
+ break;
632
+ case "deleteContentBackward":
633
+ // Android doesn't cancel deleteContentBackward in some situations
634
+ // related to text re-composition. Nothing for us to do here,
635
+ // but also no need to log a warning.
664
636
  break;
665
637
  default:
666
638
  // logger.debug("input: %{public}", e.data);
667
- logger.warn("contenteditable received input");
639
+ if (this.shouldPreventSpaceInput || e.inputType !== "insertText" || e.data !== " "){
640
+ logger.warn("contenteditable received input %{public}", e.inputType);
641
+ }
668
642
  // Trigger a new layout of the edited text, which will recreate
669
643
  // the lines/runs without whatever was just inserted by the browser
670
- if (layoutManager !== null){
671
- layoutManager.setNeedsLayout();
644
+ if (this._layoutManager !== null){
645
+ this._layoutManager.setNeedsLayout();
672
646
  this.scheduleDocumentSelectionUpdate();
673
647
  }
674
648
  break;
675
649
  }
676
650
  },
677
651
 
678
- compositionstartHasData: false,
679
- compositionupdateData: null,
652
+ _isComposing: false,
680
653
 
681
654
  compositionstart: function(e){
682
- // logger.debug("compositionstart: %{public}", e.data);
683
- if (e.data !== null && e.data.length > 0){
684
- this.compositionstartHasData = true;
685
- // data will be deleted in beforeinput
655
+ // var domSelection = this.domDocument.getSelection();
656
+ // var domRanges = [];
657
+ // for (var i = 0, l = domSelection.rangeCount; i < l; ++i){
658
+ // domRanges.push(domSelection.getRangeAt(i));
659
+ // }
660
+ // var selections = this.selectionsForDOMRanges(domRanges);
661
+ // logger.debug("compositionstart %{public}: %{public}", selections, e.data);
662
+ if (!this._isComposing){
663
+ this._isComposing = true;
664
+ this._preventDOMUpdates();
665
+ }else{
666
+ // known to happen with Android SwiftKey keyboard
667
+ logger.warn("received compositionstart, but already started");
686
668
  }
687
669
  },
688
670
 
689
671
  compositionupdate: function(e){
690
- // logger.debug("compositionupdate: %{public}", e.data);
691
- if (this.compositionstartHasData){
692
- this.compositionupdateData = e.data;
693
- }else{
694
- if (this.textInputClient.setMarkedText){
695
- this.textInputClient.setMarkedText(e.data);
696
- }else{
697
- logger.warn("UITextInputClient missing implementation of setMarkedText()");
698
- }
672
+ // var domSelection = this.domDocument.getSelection();
673
+ // var domRanges = [];
674
+ // for (var i = 0, l = domSelection.rangeCount; i < l; ++i){
675
+ // domRanges.push(domSelection.getRangeAt(i));
676
+ // }
677
+ // var selections = this.selectionsForDOMRanges(domRanges);
678
+ // logger.debug("compositionupdate %{public}: %{public}", selections, e.data);
679
+ if (!this._isComposing){
680
+ logger.warn("got compositionupdate, but not started");
699
681
  }
700
682
  },
701
683
 
702
684
  compositionend: function(e){
703
685
  // logger.debug("compositionend: %{public}", e.data);
704
- if (this.textInputClient.clearMarkedText){
705
- this.textInputClient.clearMarkedText();
686
+
687
+ if (this._isComposing){
688
+ this._isComposing = false;
689
+ this._resumeDOMUpdates();
690
+ this.scheduleDocumentSelectionUpdate();
691
+ this._cleanupTextNodes();
706
692
  }else{
707
- logger.warn("UITextInputClient missing implementation of clearMarkedText");
693
+ logger.warn("got compositionend, but already ended");
708
694
  }
709
- this.textInputClient.insertText(e.data);
710
- this.scheduleDocumentSelectionUpdate();
711
- this._cleanupTextNodes();
695
+ },
696
+
697
+ _originalLayoutMethod: null,
698
+
699
+ _preventDOMUpdates: function(){
700
+ if (this._layoutManager === null){
701
+ return;
702
+ }
703
+ this._originalLayoutMethod = this._layoutManager.layout;
704
+ Object.defineProperty(this._layoutManager, "layout", {
705
+ configurable: true,
706
+ writable: false,
707
+ enumerable: false,
708
+ value: function(){
709
+ var container;
710
+ var offset = 0;
711
+ for (var i = 0, l = this._textContainers.length; i < l; ++i){
712
+ container = this._textContainers[i];
713
+ offset += container._textFrame.recalculateRange(offset);
714
+ container._textFrame.recalculateSize();
715
+ container.framesetter._updateSizesAndPositionsOfLinesInFrame(container._textFrame);
716
+ }
717
+ }
718
+ });
719
+ // var container;
720
+ // var i, l;
721
+ // for (i = 0, l = this._layoutManager._textContainers.length; i < l; ++i){
722
+ // container = this._layoutManager._textContainers[i];
723
+ // if (container._textFrame instanceof UIHTMLTextFrame){
724
+ // container._textFrame.element.style.overflow = "visible";
725
+ // }
726
+ // }
727
+ },
728
+
729
+ _resumeDOMUpdates: function(){
730
+ if (this._layoutManager === null){
731
+ return;
732
+ }
733
+ // var container;
734
+ // var i, l;
735
+ // for (i = 0, l = this._layoutManager._textContainers.length; i < l; ++i){
736
+ // container = this._layoutManager._textContainers[i];
737
+ // if (container._textFrame instanceof UIHTMLTextFrame){
738
+ // container._textFrame.element.style.overflow = "hidden";
739
+ // }
740
+ // }
741
+ Object.defineProperty(this._layoutManager, "layout", {
742
+ configurable: true,
743
+ writable: false,
744
+ enumerable: false,
745
+ value: this._originalLayoutMethod
746
+ });
747
+ this._originalLayoutMethod = null;
712
748
  },
713
749
 
714
750
  _ensureCorrectFocus: function(){
@@ -743,27 +779,20 @@ var userAgentKnownToSupportBeforeInput = function(){
743
779
  return parseInt(matches[1]) >= 88;
744
780
  }
745
781
  }catch (e){
746
- logger(e);
782
+ logger.warn("Failed to check user agent: %{error}", e);
747
783
  }
748
784
  return false;
749
785
  };
750
786
 
751
- var userAgentIsKnownToFailCompositionIfDOMChanges = function(){
752
- // User agent checks aren't ideal, but there's really no other
753
- // way to tell how the browser behaves during input composition.
754
- // 2021-03-29: Safari and Firefox are known to be fine with DOM
755
- // changes during input composition. Chrome is known have problems.
787
+ var userAgentKnownToRequireSpaceInput = function(){
788
+ // User agent checks aren't ideal, but Android has issues with calling
789
+ // preventDefault() on beforeinput when the input text is a space, so
790
+ // a user agent check is the best we can do.
756
791
  try{
757
- var matches = navigator.userAgent.match(/Android\/(\d+)/);
758
- if (matches !== null){
759
- return true;
760
- }
761
- matches = navigator.userAgent.match(/Chrome\/(\d+)/);
762
- if (matches !== null){
763
- return true;
764
- }
792
+ var matches = navigator.userAgent.match(/Android[\/ ](\d+)/);
793
+ return matches !== null;
765
794
  }catch (e){
766
- logger(e);
795
+ logger.warn("Failed to check user agent: %{error}", e);
767
796
  }
768
797
  return false;
769
798
  };
@@ -809,20 +838,4 @@ htmlAutocompleteByTextContentType[UITextInput.TextContentType.locality] = "addre
809
838
  htmlAutocompleteByTextContentType[UITextInput.TextContentType.country] = "country-name";
810
839
  htmlAutocompleteByTextContentType[UITextInput.TextContentType.postalCode] = "postal-code";
811
840
 
812
- JSTextLayoutManager.definePropertiesFromExtensions({
813
-
814
- updateHTMLFrameRangesAfterEdit: function(){
815
- this._needsLayout = false;
816
- var container;
817
- var offset = 0;
818
- for (var i = 0, l = this._textContainers.length; i < l; ++i){
819
- container = this._textContainers[i];
820
- offset += container._textFrame.recalculateRange(offset);
821
- container._textFrame.recalculateSize();
822
- container.framesetter._updateSizesAndPositionsOfLinesInFrame(container._textFrame);
823
- }
824
- }
825
-
826
- });
827
-
828
841
  })();
@@ -105,7 +105,7 @@ JSClass("UIHTMLTextField", UIControl, {
105
105
  this._clipView.userInteractionEnabled = false;
106
106
  this._clipView.backgroundColor = null;
107
107
  this._clipView.clipsToBounds = true;
108
- this.cursor = UICursor.arrow;
108
+ this.cursor = UICursor.systemDefault;
109
109
  if (this._styler === null){
110
110
  this._styler = UITextField.Styler.default;
111
111
  }
@@ -784,6 +784,7 @@ JSClass("UIHTMLTextField", UIControl, {
784
784
 
785
785
  windowDidChangeKeyStatus: function(){
786
786
  this.updateInputElementFocus();
787
+ UIHTMLTextField.$super.windowDidChangeKeyStatus.call(this);
787
788
  },
788
789
 
789
790
  _inputElementFocused: false,
@@ -20,28 +20,17 @@
20
20
 
21
21
  (function(){
22
22
 
23
+ var logger = JSLog("uikit", "text");
24
+
23
25
  JSClass("UIHTMLTextLine", JSTextLine, {
24
26
 
25
27
  element: null,
26
- emptyTextNode: null,
27
28
  attachmentRuns: null,
28
29
  fontLineHeight: 0,
29
30
  canvasContext: null,
31
+ brElement: null,
30
32
 
31
- initWithElementAndFont: function(element, font, height, location, canvasContext){
32
- UIHTMLTextLine.$super.initWithHeight.call(this, height, -font.displayDescender, location);
33
- this.element = element;
34
- element.style.font = font.cssString(height);
35
- this.emptyTextNode = element.appendChild(element.ownerDocument.createTextNode('\u200B'));
36
- this.fontLineHeight = font.displayLineHeight;
37
- this.attachmentRuns = [];
38
- this.element.dataset.rangeLocation = location;
39
- this.element.dataset.rangeLength = 0;
40
- this.element.dataset.jstext = "line";
41
- this.canvasContext = canvasContext;
42
- },
43
-
44
- initWithElement: function(element, runs, trailingWhitespaceWidth, attachmentRuns, canvasContext){
33
+ initWithElement: function(element, runs, trailingWhitespaceWidth, attachmentRuns, canvasContext, endsWithMandatoryLineBreak){
45
34
  // constructing this.element before super init because super init calls
46
35
  // this.align, which neesd to use this.element
47
36
  UIHTMLTextLine.$super.initWithRuns.call(this, runs, trailingWhitespaceWidth);
@@ -54,6 +43,22 @@ JSClass("UIHTMLTextLine", JSTextLine, {
54
43
  this.element.appendChild(run.element);
55
44
  }
56
45
  }
46
+ if (endsWithMandatoryLineBreak && features.shouldIncludeBR){
47
+ // Not necessary for visual, but helps Android Chrome understand
48
+ // there's a hard line break during contenteditable mode (used by
49
+ // UIHTMLContentEditableTextInputManager). Other broswers don't
50
+ // seem to have an issue, as it seems to be an issue related to
51
+ // android's so we'll limit this to Android only.
52
+ //
53
+ // It would perhaps be more correct to remove the trailing newline
54
+ // character in the final run's textNode and replace it with a <br>
55
+ // inside the run, and that's maybe even a job for the HTML
56
+ // typesetter, but since this is only used on Android, it's ok to
57
+ // just throw it at the end of the line.
58
+ this.brElement = this.element.appendChild(this.element.ownerDocument.createElement("br"));
59
+ this.brElement.dataset.rangeLocation = this._range.end - 1;
60
+ this.brElement.dataset.rangeLength = 1;
61
+ }
57
62
  this.element.dataset.rangeLocation = this._range.location;
58
63
  this.element.dataset.rangeLength = this._range.length;
59
64
  this.element.dataset.jstext = "line";
@@ -151,10 +156,6 @@ JSClass("UIHTMLTextLine", JSTextLine, {
151
156
  copy: function(){
152
157
  var line = UIHTMLTextLine.$super.copy.call(this);
153
158
  line.element = this.element.cloneNode();
154
- if (this.emptyTextNode !== null){
155
- line.emptyTextNode = this.emptyTextNode.cloneNode();
156
- line.element.appendChild(line.emptyTextNode);
157
- }
158
159
  for (var i = 0, l = line.runs.length; i < l; ++i){
159
160
  line.element.appendChild(line.runs[i].element);
160
161
  }
@@ -169,9 +170,6 @@ JSClass("UIHTMLTextLine", JSTextLine, {
169
170
  },
170
171
 
171
172
  domSelectionPointForCharacterAtIndex: function(index){
172
- if (this.range.length === 0){
173
- return {node: this.emptyTextNode, offset: 0};
174
- }
175
173
  var run = this.runForCharacterAtIndex(index);
176
174
  if (run !== null){
177
175
  return run.domSelectionPointForCharacterAtIndex(index);
@@ -182,9 +180,6 @@ JSClass("UIHTMLTextLine", JSTextLine, {
182
180
  debugDescription: JSReadOnlyProperty(),
183
181
 
184
182
  getDebugDescription: function(){
185
- if (this.emptyTextNode !== null){
186
- " %dx%d @%d->%d [empty]".sprintf(this._size.width, this._size.height, this._range.location, this._range.end);
187
- }
188
183
  var lines = [];
189
184
  lines.push([" %dx%d @%d->%d".sprintf(this._size.width, this._size.height, this._range.location, this._range.end)]);
190
185
  for (var i = 0, l = this.runs.length; i < l; ++i){
@@ -244,9 +239,34 @@ JSClass("UIHTMLTextLine", JSTextLine, {
244
239
  }
245
240
  this.element.dataset.rangeLocation = this._range.location;
246
241
  this.element.dataset.rangeLength = this._range.length;
242
+ if (this.brElement !== null){
243
+ this.brElement.dataset.rangeLocation = this._range.end - 1;
244
+ this.brElement.dataset.rangeLength = 1;
245
+ }
247
246
  return diff;
248
247
  },
249
248
 
250
249
  });
251
250
 
251
+ var features = Object.create({}, {
252
+
253
+ shouldIncludeBR: {
254
+ configurable: true,
255
+ get: function(){
256
+ // User agent checks aren't ideal, but Android is the only place
257
+ // where we need to include BRs to have text input work correctly.
258
+ var shouldIncludeBR = false;
259
+ try{
260
+ var matches = navigator.userAgent.match(/Android[\/ ](\d+)/);
261
+ shouldIncludeBR = matches !== null;
262
+ }catch (e){
263
+ logger.warn("Failed to check user agent: %{error}", e);
264
+ }
265
+ Object.defineProperty(this, "shouldIncludeBR", {value: shouldIncludeBR});
266
+ return shouldIncludeBR;
267
+ }
268
+ }
269
+
270
+ });
271
+
252
272
  })();
@@ -247,9 +247,14 @@ JSClass("UIHTMLTextRun", JSTextRun, {
247
247
 
248
248
  recalculateRange: function(offset){
249
249
  var diff = 0;
250
+ var length = 0;
250
251
  if (this.textNode){
251
- diff = this.textNode.nodeValue.length - this._range.length;
252
- this._range = JSRange(this._range.location + offset, this.textNode.nodeValue.length);
252
+ length = this.textNode.nodeValue.length;
253
+ if (length > 0 && this.textNode.nodeValue[length - 1] === 0x200B){
254
+ length -= 1;
255
+ }
256
+ diff = length - this._range.length;
257
+ this._range = JSRange(this._range.location + offset, length);
253
258
  }else{
254
259
  this._range = JSRange(this._range.location + offset, this._range.length);
255
260
  }
@@ -125,8 +125,12 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
125
125
  var attributes = this._attributedString.attributesAtIndex(range.location);
126
126
  attributes = this.resolveAttributes(attributes);
127
127
  var font = attributes.font || null;
128
- var element = this._createLineElement();
129
- return UIHTMLTextLine.initWithElementAndFont(element, font, font.displayLineHeight, range.location, this.canvasContext);
128
+ var span = this._createRunElement();
129
+ span.appendChild(span.ownerDocument.createTextNode("\u200B"));
130
+ this._styleTextElementWithAttributes(span, attributes, font);
131
+ var div = this._createLineElement();
132
+ var emptyRun = UIHTMLTextRun.initWithElement(span, font, attributes, range);
133
+ return UIHTMLTextLine.initWithElement(div, [emptyRun], 0, [], this.canvasContext);
130
134
  },
131
135
 
132
136
  _createLineFromHTMLLayout: function(layout){
@@ -136,6 +140,7 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
136
140
  var runDescriptor;
137
141
  var span;
138
142
  var attachmentRuns = [];
143
+ var endsWithMandatoryLineBreak = false;
139
144
  for (var i = 0, l = layout.runDescriptors.length; i < l; ++i){
140
145
  runDescriptor = layout.runDescriptors[i];
141
146
  span = this._createRunElement();
@@ -150,6 +155,7 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
150
155
  }else{
151
156
  span.appendChild(span.ownerDocument.createTextNode(utf16));
152
157
  }
158
+ endsWithMandatoryLineBreak = utf16.userPerceivedCharacterIterator(utf16.length - 1).isMandatoryLineBreak;
153
159
  }else{
154
160
  this._styleAttachmentElementWithAttributes(span, runDescriptor.attributes, runDescriptor.attachment);
155
161
  if (span.childNodes.length > 0){
@@ -169,7 +175,7 @@ JSClass("UIHTMLTextTypesetter", JSTextTypesetter, {
169
175
  return this._createEmptyHTMLLine(layout.range);
170
176
  }
171
177
  var div = this._createLineElement();
172
- return UIHTMLTextLine.initWithElement(div, runs, layout.trailingWhitespaceWidth, attachmentRuns, this.canvasContext);
178
+ return UIHTMLTextLine.initWithElement(div, runs, layout.trailingWhitespaceWidth, attachmentRuns, this.canvasContext, endsWithMandatoryLineBreak);
173
179
  },
174
180
 
175
181
  _styleTextElementWithAttributes: function(span, attributes, font){