[xfa] Propagate the xfa change data for text to JS and back.

This CL adds the necessary plumbing to propagate the change information
for a text widget from FWL out to JS and handle the returned value as
necessary.

Bug: pdfium:1066
Change-Id: I78fd81761b90294f1836e9f09dba12ed238963cc
Reviewed-on: https://pdfium-review.googlesource.com/33070
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: dsinclair <dsinclair@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index e9f21e0..5bba3fe 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1908,8 +1908,8 @@
       "xfa/fwl/cfwl_eventselectchanged.h",
       "xfa/fwl/cfwl_eventtarget.cpp",
       "xfa/fwl/cfwl_eventtarget.h",
-      "xfa/fwl/cfwl_eventtextchanged.cpp",
-      "xfa/fwl/cfwl_eventtextchanged.h",
+      "xfa/fwl/cfwl_eventtextwillchange.cpp",
+      "xfa/fwl/cfwl_eventtextwillchange.h",
       "xfa/fwl/cfwl_eventvalidate.cpp",
       "xfa/fwl/cfwl_eventvalidate.h",
       "xfa/fwl/cfwl_form.cpp",
diff --git a/fxjs/cfxjse_engine.h b/fxjs/cfxjse_engine.h
index ca5f3ee..33723ad 100644
--- a/fxjs/cfxjse_engine.h
+++ b/fxjs/cfxjse_engine.h
@@ -11,6 +11,7 @@
 #include <memory>
 #include <vector>
 
+#include "core/fxcrt/unowned_ptr.h"
 #include "fxjs/cfx_v8.h"
 #include "fxjs/cfxjse_formcalc_context.h"
 #include "v8/include/v8.h"
@@ -54,8 +55,8 @@
   CFXJSE_Engine(CXFA_Document* pDocument, CFXJS_Engine* fxjs_engine);
   ~CFXJSE_Engine() override;
 
-  void SetEventParam(CXFA_EventParam param) { m_eventParam = param; }
-  CXFA_EventParam* GetEventParam() { return &m_eventParam; }
+  void SetEventParam(CXFA_EventParam* param) { m_eventParam = param; }
+  CXFA_EventParam* GetEventParam() { return m_eventParam.Get(); }
   bool RunScript(CXFA_Script::Type eScriptType,
                  const WideStringView& wsScript,
                  CFXJSE_Value* pRetValue,
@@ -113,7 +114,7 @@
   std::map<CXFA_Object*, std::unique_ptr<CFXJSE_Value>> m_mapObjectToValue;
   std::map<CXFA_Object*, std::unique_ptr<CFXJSE_Context>>
       m_mapVariableToContext;
-  CXFA_EventParam m_eventParam;
+  UnownedPtr<CXFA_EventParam> m_eventParam;
   std::vector<CXFA_Node*> m_upObjectArray;
   // CacheList holds the List items so we can clean them up when we're done.
   std::vector<std::unique_ptr<CXFA_List>> m_CacheList;
diff --git a/fxjs/cfxjse_formcalc_context_embeddertest.cpp b/fxjs/cfxjse_formcalc_context_embeddertest.cpp
index ed0c5bc..a227ac5 100644
--- a/fxjs/cfxjse_formcalc_context_embeddertest.cpp
+++ b/fxjs/cfxjse_formcalc_context_embeddertest.cpp
@@ -1465,7 +1465,7 @@
   params.m_wsChange = L"changed";
 
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
   const char test[] = {"xfa.event.change"};
   EXPECT_TRUE(Execute(test));
@@ -1473,6 +1473,7 @@
   CFXJSE_Value* value = GetValue();
   EXPECT_TRUE(value->IsString());
   EXPECT_STREQ("changed", value->ToString().c_str());
+  context->SetEventParam(nullptr);
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, SetXFAEventChange) {
@@ -1480,11 +1481,12 @@
 
   CXFA_EventParam params;
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
   const char test[] = {"xfa.event.change = \"changed\""};
   EXPECT_TRUE(Execute(test));
-  EXPECT_EQ(L"changed", context->GetEventParam()->m_wsChange);
+  EXPECT_EQ(L"changed", params.m_wsChange);
+  context->SetEventParam(nullptr);
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, SetXFAEventFullTextFails) {
@@ -1494,11 +1496,12 @@
   params.m_wsFullText = L"Original Full Text";
 
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
   const char test[] = {"xfa.event.fullText = \"Changed Full Text\""};
   EXPECT_TRUE(Execute(test));
-  EXPECT_EQ(L"Original Full Text", context->GetEventParam()->m_wsFullText);
+  EXPECT_EQ(L"Original Full Text", params.m_wsFullText);
+  context->SetEventParam(nullptr);
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, EventChangeSelection) {
@@ -1510,49 +1513,51 @@
   params.m_iSelEnd = 3;
 
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
   // Moving end to start works fine.
   EXPECT_TRUE(Execute("xfa.event.selEnd = \"1\""));
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(1, params.m_iSelStart);
+  EXPECT_EQ(1, params.m_iSelEnd);
 
   // Moving end before end, forces start to move in response.
   EXPECT_TRUE(Execute("xfa.event.selEnd = \"0\""));
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(0, params.m_iSelStart);
+  EXPECT_EQ(0, params.m_iSelEnd);
 
   // Negatives not allowed
   EXPECT_TRUE(Execute("xfa.event.selEnd = \"-1\""));
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(0, params.m_iSelStart);
+  EXPECT_EQ(0, params.m_iSelEnd);
 
   // Negatives not allowed
   EXPECT_TRUE(Execute("xfa.event.selStart = \"-1\""));
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(0, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(0, params.m_iSelStart);
+  EXPECT_EQ(0, params.m_iSelEnd);
 
-  context->GetEventParam()->m_iSelEnd = 1;
+  params.m_iSelEnd = 1;
 
   // Moving start to end works fine.
   EXPECT_TRUE(Execute("xfa.event.selStart = \"1\""));
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(1, params.m_iSelStart);
+  EXPECT_EQ(1, params.m_iSelEnd);
 
   // Moving start after end moves end.
   EXPECT_TRUE(Execute("xfa.event.selStart = \"2\""));
-  EXPECT_EQ(2, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(2, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(2, params.m_iSelStart);
+  EXPECT_EQ(2, params.m_iSelEnd);
 
   // Setting End past end of string clamps to string length;
   EXPECT_TRUE(Execute("xfa.event.selEnd = \"20\""));
-  EXPECT_EQ(2, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(4, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(2, params.m_iSelStart);
+  EXPECT_EQ(4, params.m_iSelEnd);
 
   // Setting Start past end of string clamps to string length;
   EXPECT_TRUE(Execute("xfa.event.selStart = \"20\""));
-  EXPECT_EQ(4, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(4, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(4, params.m_iSelStart);
+  EXPECT_EQ(4, params.m_iSelEnd);
+
+  context->SetEventParam(nullptr);
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, XFAEventCancelAction) {
@@ -1562,7 +1567,7 @@
   params.m_bCancelAction = false;
 
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
   EXPECT_TRUE(Execute("xfa.event.cancelAction"));
 
@@ -1571,7 +1576,9 @@
   EXPECT_FALSE(value->ToBoolean());
 
   EXPECT_TRUE(Execute("xfa.event.cancelAction = \"true\""));
-  EXPECT_TRUE(context->GetEventParam()->m_bCancelAction);
+  EXPECT_TRUE(params.m_bCancelAction);
+
+  context->SetEventParam(nullptr);
 }
 
 TEST_F(CFXJSE_FormCalcContextEmbedderTest, ComplexTextChangeEvent) {
@@ -1584,29 +1591,31 @@
   params.m_iSelEnd = 3;
 
   CFXJSE_Engine* context = GetScriptContext();
-  context->SetEventParam(params);
+  context->SetEventParam(&params);
 
-  EXPECT_EQ(L"abcd", context->GetEventParam()->m_wsPrevText);
-  EXPECT_EQ(L"agd", context->GetEventParam()->GetNewText());
-  EXPECT_EQ(L"g", context->GetEventParam()->m_wsChange);
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(3, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(L"abcd", params.m_wsPrevText);
+  EXPECT_EQ(L"agd", params.GetNewText());
+  EXPECT_EQ(L"g", params.m_wsChange);
+  EXPECT_EQ(1, params.m_iSelStart);
+  EXPECT_EQ(3, params.m_iSelEnd);
 
   const char change_event[] = {"xfa.event.change = \"xyz\""};
   EXPECT_TRUE(Execute(change_event));
 
-  EXPECT_EQ(L"abcd", context->GetEventParam()->m_wsPrevText);
-  EXPECT_EQ(L"xyz", context->GetEventParam()->m_wsChange);
-  EXPECT_EQ(L"axyzd", context->GetEventParam()->GetNewText());
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(3, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(L"abcd", params.m_wsPrevText);
+  EXPECT_EQ(L"xyz", params.m_wsChange);
+  EXPECT_EQ(L"axyzd", params.GetNewText());
+  EXPECT_EQ(1, params.m_iSelStart);
+  EXPECT_EQ(3, params.m_iSelEnd);
 
   const char sel_event[] = {"xfa.event.selEnd = \"1\""};
   EXPECT_TRUE(Execute(sel_event));
 
-  EXPECT_EQ(L"abcd", context->GetEventParam()->m_wsPrevText);
-  EXPECT_EQ(L"xyz", context->GetEventParam()->m_wsChange);
-  EXPECT_EQ(L"axyzbcd", context->GetEventParam()->GetNewText());
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelStart);
-  EXPECT_EQ(1, context->GetEventParam()->m_iSelEnd);
+  EXPECT_EQ(L"abcd", params.m_wsPrevText);
+  EXPECT_EQ(L"xyz", params.m_wsChange);
+  EXPECT_EQ(L"axyzbcd", params.GetNewText());
+  EXPECT_EQ(1, params.m_iSelStart);
+  EXPECT_EQ(1, params.m_iSelEnd);
+
+  context->SetEventParam(nullptr);
 }
diff --git a/xfa/fde/cfde_texteditengine.cpp b/xfa/fde/cfde_texteditengine.cpp
index 3363baa..cfe844e 100644
--- a/xfa/fde/cfde_texteditengine.cpp
+++ b/xfa/fde/cfde_texteditengine.cpp
@@ -262,11 +262,35 @@
 }
 
 void CFDE_TextEditEngine::Insert(size_t idx,
-                                 const WideString& text,
+                                 const WideString& request_text,
                                  RecordOperation add_operation) {
+  WideString text = request_text;
+  if (text.GetLength() == 0)
+    return;
   if (idx > text_length_)
     idx = text_length_;
 
+  TextChange change;
+  change.selection_start = idx;
+  change.selection_end = idx;
+  change.text = text;
+  change.previous_text = GetText();
+  change.cancelled = false;
+
+  if (delegate_ && (add_operation != RecordOperation::kSkipRecord &&
+                    add_operation != RecordOperation::kSkipNotify)) {
+    delegate_->OnTextWillChange(&change);
+    if (change.cancelled)
+      return;
+
+    text = change.text;
+    idx = change.selection_start;
+
+    // JS extended the selection, so delete it before we insert.
+    if (change.selection_end != change.selection_start)
+      DeleteSelectedText(RecordOperation::kSkipRecord);
+  }
+
   size_t length = text.GetLength();
   if (length == 0)
     return;
@@ -276,11 +300,11 @@
   bool exceeded_limit = false;
 
   // Currently we allow inserting a number of characters over the text limit if
-  // the text edit is already empty. This allows supporting text fields which
-  // do formatting. Otherwise, if you enter 123456789 for an SSN into a field
+  // we're skipping notify. This means we're setting the formatted text into the
+  // engine. Otherwise, if you enter 123456789 for an SSN into a field
   // with a 9 character limit and we reformat to 123-45-6789 we'll truncate
   // the 89 when inserting into the text edit. See https://crbug.com/pdfium/1089
-  if (has_character_limit_ && text_length_ > 0 &&
+  if (has_character_limit_ && add_operation != RecordOperation::kSkipNotify &&
       text_length_ + length > character_limit_) {
     exceeded_limit = true;
     length = character_limit_ - text_length_;
@@ -348,7 +372,7 @@
     if (exceeded_limit)
       delegate_->NotifyTextFull();
 
-    delegate_->OnTextChanged(previous_text);
+    delegate_->OnTextChanged();
   }
 }
 
@@ -811,6 +835,23 @@
   if (start_idx >= text_length_)
     return L"";
 
+  TextChange change;
+  change.text = L"";
+  change.cancelled = false;
+  if (delegate_ && (add_operation != RecordOperation::kSkipRecord &&
+                    add_operation != RecordOperation::kSkipNotify)) {
+    change.previous_text = GetText();
+    change.selection_start = start_idx;
+    change.selection_end = start_idx + length;
+
+    delegate_->OnTextWillChange(&change);
+    if (change.cancelled)
+      return L"";
+
+    start_idx = change.selection_start;
+    length = change.selection_end - change.selection_start;
+  }
+
   length = std::min(length, text_length_ - start_idx);
   AdjustGap(start_idx + length, 0);
 
@@ -831,15 +872,37 @@
   is_dirty_ = true;
   ClearSelection();
 
+  // The JS requested the insertion of text instead of just a deletion.
+  if (change.text != L"")
+    Insert(start_idx, change.text, RecordOperation::kSkipRecord);
+
   if (delegate_)
-    delegate_->OnTextChanged(previous_text);
+    delegate_->OnTextChanged();
 
   return ret;
 }
 
-void CFDE_TextEditEngine::ReplaceSelectedText(const WideString& rep) {
-  size_t start_idx = selection_.start_idx;
+void CFDE_TextEditEngine::ReplaceSelectedText(const WideString& requested_rep) {
+  WideString rep = requested_rep;
 
+  if (delegate_) {
+    TextChange change;
+    change.selection_start = selection_.start_idx;
+    change.selection_end = selection_.start_idx + selection_.count;
+    change.text = rep;
+    change.previous_text = GetText();
+    change.cancelled = false;
+
+    delegate_->OnTextWillChange(&change);
+    if (change.cancelled)
+      return;
+
+    rep = change.text;
+    selection_.start_idx = change.selection_start;
+    selection_.count = change.selection_end - change.selection_start;
+  }
+
+  size_t start_idx = selection_.start_idx;
   WideString txt = DeleteSelectedText(RecordOperation::kSkipRecord);
   Insert(gap_position_, rep, RecordOperation::kSkipRecord);
 
@@ -1079,13 +1142,7 @@
   if (IsAlignedRight() && bounds_smaller) {
     delta = available_width_ - contents_bounding_box_.width;
   } else if (IsAlignedCenter() && bounds_smaller) {
-    // TODO(dsinclair): Old code used CombText here and set the space to
-    // something unrelated to the available width .... Figure out if this is
-    // needed and what it should do.
-    // if (is_comb_text_) {
-    // } else {
     delta = (available_width_ - contents_bounding_box_.width) / 2.0f;
-    // }
   }
 
   if (delta != 0.0) {
diff --git a/xfa/fde/cfde_texteditengine.h b/xfa/fde/cfde_texteditengine.h
index 58f77ed..868be88 100644
--- a/xfa/fde/cfde_texteditengine.h
+++ b/xfa/fde/cfde_texteditengine.h
@@ -61,21 +61,27 @@
     virtual void Undo() const = 0;
   };
 
+  struct TextChange {
+    WideString text;
+    WideString previous_text;
+    size_t selection_start;
+    size_t selection_end;
+    bool cancelled;
+  };
+
   class Delegate {
    public:
     virtual ~Delegate() = default;
     virtual void NotifyTextFull() = 0;
     virtual void OnCaretChanged() = 0;
-    virtual void OnTextChanged(const WideString& prevText) = 0;
+    virtual void OnTextWillChange(TextChange* change) = 0;
+    virtual void OnTextChanged() = 0;
     virtual void OnSelChanged() = 0;
     virtual bool OnValidate(const WideString& wsText) = 0;
     virtual void SetScrollOffset(float fScrollOffset) = 0;
   };
 
-  enum class RecordOperation {
-    kInsertRecord,
-    kSkipRecord,
-  };
+  enum class RecordOperation { kInsertRecord, kSkipRecord, kSkipNotify };
 
   CFDE_TextEditEngine();
   ~CFDE_TextEditEngine();
diff --git a/xfa/fde/cfde_texteditengine_unittest.cpp b/xfa/fde/cfde_texteditengine_unittest.cpp
index 123d16c..c5efe52 100644
--- a/xfa/fde/cfde_texteditengine_unittest.cpp
+++ b/xfa/fde/cfde_texteditengine_unittest.cpp
@@ -21,7 +21,8 @@
     void NotifyTextFull() override { text_is_full = true; }
 
     void OnCaretChanged() override {}
-    void OnTextChanged(const WideString& prevText) override {}
+    void OnTextWillChange(CFDE_TextEditEngine::TextChange* change) override {}
+    void OnTextChanged() override {}
     void OnSelChanged() override {}
     bool OnValidate(const WideString& wsText) override {
       return !fail_validation;
diff --git a/xfa/fwl/README.md b/xfa/fwl/README.md
index 95a0f4c..6460ff1 100644
--- a/xfa/fwl/README.md
+++ b/xfa/fwl/README.md
@@ -46,7 +46,7 @@
     * CFWL_EventMouse
     * CFWL_EventScroll
     * CFWL_EventSelectChanged
-    * CFWL_EventTextChanged
+    * CFWL_EventTextWillChange
     * CFWL_EventValidate
 
 The widgets use IFWL_ThemeProvider for rendering everything, calling
diff --git a/xfa/fwl/cfwl_barcode.cpp b/xfa/fwl/cfwl_barcode.cpp
index 74ac7dd..649dea0 100644
--- a/xfa/fwl/cfwl_barcode.cpp
+++ b/xfa/fwl/cfwl_barcode.cpp
@@ -67,10 +67,11 @@
   m_dwStatus = XFA_BCS_NeedUpdate;
 }
 
-void CFWL_Barcode::SetText(const WideString& wsText) {
+void CFWL_Barcode::SetText(const WideString& wsText,
+                           CFDE_TextEditEngine::RecordOperation op) {
   m_pBarcodeEngine.reset();
   m_dwStatus = XFA_BCS_NeedUpdate;
-  CFWL_Edit::SetText(wsText);
+  CFWL_Edit::SetText(wsText, op);
 }
 
 bool CFWL_Barcode::IsProtectedType() const {
@@ -86,7 +87,7 @@
 }
 
 void CFWL_Barcode::OnProcessEvent(CFWL_Event* pEvent) {
-  if (pEvent->GetType() == CFWL_Event::Type::TextChanged) {
+  if (pEvent->GetType() == CFWL_Event::Type::TextWillChange) {
     m_pBarcodeEngine.reset();
     m_dwStatus = XFA_BCS_NeedUpdate;
   }
diff --git a/xfa/fwl/cfwl_barcode.h b/xfa/fwl/cfwl_barcode.h
index 2fc7960..58484df 100644
--- a/xfa/fwl/cfwl_barcode.h
+++ b/xfa/fwl/cfwl_barcode.h
@@ -49,7 +49,10 @@
   void OnProcessEvent(CFWL_Event* pEvent) override;
 
   // CFWL_Edit
-  void SetText(const WideString& wsText) override;
+  void SetText(
+      const WideString& wsText,
+      CFDE_TextEditEngine::RecordOperation op =
+          CFDE_TextEditEngine::RecordOperation::kInsertRecord) override;
 
   void SetType(BC_TYPE type);
   bool IsProtectedType() const;
diff --git a/xfa/fwl/cfwl_combobox.cpp b/xfa/fwl/cfwl_combobox.cpp
index 8980dc3..9cd2119 100644
--- a/xfa/fwl/cfwl_combobox.cpp
+++ b/xfa/fwl/cfwl_combobox.cpp
@@ -16,7 +16,6 @@
 #include "xfa/fwl/cfwl_app.h"
 #include "xfa/fwl/cfwl_event.h"
 #include "xfa/fwl/cfwl_eventselectchanged.h"
-#include "xfa/fwl/cfwl_eventtextchanged.h"
 #include "xfa/fwl/cfwl_listbox.h"
 #include "xfa/fwl/cfwl_messagekey.h"
 #include "xfa/fwl/cfwl_messagekillfocus.h"
@@ -505,7 +504,7 @@
     pScrollEv.m_iScrollCode = pScrollEvent->m_iScrollCode;
     pScrollEv.m_fPos = pScrollEvent->m_fPos;
     DispatchEvent(&pScrollEv);
-  } else if (type == CFWL_Event::Type::TextChanged) {
+  } else if (type == CFWL_Event::Type::TextWillChange) {
     CFWL_Event pTemp(CFWL_Event::Type::EditChanged, this);
     DispatchEvent(&pTemp);
   }
diff --git a/xfa/fwl/cfwl_edit.cpp b/xfa/fwl/cfwl_edit.cpp
index 7bdb818..7c4f0e1 100644
--- a/xfa/fwl/cfwl_edit.cpp
+++ b/xfa/fwl/cfwl_edit.cpp
@@ -19,7 +19,7 @@
 #include "xfa/fwl/cfwl_app.h"
 #include "xfa/fwl/cfwl_caret.h"
 #include "xfa/fwl/cfwl_event.h"
-#include "xfa/fwl/cfwl_eventtextchanged.h"
+#include "xfa/fwl/cfwl_eventtextwillchange.h"
 #include "xfa/fwl/cfwl_eventvalidate.h"
 #include "xfa/fwl/cfwl_messagekey.h"
 #include "xfa/fwl/cfwl_messagemouse.h"
@@ -171,9 +171,10 @@
   m_pProperties->m_pThemeProvider = pThemeProvider;
 }
 
-void CFWL_Edit::SetText(const WideString& wsText) {
+void CFWL_Edit::SetText(const WideString& wsText,
+                        CFDE_TextEditEngine::RecordOperation op) {
   m_EdtEngine.Clear();
-  m_EdtEngine.Insert(0, wsText);
+  m_EdtEngine.Insert(0, wsText, op);
 }
 
 int32_t CFWL_Edit::GetTextLength() const {
@@ -297,14 +298,26 @@
   }
 }
 
-void CFWL_Edit::OnTextChanged(const WideString& prevText) {
+void CFWL_Edit::OnTextWillChange(CFDE_TextEditEngine::TextChange* change) {
+  CFWL_EventTextWillChange event(this);
+  event.previous_text = change->previous_text;
+  event.change_text = change->text;
+  event.selection_start = change->selection_start;
+  event.selection_end = change->selection_end;
+  event.cancelled = false;
+
+  DispatchEvent(&event);
+
+  change->text = event.change_text;
+  change->selection_start = event.selection_start;
+  change->selection_end = event.selection_end;
+  change->cancelled = event.cancelled;
+}
+
+void CFWL_Edit::OnTextChanged() {
   if (m_pProperties->m_dwStyleExes & FWL_STYLEEXT_EDT_VAlignMask)
     UpdateVAlignment();
 
-  CFWL_EventTextChanged event(this);
-  event.wsPrevText = prevText;
-  DispatchEvent(&event);
-
   LayoutScrollBar();
   RepaintRect(GetClientRect());
 }
diff --git a/xfa/fwl/cfwl_edit.h b/xfa/fwl/cfwl_edit.h
index 894315d..634ac22 100644
--- a/xfa/fwl/cfwl_edit.h
+++ b/xfa/fwl/cfwl_edit.h
@@ -65,7 +65,9 @@
   void OnDrawWidget(CXFA_Graphics* pGraphics,
                     const CFX_Matrix& matrix) override;
 
-  virtual void SetText(const WideString& wsText);
+  virtual void SetText(const WideString& wsText,
+                       CFDE_TextEditEngine::RecordOperation op =
+                           CFDE_TextEditEngine::RecordOperation::kInsertRecord);
 
   int32_t GetTextLength() const;
   WideString GetText() const;
@@ -93,7 +95,8 @@
   // CFDE_TextEditEngine::Delegate
   void NotifyTextFull() override;
   void OnCaretChanged() override;
-  void OnTextChanged(const WideString& prevText) override;
+  void OnTextWillChange(CFDE_TextEditEngine::TextChange* change) override;
+  void OnTextChanged() override;
   void OnSelChanged() override;
   bool OnValidate(const WideString& wsText) override;
   void SetScrollOffset(float fScrollOffset) override;
diff --git a/xfa/fwl/cfwl_event.h b/xfa/fwl/cfwl_event.h
index 787f8c2..8546447 100644
--- a/xfa/fwl/cfwl_event.h
+++ b/xfa/fwl/cfwl_event.h
@@ -28,7 +28,7 @@
     PreDropDown,
     Scroll,
     SelectChanged,
-    TextChanged,
+    TextWillChange,
     TextFull,
     Validate
   };
diff --git a/xfa/fwl/cfwl_eventtextchanged.cpp b/xfa/fwl/cfwl_eventtextchanged.cpp
deleted file mode 100644
index 439d99d..0000000
--- a/xfa/fwl/cfwl_eventtextchanged.cpp
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2016 PDFium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
-
-#include "xfa/fwl/cfwl_eventtextchanged.h"
-
-CFWL_EventTextChanged::CFWL_EventTextChanged(CFWL_Widget* pSrcTarget)
-    : CFWL_Event(CFWL_Event::Type::TextChanged, pSrcTarget) {}
-
-CFWL_EventTextChanged::~CFWL_EventTextChanged() {}
diff --git a/xfa/fwl/cfwl_eventtextchanged.h b/xfa/fwl/cfwl_eventtextchanged.h
deleted file mode 100644
index 4494f08..0000000
--- a/xfa/fwl/cfwl_eventtextchanged.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2016 PDFium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
-
-#ifndef XFA_FWL_CFWL_EVENTTEXTCHANGED_H_
-#define XFA_FWL_CFWL_EVENTTEXTCHANGED_H_
-
-#include "xfa/fwl/cfwl_event.h"
-
-class CFWL_EventTextChanged : public CFWL_Event {
- public:
-  explicit CFWL_EventTextChanged(CFWL_Widget* pSrcTarget);
-  ~CFWL_EventTextChanged() override;
-
-  WideString wsPrevText;
-};
-
-#endif  // XFA_FWL_CFWL_EVENTTEXTCHANGED_H_
diff --git a/xfa/fwl/cfwl_eventtextwillchange.cpp b/xfa/fwl/cfwl_eventtextwillchange.cpp
new file mode 100644
index 0000000..22b1100
--- /dev/null
+++ b/xfa/fwl/cfwl_eventtextwillchange.cpp
@@ -0,0 +1,12 @@
+// Copyright 2016 PDFium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
+
+#include "xfa/fwl/cfwl_eventtextwillchange.h"
+
+CFWL_EventTextWillChange::CFWL_EventTextWillChange(CFWL_Widget* pSrcTarget)
+    : CFWL_Event(CFWL_Event::Type::TextWillChange, pSrcTarget) {}
+
+CFWL_EventTextWillChange::~CFWL_EventTextWillChange() = default;
diff --git a/xfa/fwl/cfwl_eventtextwillchange.h b/xfa/fwl/cfwl_eventtextwillchange.h
new file mode 100644
index 0000000..4b2781f
--- /dev/null
+++ b/xfa/fwl/cfwl_eventtextwillchange.h
@@ -0,0 +1,24 @@
+// Copyright 2016 PDFium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
+
+#ifndef XFA_FWL_CFWL_EVENTTEXTWILLCHANGE_H_
+#define XFA_FWL_CFWL_EVENTTEXTWILLCHANGE_H_
+
+#include "xfa/fwl/cfwl_event.h"
+
+class CFWL_EventTextWillChange : public CFWL_Event {
+ public:
+  explicit CFWL_EventTextWillChange(CFWL_Widget* pSrcTarget);
+  ~CFWL_EventTextWillChange() override;
+
+  WideString change_text;
+  WideString previous_text;
+  bool cancelled = false;
+  size_t selection_start = 0;
+  size_t selection_end = 0;
+};
+
+#endif  // XFA_FWL_CFWL_EVENTTEXTWILLCHANGE_H_
diff --git a/xfa/fxfa/cxfa_eventparam.cpp b/xfa/fxfa/cxfa_eventparam.cpp
index 4e3cae9..4746a6a 100644
--- a/xfa/fxfa/cxfa_eventparam.cpp
+++ b/xfa/fxfa/cxfa_eventparam.cpp
@@ -21,7 +21,7 @@
       m_bShift(false),
       m_bIsFormReady(false) {}
 
-CXFA_EventParam::~CXFA_EventParam() {}
+CXFA_EventParam::~CXFA_EventParam() = default;
 
 CXFA_EventParam::CXFA_EventParam(const CXFA_EventParam& other) = default;
 
diff --git a/xfa/fxfa/cxfa_fftextedit.cpp b/xfa/fxfa/cxfa_fftextedit.cpp
index 02787d5..a8080d1 100644
--- a/xfa/fxfa/cxfa_fftextedit.cpp
+++ b/xfa/fxfa/cxfa_fftextedit.cpp
@@ -11,7 +11,7 @@
 #include "xfa/fwl/cfwl_datetimepicker.h"
 #include "xfa/fwl/cfwl_edit.h"
 #include "xfa/fwl/cfwl_eventtarget.h"
-#include "xfa/fwl/cfwl_eventtextchanged.h"
+#include "xfa/fwl/cfwl_eventtextwillchange.h"
 #include "xfa/fwl/cfwl_messagekillfocus.h"
 #include "xfa/fwl/cfwl_messagesetfocus.h"
 #include "xfa/fwl/cfwl_notedriver.h"
@@ -287,7 +287,7 @@
   WideString wsText = m_pNode->GetValue(eType);
   WideString wsOldText = pEdit->GetText();
   if (wsText != wsOldText || (eType == XFA_VALUEPICTURE_Edit && bUpdate)) {
-    pEdit->SetText(wsText);
+    pEdit->SetText(wsText, CFDE_TextEditEngine::RecordOperation::kSkipNotify);
     bUpdate = true;
   }
   if (bUpdate)
@@ -296,28 +296,26 @@
   return true;
 }
 
-void CXFA_FFTextEdit::OnTextChanged(CFWL_Widget* pWidget,
-                                    const WideString& wsChanged,
-                                    const WideString& wsPrevText) {
+void CXFA_FFTextEdit::OnTextWillChange(CFWL_Widget* pWidget,
+                                       CFWL_EventTextWillChange* event) {
   m_dwStatus |= XFA_WidgetStatus_TextEditValueChanged;
+
   CXFA_EventParam eParam;
   eParam.m_eType = XFA_EVENT_Change;
-  eParam.m_wsChange = wsChanged;
+  eParam.m_wsChange = event->change_text;
   eParam.m_pTarget = m_pNode.Get();
-  eParam.m_wsPrevText = wsPrevText;
-  if (m_pNode->GetFFWidgetType() == XFA_FFWidgetType::kDateTimeEdit) {
-    auto* pDateTime = static_cast<CFWL_DateTimePicker*>(m_pNormalWidget.get());
-    if (pDateTime->HasSelection()) {
-      size_t count;
-      std::tie(eParam.m_iSelStart, count) = pDateTime->GetSelection();
-      eParam.m_iSelEnd = eParam.m_iSelStart + count;
-    }
-  } else {
-    CFWL_Edit* pEdit = ToEdit(m_pNormalWidget.get());
-    if (pEdit->HasSelection())
-      std::tie(eParam.m_iSelStart, eParam.m_iSelEnd) = pEdit->GetSelection();
-  }
+  eParam.m_wsPrevText = event->previous_text;
+  eParam.m_iSelStart = static_cast<int32_t>(event->selection_start);
+  eParam.m_iSelEnd = static_cast<int32_t>(event->selection_end);
+
   m_pNode->ProcessEvent(GetDocView(), XFA_AttributeEnum::Change, &eParam);
+
+  // Copy the data back out of the EventParam and into the TextChanged event so
+  // it can propagate back to the calling widget.
+  event->cancelled = eParam.m_bCancelAction;
+  event->change_text = eParam.m_wsChange;
+  event->selection_start = static_cast<size_t>(eParam.m_iSelStart);
+  event->selection_end = static_cast<size_t>(eParam.m_iSelEnd);
 }
 
 void CXFA_FFTextEdit::OnTextFull(CFWL_Widget* pWidget) {
@@ -334,17 +332,13 @@
 void CXFA_FFTextEdit::OnProcessEvent(CFWL_Event* pEvent) {
   CXFA_FFField::OnProcessEvent(pEvent);
   switch (pEvent->GetType()) {
-    case CFWL_Event::Type::TextChanged: {
-      CFWL_EventTextChanged* event =
-          static_cast<CFWL_EventTextChanged*>(pEvent);
-      WideString wsChange;
-      OnTextChanged(m_pNormalWidget.get(), wsChange, event->wsPrevText);
+    case CFWL_Event::Type::TextWillChange:
+      OnTextWillChange(m_pNormalWidget.get(),
+                       static_cast<CFWL_EventTextWillChange*>(pEvent));
       break;
-    }
-    case CFWL_Event::Type::TextFull: {
+    case CFWL_Event::Type::TextFull:
       OnTextFull(m_pNormalWidget.get());
       break;
-    }
     default:
       break;
   }
diff --git a/xfa/fxfa/cxfa_fftextedit.h b/xfa/fxfa/cxfa_fftextedit.h
index 7c19444..9b61373 100644
--- a/xfa/fxfa/cxfa_fftextedit.h
+++ b/xfa/fxfa/cxfa_fftextedit.h
@@ -12,6 +12,7 @@
 #include "xfa/fxfa/cxfa_fffield.h"
 
 class CFWL_Event;
+class CFWL_EventTextWillChange;
 class CFWL_Widget;
 class CFX_Matrix;
 class CXFA_FFWidget;
@@ -38,9 +39,7 @@
   void OnDrawWidget(CXFA_Graphics* pGraphics,
                     const CFX_Matrix& matrix) override;
 
-  void OnTextChanged(CFWL_Widget* pWidget,
-                     const WideString& wsChanged,
-                     const WideString& wsPrevText);
+  void OnTextWillChange(CFWL_Widget* pWidget, CFWL_EventTextWillChange* change);
   void OnTextFull(CFWL_Widget* pWidget);
 
   // CXFA_FFWidget
diff --git a/xfa/fxfa/parser/cxfa_node.cpp b/xfa/fxfa/parser/cxfa_node.cpp
index 66c4fb9..6672e7f 100644
--- a/xfa/fxfa/parser/cxfa_node.cpp
+++ b/xfa/fxfa/parser/cxfa_node.cpp
@@ -2290,7 +2290,7 @@
 
   CXFA_FFDoc* pDoc = docView->GetDoc();
   CFXJSE_Engine* pContext = pDoc->GetXFADoc()->GetScriptContext();
-  pContext->SetEventParam(*pEventParam);
+  pContext->SetEventParam(pEventParam);
   pContext->SetRunAtType(script->GetRunAt());
 
   std::vector<CXFA_Node*> refNodes;
@@ -2344,6 +2344,7 @@
     }
   }
   pContext->SetNodesOfRunScript(nullptr);
+  pContext->SetEventParam(nullptr);
 
   return {iRet, pTmpRetValue->IsBoolean() ? pTmpRetValue->ToBoolean() : false};
 }