fit-ui 2.5.4 → 2.6.0

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 (39) hide show
  1. package/dist/Documentation.html +3 -3
  2. package/dist/Fit.UI.css +102 -6
  3. package/dist/Fit.UI.js +986 -132
  4. package/dist/Fit.UI.min.css +1 -1
  5. package/dist/Fit.UI.min.js +1 -1
  6. package/dist/Resources/CKEditor/CHANGES.md +107 -0
  7. package/dist/Resources/CKEditor/LICENSE.md +1 -0
  8. package/dist/Resources/CKEditor/build-config.js +7 -3
  9. package/dist/Resources/CKEditor/ckeditor.js +638 -538
  10. package/dist/Resources/CKEditor/contents.css +208 -208
  11. package/dist/Resources/CKEditor/index.html +8 -0
  12. package/dist/Resources/CKEditor/lang/da.js +2 -2
  13. package/dist/Resources/CKEditor/lang/de.js +2 -2
  14. package/dist/Resources/CKEditor/lang/en.js +2 -2
  15. package/dist/Resources/CKEditor/plugins/autocomplete/skins/default.css +38 -0
  16. package/dist/Resources/CKEditor/plugins/base64image/dialogs/base64image.js +38 -33
  17. package/dist/Resources/CKEditor/plugins/base64image/plugin.js +1 -1
  18. package/dist/Resources/CKEditor/plugins/emoji/assets/iconsall.png +0 -0
  19. package/dist/Resources/CKEditor/plugins/emoji/assets/iconsall.svg +58 -0
  20. package/dist/Resources/CKEditor/plugins/emoji/emoji.json +1 -0
  21. package/dist/Resources/CKEditor/plugins/emoji/skins/default.css +237 -0
  22. package/dist/Resources/CKEditor/plugins/icons.png +0 -0
  23. package/dist/Resources/CKEditor/plugins/icons_hidpi.png +0 -0
  24. package/dist/Resources/CKEditor/plugins/link/dialogs/anchor.js +4 -4
  25. package/dist/Resources/CKEditor/skins/moono-lisa/dialog.css +5 -5
  26. package/dist/Resources/CKEditor/skins/moono-lisa/dialog_ie.css +5 -5
  27. package/dist/Resources/CKEditor/skins/moono-lisa/dialog_ie8.css +5 -5
  28. package/dist/Resources/CKEditor/skins/moono-lisa/dialog_iequirks.css +5 -5
  29. package/dist/Resources/CKEditor/skins/moono-lisa/editor.css +1 -1
  30. package/dist/Resources/CKEditor/skins/moono-lisa/editor_gecko.css +1 -1
  31. package/dist/Resources/CKEditor/skins/moono-lisa/editor_ie.css +1 -1
  32. package/dist/Resources/CKEditor/skins/moono-lisa/editor_ie8.css +1 -1
  33. package/dist/Resources/CKEditor/skins/moono-lisa/editor_iequirks.css +1 -1
  34. package/dist/Resources/CKEditor/skins/moono-lisa/icons.png +0 -0
  35. package/dist/Resources/CKEditor/skins/moono-lisa/icons_hidpi.png +0 -0
  36. package/dist/Resources/CKEditor/styles.js +137 -137
  37. package/package.json +1 -1
  38. package/types/index.d.ts +417 -38
  39. package/dist/Resources/CKEditor/plugins/resize/plugin.js +0 -187
package/dist/Fit.UI.js CHANGED
@@ -648,7 +648,7 @@ Fit._internal =
648
648
  {
649
649
  Core:
650
650
  {
651
- VersionInfo: { Major: 2, Minor: 5, Patch: 4 } // Do NOT modify format - version numbers are programmatically changed when releasing new versions - MUST be on a separate line!
651
+ VersionInfo: { Major: 2, Minor: 6, Patch: 0 } // Do NOT modify format - version numbers are programmatically changed when releasing new versions - MUST be on a separate line!
652
652
  }
653
653
  };
654
654
 
@@ -4281,6 +4281,7 @@ Fit.Controls.ControlBase = function(controlId)
4281
4281
 
4282
4282
  var onChangeHandler = me._internal.FireOnChange;
4283
4283
  me._internal.FireOnChange = function() {};
4284
+ me._internal.FireOnChangeSuppressed = true; // Allow specialized controls to detect when OnChange event will be suppressed for performance optimizations
4284
4285
 
4285
4286
  var error = null;
4286
4287
 
@@ -4294,10 +4295,12 @@ Fit.Controls.ControlBase = function(controlId)
4294
4295
  }
4295
4296
 
4296
4297
  me._internal.FireOnChange = onChangeHandler;
4298
+ me._internal.FireOnChangeSuppressed = false;
4297
4299
 
4298
4300
  if (error !== null)
4299
4301
  Fit.Validation.ThrowError(error);
4300
4302
  }
4303
+ this._internal.FireOnChangeSuppressed = false;
4301
4304
 
4302
4305
  this._internal.Data = function(key, val)
4303
4306
  {
@@ -9264,6 +9267,7 @@ Fit.Http.JsonpRequest = function(uri, jsonpCallbackName)
9264
9267
  var timeout = 30000;
9265
9268
  var timer = -1;
9266
9269
  var response = null;
9270
+ var aborted = false;
9267
9271
 
9268
9272
  var onRequestHandlers = [];
9269
9273
  var onSuccessHandlers = [];
@@ -9321,6 +9325,14 @@ Fit.Http.JsonpRequest = function(uri, jsonpCallbackName)
9321
9325
  return timeout;
9322
9326
  }
9323
9327
 
9328
+ /// <function container="Fit.Http.JsonpRequest" name="Abort" access="public">
9329
+ /// <description> Abort request </description>
9330
+ /// </function>
9331
+ this.Abort = function()
9332
+ {
9333
+ aborted = true;
9334
+ }
9335
+
9324
9336
  /// <function container="Fit.Http.JsonpRequest" name="SetParameter" access="public">
9325
9337
  /// <description> Set URL parameter </description>
9326
9338
  /// <param name="key" type="string"> URL parameter key </param>
@@ -9370,6 +9382,8 @@ Fit.Http.JsonpRequest = function(uri, jsonpCallbackName)
9370
9382
  /// </function>
9371
9383
  this.Start = function()
9372
9384
  {
9385
+ aborted = false;
9386
+
9373
9387
  // Fire OnRequest handlers
9374
9388
 
9375
9389
  if (fireEvent(onRequestHandlers) === false)
@@ -9403,7 +9417,10 @@ Fit.Http.JsonpRequest = function(uri, jsonpCallbackName)
9403
9417
  response = resp;
9404
9418
  delete Fit._internal.Http.JsonpRequest.Callbacks[cbId];
9405
9419
 
9406
- fireEvent(onSuccessHandlers);
9420
+ if (aborted === false)
9421
+ {
9422
+ fireEvent(onSuccessHandlers);
9423
+ }
9407
9424
  }
9408
9425
 
9409
9426
  // Configure timeout
@@ -9416,7 +9433,10 @@ Fit.Http.JsonpRequest = function(uri, jsonpCallbackName)
9416
9433
  // NOTICE: Do not remove callback - it would cause a JavaScript error if the situation described above occures.
9417
9434
  Fit._internal.Http.JsonpRequest.Callbacks[cbId] = function(response) { };
9418
9435
 
9419
- fireEvent(onTimeoutHandlers);
9436
+ if (aborted === false)
9437
+ {
9438
+ fireEvent(onTimeoutHandlers);
9439
+ }
9420
9440
  }, timeout);
9421
9441
 
9422
9442
  // Construct request URL
@@ -14230,6 +14250,11 @@ Fit.Controls.DatePicker = function(ctlId)
14230
14250
 
14231
14251
  function moveCalenderWidgetLocally()
14232
14252
  {
14253
+ if (Fit._internal.ControlBase.ReduceDocumentRootPollution !== true)
14254
+ {
14255
+ return;
14256
+ }
14257
+
14233
14258
  // We want the benefits of a connected calendar control (connected to input control),
14234
14259
  // but do not want it rendered in the root of the document. It pollutes the global scope,
14235
14260
  // and it doesn't work with dialogs with light dismiss, since interacting with the calendar
@@ -14305,6 +14330,11 @@ Fit.Controls.DatePicker = function(ctlId)
14305
14330
 
14306
14331
  function moveCalenderWidgetGlobally() // Undo everything done in moveCalenderWidgetLocally()
14307
14332
  {
14333
+ if (Fit._internal.ControlBase.ReduceDocumentRootPollution !== true)
14334
+ {
14335
+ return;
14336
+ }
14337
+
14308
14338
  var calendarWidget = document.getElementById("fitui-datepicker-div");
14309
14339
  Fit.Dom.Add(document.body, calendarWidget);
14310
14340
 
@@ -15795,6 +15825,7 @@ Fit.Controls.DropDown = function(ctlId)
15795
15825
  var detectBoundariesRelToViewPort = false; // Flag indicating whether drop down menu should be positioned relative to viewport (true) or scroll parent (false)
15796
15826
  var persistView = false; // Flag indicating whether picker controls should remember and restore its scroll position and highlighted item when reopened
15797
15827
  var highlightFirst = false; // Flag indicating whether picker controls should focus its first node automatically when opened
15828
+ var searchModeOnFocus = false; // Flag indicating whether control goes into search mode when it is focused (search mode clears input field and displays "search.." placeholder)
15798
15829
 
15799
15830
  var onInputChangedHandlers = []; // Invoked when input value is changed - takes two arguments (sender (this), text value)
15800
15831
  var onPasteHandlers = []; // Invoked when a value is pasted - takes two arguments (sender (this), text value)
@@ -16040,6 +16071,53 @@ Fit.Controls.DropDown = function(ctlId)
16040
16071
  }
16041
16072
  });
16042
16073
 
16074
+ // Support for SearchModeOnFocus
16075
+
16076
+ me.OnFocus(function(sender)
16077
+ {
16078
+ if (searchModeOnFocus === true)
16079
+ {
16080
+ searchModeOnFocus = false; // Temporarily disable searchModeOnFocus to allow setInputEditing(..) (which is called by ClearInputForSearch(..)) to change editing state for txtPrimary
16081
+ me._internal.ClearInputForSearch(true); // True = keep DropDown open - do not auto close it
16082
+ searchModeOnFocus = true;
16083
+ }
16084
+ });
16085
+
16086
+ me.OnBlur(function()
16087
+ {
16088
+ if (searchModeOnFocus === true)
16089
+ {
16090
+ searchModeOnFocus = false; // Temporarily disable searchModeOnFocus to allow setInputEditing(..) to change editing state for txtPrimary
16091
+ me._internal.UndoClearInputForSearch();
16092
+ searchModeOnFocus = true;
16093
+ }
16094
+ });
16095
+
16096
+ me.OnChange(function()
16097
+ {
16098
+ // Determine whether value was changed by user or programmatically,
16099
+ // so placeholder is not set when value is assigned programmatically.
16100
+ // Control might not be focused on mobile if opened using arrow icon.
16101
+ // In this case we simply use the DropDown's opened state instead.
16102
+ var controlIsActive = me.Focused() === true || me.IsDropDownOpen() === true;
16103
+
16104
+ if (searchModeOnFocus === true && controlIsActive === true)
16105
+ {
16106
+ if (me.GetSelections().length === 0)
16107
+ {
16108
+ // DropDown has a synthetic placeholder which is displayed
16109
+ // when no items are selected. Remove placeholder from input
16110
+ // field so we do not get two placeholders on top of each other.
16111
+ txtPrimary.placeholder = "";
16112
+ }
16113
+ else
16114
+ {
16115
+ // Display placeholder in input field when items are selected
16116
+ txtPrimary.placeholder = me.Placeholder();
16117
+ }
16118
+ }
16119
+ });
16120
+
16043
16121
  // PickerBase - make picker aware of focused state of host control
16044
16122
 
16045
16123
  me.OnFocus(function()
@@ -16270,7 +16348,7 @@ Fit.Controls.DropDown = function(ctlId)
16270
16348
  itemDropZones[key].Dispose();
16271
16349
  });
16272
16350
 
16273
- me = itemContainer = itemCollection = itemDropZones = arrow = txtPrimary = txtActive = txtEnabled = dropDownMenu = picker = orgSelections = invalidMessage = invalidMessageChanged = initialFocus = maxHeight = prevValue = focusAssigned = closeHandlers = dropZone = isMobile = focusInputOnMobile = detectBoundaries = detectBoundariesRelToViewPort = persistView = highlightFirst = onInputChangedHandlers = onPasteHandlers = onOpenHandlers = onCloseHandlers = suppressUpdateItemSelectionState = suppressOnItemSelectionChanged = clearTextSelectionOnInputChange = prevTextSelection = textSelectionCallback = cmdToggleTextMode = null;
16351
+ me = itemContainer = itemCollection = itemDropZones = arrow = txtPrimary = txtActive = txtEnabled = dropDownMenu = picker = orgSelections = invalidMessage = invalidMessageChanged = initialFocus = maxHeight = prevValue = focusAssigned = closeHandlers = dropZone = isMobile = focusInputOnMobile = detectBoundaries = detectBoundariesRelToViewPort = persistView = highlightFirst = searchModeOnFocus = onInputChangedHandlers = onPasteHandlers = onOpenHandlers = onCloseHandlers = suppressUpdateItemSelectionState = suppressOnItemSelectionChanged = clearTextSelectionOnInputChange = prevTextSelection = textSelectionCallback = cmdToggleTextMode = null;
16274
16352
 
16275
16353
  base();
16276
16354
  });
@@ -17261,9 +17339,30 @@ Fit.Controls.DropDown = function(ctlId)
17261
17339
  if (Fit._internal.DropDown.Current !== null && Fit._internal.DropDown.Current !== me)
17262
17340
  Fit._internal.DropDown.Current.CloseDropDown();
17263
17341
 
17264
- if (txtActive === txtPrimary && me.GetInputValue() === "")
17342
+ if (searchModeOnFocus === false && me.TextSelectionMode() === false && txtActive === txtPrimary && me.GetInputValue() === "")
17265
17343
  {
17266
- me._internal.UndoClearInputForSearch();
17344
+ // Visual Selection Mode with SearchModeOnFocus (which keeps txtPrimary locked in place) disabled:
17345
+
17346
+ // When placing the control in search mode temporarily (not "locked" by
17347
+ // SearchModeOnFocus) via me._internal.ClearInputForSearch() or by entering a value
17348
+ // in txtPrimary, the input control "word wraps" to a new line. When it lose focus,
17349
+ // it returns to its normal position if it is empty.
17350
+ // In Visual Selection Mode with SearchModeOnFocus disabled this poses a problem
17351
+ // since interacting with the picker control results in the input control losing
17352
+ // focus.
17353
+ // So, if the user clicks on an item in the picker control while the control
17354
+ // is temporarily in search mode (remember, SearchModeOnFocus is not enabled),
17355
+ // txtPrimary lose focus, the control's height is changed because the input
17356
+ // control returns to its normal position (it no longer "word wraps"), the picker
17357
+ // control moves up, and the mouse button is released on another element than the
17358
+ // one the user initially clicked on.
17359
+ // OnClick does not fire unless the mouse button is clicked and released
17360
+ // on the same element. So in this situation the picker control would not cause
17361
+ // the given item to be selected. The user would have to try again.
17362
+ // To avoid this, we exit search mode to force txtPrimary back into place when
17363
+ // re-opening the DropDown control.
17364
+
17365
+ me._internal.UndoClearInputForSearch(); // Undo/exit search mode to return txtPrimary to its normal position
17267
17366
  }
17268
17367
 
17269
17368
  // Do this before displaying drop down to prevent dropdown with position:absolute
@@ -17446,8 +17545,10 @@ Fit.Controls.DropDown = function(ctlId)
17446
17545
 
17447
17546
  this._internal = (this._internal ? this._internal : {});
17448
17547
 
17449
- this._internal.ClearInputForSearch = function()
17548
+ this._internal.ClearInputForSearch = function(keepDropDownOpen) // Put input in search mode
17450
17549
  {
17550
+ Fit.Validation.ExpectBoolean(keepDropDownOpen, true);
17551
+
17451
17552
  if (me.TextSelectionMode() === true)
17452
17553
  {
17453
17554
  forceFocusInput(txtPrimary);
@@ -17471,10 +17572,13 @@ Fit.Controls.DropDown = function(ctlId)
17471
17572
  // event to register interactions, which doesn't fire unless the mouse button is released
17472
17573
  // on the same object it was pressed down on. We avoid this by closing the control.
17473
17574
  // We close it in both Text Selection Mode and Visual Selection Mode for consistency.
17474
- me.CloseDropDown();
17575
+ if (keepDropDownOpen !== true)
17576
+ {
17577
+ me.CloseDropDown();
17578
+ }
17475
17579
  }
17476
17580
 
17477
- this._internal.UndoClearInputForSearch = function()
17581
+ this._internal.UndoClearInputForSearch = function() // Undo/exit search mode for input field
17478
17582
  {
17479
17583
  if (me.TextSelectionMode() === true)
17480
17584
  {
@@ -17947,12 +18051,45 @@ Fit.Controls.DropDown = function(ctlId)
17947
18051
  focusInputOnMobile = orgFocusInputOnMobile;
17948
18052
  }
17949
18053
 
17950
- function setInputEditing(input, val, keepStateOnParent)
18054
+ /// <function container="Fit.Controls.DropDown" name="SearchModeOnFocus" access="public" returns="boolean">
18055
+ /// <description>
18056
+ /// Clear input and display "Search.." placeholder when control receives focus.
18057
+ /// If SearchModeOnFocus is enabled, InputEnabled will also be enabled. Disabling
18058
+ /// SearchModeOnFocus does not disable InputEnabled.
18059
+ /// </description>
18060
+ /// <param name="val" type="boolean" default="undefined"> If defined, True enables search mode on focus, False disables it </param>
18061
+ /// </function>
18062
+ this.SearchModeOnFocus = function(val)
18063
+ {
18064
+ Fit.Validation.ExpectBoolean(val, true);
18065
+
18066
+ if (Fit.Validation.IsSet(val) === true)
18067
+ {
18068
+ searchModeOnFocus = val;
18069
+
18070
+ if (me.InputEnabled() === false && val === true)
18071
+ {
18072
+ me.InputEnabled(true);
18073
+ }
18074
+ }
18075
+
18076
+ return searchModeOnFocus;
18077
+ }
18078
+
18079
+ function setInputEditing(input, val, keepStateOnParent) // Input being edited "word wraps" to separate line
17951
18080
  {
17952
18081
  Fit.Validation.ExpectInstance(input, HTMLInputElement);
17953
18082
  Fit.Validation.ExpectBoolean(val);
17954
18083
  Fit.Validation.ExpectBoolean(keepStateOnParent, true);
17955
18084
 
18085
+ // Do not change editing state for txtPrimary when SearchModeOnFocus is enabled.
18086
+ // In this case txtPrimary remains locked in editing mode so it remains visible,
18087
+ // even when changing focus within the control and when adding or removing items.
18088
+ if (searchModeOnFocus === true && input === txtPrimary)
18089
+ {
18090
+ return;
18091
+ }
18092
+
17956
18093
  if (keepStateOnParent !== true)
17957
18094
  {
17958
18095
  Fit.Dom.Data(input.parentElement, "editing", val === true ? "true" : null);
@@ -19432,7 +19569,7 @@ Fit.Controls.WSDropDown = function(ctlId)
19432
19569
  }
19433
19570
  else if (item.Value === "ShowAll")
19434
19571
  {
19435
- me._internal.UndoClearInputForSearch(); // In case user first picked SearchMore, changed their mind, and then selected ShowAll
19572
+ me._internal.UndoClearInputForSearch(); // In case user first picked SearchMore, entered a search value, changed their mind, and then selected ShowAll
19436
19573
 
19437
19574
  useActionMenuAfterLoad = false;
19438
19575
 
@@ -19850,6 +19987,19 @@ Fit.Controls.WSDropDown = function(ctlId)
19850
19987
  return me.InputEnabled(val);
19851
19988
  }
19852
19989
 
19990
+ this.SearchModeOnFocus = Fit.Core.CreateOverride(this.SearchModeOnFocus, function(val)
19991
+ {
19992
+ Fit.Validation.ExpectBoolean(val, true);
19993
+
19994
+ if (Fit.Validation.IsSet(val) === true && val !== base())
19995
+ {
19996
+ base(val);
19997
+ updateActionMenu(); // Add/remove search option depending on whether SearchModeOnFocus is enabled or not
19998
+ }
19999
+
20000
+ return base();
20001
+ });
20002
+
19853
20003
  this.Placeholder = function(val)
19854
20004
  {
19855
20005
  Fit.Validation.ExpectString(val, true);
@@ -20170,7 +20320,7 @@ Fit.Controls.WSDropDown = function(ctlId)
20170
20320
 
20171
20321
  actionMenu.RemoveItems();
20172
20322
 
20173
- if (me.InputEnabled() === true)
20323
+ if (me.InputEnabled() === true && me.SearchModeOnFocus() === false)
20174
20324
  {
20175
20325
  actionMenu.AddItem(searchIcon + translations.SearchMore, "SearchMore");
20176
20326
  }
@@ -20183,7 +20333,7 @@ Fit.Controls.WSDropDown = function(ctlId)
20183
20333
  }
20184
20334
  else //if (nodesPopulated === true && tree.GetChildren().length === 0)
20185
20335
  {
20186
- actionMenu.AddItem(showAllIcon + "<i>" + translations.NoneAvailable + ": " + translations.ShowAllOptions + "</i>", "ShowAllNoneFound");
20336
+ actionMenu.AddItem(showAllIcon + "<i>" + translations.NoneAvailable + "</i>", "ShowAllNoneFound");
20187
20337
  }
20188
20338
  }
20189
20339
 
@@ -20301,7 +20451,7 @@ Fit.Controls.WSDropDown = function(ctlId)
20301
20451
  ShowAllOptions : "Show available options",
20302
20452
  RemoveAll : "Remove all selected",
20303
20453
  Remove : "Remove",
20304
- NoneAvailable : "The list is empty"
20454
+ NoneAvailable : "List with options is empty"
20305
20455
  }
20306
20456
  },
20307
20457
  "da":
@@ -20316,7 +20466,7 @@ Fit.Controls.WSDropDown = function(ctlId)
20316
20466
  ShowAllOptions : "Vis tilgængelige valgmuligheder",
20317
20467
  RemoveAll : "Fjern alle valgte",
20318
20468
  Remove : "Fjern",
20319
- NoneAvailable : "Listen er tom"
20469
+ NoneAvailable : "Liste med valgmuligheder er tom"
20320
20470
  }
20321
20471
  },
20322
20472
  "de":
@@ -20331,7 +20481,7 @@ Fit.Controls.WSDropDown = function(ctlId)
20331
20481
  ShowAllOptions : "Verfügbare Optionen anzeigen",
20332
20482
  RemoveAll : "Alle ausgewählten entfernen",
20333
20483
  Remove : "Entfernen",
20334
- NoneAvailable : "Die Liste ist leer"
20484
+ NoneAvailable : "Liste der Optionen ist leer"
20335
20485
  }
20336
20486
  }
20337
20487
  }
@@ -21455,12 +21605,17 @@ Fit.Controls.Input = function(ctlId)
21455
21605
  Fit.Core.Extend(this, Fit.Controls.ControlBase).Apply(ctlId);
21456
21606
 
21457
21607
  var me = this;
21458
- var orgVal = "";
21459
- var preVal = "";
21608
+ var orgVal = ""; // Holds initial value used to determine IsDirty state
21609
+ var preVal = ""; // Holds latest change made by user - used to determine whether OnChange needs to be fired
21460
21610
  var input = null;
21461
21611
  var cmdResize = null;
21462
21612
  var designEditor = null;
21463
- var htmlWrappedInParagraph = false;
21613
+ var designEditorDirty = false;
21614
+ var designEditorDirtyPending = false;
21615
+ var designEditorConfig = null;
21616
+ var designEditorRestoreButtonState = null;
21617
+ var designEditorSuppressPaste = false;
21618
+ //var htmlWrappedInParagraph = false;
21464
21619
  var wasAutoChangedToMultiLineMode = false; // Used to revert to single line if multi line was automatically enabled along with DesignMode(true), Maximizable(true), or Resizable(true)
21465
21620
  var minimizeHeight = -1;
21466
21621
  var maximizeHeight = -1;
@@ -21553,6 +21708,21 @@ Fit.Controls.Input = function(ctlId)
21553
21708
  }
21554
21709
 
21555
21710
  fireOnChange(); // Only fires OnChange if value has actually changed
21711
+
21712
+ // Restore editor's toolbar buttons in case they were temporarily disabled
21713
+
21714
+ if (designEditor !== null)
21715
+ {
21716
+ restoreDesignEditorButtons();
21717
+ }
21718
+ });
21719
+
21720
+ Fit.Events.AddHandler(me.GetDomElement(), "paste", true, function(e)
21721
+ {
21722
+ if (designEditor !== null && designEditorSuppressPaste === true)
21723
+ {
21724
+ Fit.Events.Stop(e);
21725
+ }
21556
21726
  });
21557
21727
 
21558
21728
  try
@@ -21698,20 +21868,34 @@ Fit.Controls.Input = function(ctlId)
21698
21868
 
21699
21869
  if (Fit.Validation.IsSet(val) === true)
21700
21870
  {
21701
- var fireOnChange = (designEditor === null && me.Value() !== val); // DesignEditor invokes input.onchange() if value is changed
21871
+ var fireOnChange = (me.Value() !== val);
21702
21872
 
21703
21873
  orgVal = (preserveDirtyState !== true ? val : orgVal);
21704
21874
  preVal = val;
21875
+ designEditorDirty = designEditorDirtyPending === true ? true : false;
21876
+ designEditorDirtyPending = false;
21705
21877
 
21706
- if (val.indexOf("<p>") === 0)
21707
- htmlWrappedInParagraph = true; // Indicates that val is comparable with value from CKEditor which wraps content in paragraphs
21878
+ /*if (val.indexOf("<p>") === 0)
21879
+ htmlWrappedInParagraph = true; // Indicates that val is comparable with value from CKEditor which wraps content in paragraphs*/
21708
21880
 
21709
21881
  if (designEditor !== null)
21710
- CKEDITOR.instances[me.GetId() + "_DesignMode"].setData(val);
21882
+ {
21883
+ // NOTICE: Invalid HTML is removed, so an all invalid HTML string will be discarded
21884
+ // by the editor, resulting in the editor's getData() function returning an empty string.
21885
+
21886
+ // Calling setData(..) fires CKEditor's onchange event which in turn fires
21887
+ // Input's OnChange event. Suppress OnChange which is fired further down.
21888
+ me._internal.ExecuteWithNoOnChange(function()
21889
+ {
21890
+ CKEDITOR.instances[me.GetId() + "_DesignMode"].setData(val);
21891
+ });
21892
+ }
21711
21893
  else
21894
+ {
21712
21895
  input.value = val;
21896
+ }
21713
21897
 
21714
- if (Fit._internal.Controls.Input.BlobManager.RevokeExternalBlobUrlsOnDispose === true)
21898
+ if (designEditorConfig !== null && designEditorConfig.Plugins && designEditorConfig.Plugins.Images && designEditorConfig.Plugins.Images.RevokeExternalBlobUrlsOnDispose === true)
21715
21899
  {
21716
21900
  // Keep track of image blobs added via Value(..) so we can dispose them automatically.
21717
21901
  // When RevokeExternalBlobUrlsOnDispose is True it basically means that the Input control
@@ -21736,14 +21920,69 @@ Fit.Controls.Input = function(ctlId)
21736
21920
  }
21737
21921
 
21738
21922
  if (designEditor !== null)
21739
- return CKEDITOR.instances[me.GetId() + "_DesignMode"].getData();
21923
+ {
21924
+ // If user has not changed value, then return the value initially set.
21925
+ // CKEditor may change (optimize) HTML when applied, but we always want
21926
+ // the value initially set when no changes have been made by the user.
21927
+ // See additional comments regarding this in the IsDirty() implementation.
21928
+ if (designEditorDirty === false)
21929
+ {
21930
+ return orgVal;
21931
+ }
21932
+
21933
+ var curVal = CKEDITOR.instances[me.GetId() + "_DesignMode"].getData();
21934
+
21935
+ // Remove extra line break added by htmlwriter plugin at the end: <p>Hello world</p>\n
21936
+ curVal = curVal.replace(/<\/p>\n$/, "</p>");
21937
+
21938
+ // Remove empty class attribute on <img> tags which may be temporarily set when selecting
21939
+ // an image using the dragresize plugin. This plugin adds a CSS class (ckimgrsz) to the image
21940
+ // tag while being selected, although the class name is removed when calling getData() above.
21941
+ // However, the empty class attribute is useless, so we remove it. It also results in IsDirty()
21942
+ // returning True while the image is selected if we keep it. Actually the class attribute should
21943
+ // never have been returned since the allowedContent option does not allow it - might be a minor bug.
21944
+ curVal = curVal.replace(/(<img.*?) class=""(.*?>)/, "$1$2"); // Not using /g switch as only one image can be selected
21945
+
21946
+ return curVal;
21947
+ }
21740
21948
 
21741
21949
  return input.value;
21742
21950
  }
21743
21951
 
21952
+ // See documentation on ControlBase
21953
+ this.UserValue = Fit.Core.CreateOverride(this.UserValue, function(val)
21954
+ {
21955
+ if (Fit.Validation.IsSet(val) === true && designEditor !== null)
21956
+ {
21957
+ designEditorDirtyPending = true;
21958
+ }
21959
+
21960
+ return base(val);
21961
+ });
21962
+
21744
21963
  // See documentation on ControlBase
21745
21964
  this.IsDirty = function()
21746
21965
  {
21966
+ if (designEditor !== null)
21967
+ {
21968
+ // Never do value comparison in DesignMode.
21969
+ // A value such as "Hello world" could have been provided,
21970
+ // which by CKEditor would be returned as "<p>Hello world</p>".
21971
+ // A value such as '<p style="text-align: center;">Hello</p>' could
21972
+ // also have been set, which by CKEditor would be optimized to
21973
+ // '<p style="text-align:center">Hello</p>' via ACF (Advanced Content Filter):
21974
+ // https://ckeditor.com/docs/ckeditor4/latest/guide/dev_advanced_content_filter.html
21975
+ // Furthermore invalid HTML is removed while valid HTML is kept.
21976
+ // All this makes it very difficult to reliably determine dirty state
21977
+ // by comparing values. Therefore, if the user changed anything by interacting
21978
+ // with the editor, or UserValue(..) was called, always consider the value dirty.
21979
+
21980
+ // Another positive of avoiding value comparison to determine dirty state
21981
+ // is that retrieving the value from CKEditor is fairly expensive.
21982
+
21983
+ return designEditorDirty;
21984
+ }
21985
+
21747
21986
  return (orgVal !== me.Value());
21748
21987
  }
21749
21988
 
@@ -21758,7 +21997,7 @@ Fit.Controls.Input = function(ctlId)
21758
21997
  {
21759
21998
  // This will destroy control - it will no longer work!
21760
21999
 
21761
- var curVal = Fit._internal.Controls.Input.BlobManager.RevokeBlobUrlsOnDispose === "UnreferencedOnly" ? me.Value() : null;
22000
+ var curVal = designEditorConfig !== null && designEditorConfig.Plugins && designEditorConfig.Plugins.Images && designEditorConfig.Plugins.Images.RevokeBlobUrlsOnDispose === "UnreferencedOnly" ? me.Value() : null;
21762
22001
 
21763
22002
  if (Fit._internal.Controls.Input.ActiveEditorForDialog === me)
21764
22003
  {
@@ -21826,7 +22065,7 @@ Fit.Controls.Input = function(ctlId)
21826
22065
  debouncedOnChange.Cancel();
21827
22066
  }
21828
22067
 
21829
- if (Fit._internal.Controls.Input.BlobManager.RevokeBlobUrlsOnDispose === "All")
22068
+ if (designEditorConfig === null || !designEditorConfig.Plugins || !designEditorConfig.Plugins.Images || !designEditorConfig.Plugins.Images.RevokeBlobUrlsOnDispose || designEditorConfig.Plugins.Images.RevokeBlobUrlsOnDispose === "All")
21830
22069
  {
21831
22070
  Fit.Array.ForEach(imageBlobUrls, function(imageUrl)
21832
22071
  {
@@ -21844,7 +22083,7 @@ Fit.Controls.Input = function(ctlId)
21844
22083
  });
21845
22084
  }
21846
22085
 
21847
- me = orgVal = preVal = input = cmdResize = designEditor = htmlWrappedInParagraph = wasAutoChangedToMultiLineMode = minimizeHeight = maximizeHeight = minMaxUnit = resizable = nativeResizableAvailable = mutationObserverId = rootedEventId = createWhenReadyIntervalId = isIe8 = debounceOnChangeTimeout = debouncedOnChange = imageBlobUrls = null;
22086
+ me = orgVal = preVal = input = cmdResize = designEditor = designEditorDirty = designEditorDirtyPending = designEditorConfig = designEditorRestoreButtonState = designEditorSuppressPaste /*= htmlWrappedInParagraph*/ = wasAutoChangedToMultiLineMode = minimizeHeight = maximizeHeight = minMaxUnit = resizable = nativeResizableAvailable = mutationObserverId = rootedEventId = createWhenReadyIntervalId = isIe8 = debounceOnChangeTimeout = debouncedOnChange = imageBlobUrls = null;
21848
22087
 
21849
22088
  base();
21850
22089
  });
@@ -22305,16 +22544,220 @@ Fit.Controls.Input = function(ctlId)
22305
22544
  return (cmdResize !== null && Fit.Dom.HasClass(cmdResize, "fa-chevron-up") === true);
22306
22545
  }
22307
22546
 
22547
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfigPluginsImagesConfig">
22548
+ /// <description> Configuration for image plugins </description>
22549
+ /// <member name="Enabled" type="boolean"> Flag indicating whether to enable image plugins or not (defaults to False) </member>
22550
+ /// <member name="EmbedType" type="'base64' | 'blob'" default="undefined">
22551
+ /// How to store and embed images. Base64 is persistent while blob (default) is temporary
22552
+ /// and must be extracted from memory and uploaded/stored to be permanantly persisted.
22553
+ /// References to blobs can be parsed from the HTML value produced by the editor.
22554
+ /// </member>
22555
+ /// <member name="RevokeBlobUrlsOnDispose" type="'All' | 'UnreferencedOnly'" default="undefined">
22556
+ /// This option is in effect when EmbedType is blob.
22557
+ /// Dispose images from blob storage (revoke blob URLs) added though image plugins when control is disposed.
22558
+ /// If "UnreferencedOnly" is specified, the component using Fit.UI's input control will be responsible for
22559
+ /// disposing referenced blobs. Failing to do so may cause a memory leak. Defaults to All.
22560
+ /// </member>
22561
+ /// <member name="RevokeExternalBlobUrlsOnDispose" type="boolean" default="undefined">
22562
+ /// This option is in effect when EmbedType is blob.
22563
+ /// Dispose images from blob storage (revoke blob URLs) added through Value(..)
22564
+ /// function when control is disposed. Basically ownership of these blobs are handed
22565
+ /// over to the control for the duration of its life time.
22566
+ /// These images are furthermore subject to the rule set in RevokeBlobUrlsOnDispose.
22567
+ /// Defaults to False.
22568
+ /// </member>
22569
+ /// </container>
22570
+
22571
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfigPlugins">
22572
+ /// <description> Additional plugins enabled in DesignMode </description>
22573
+ /// <member name="Emojis" type="boolean" default="undefined"> Plugin(s) related to emoji support (defaults to False) </member>
22574
+ /// <member name="Images" type="Fit.Controls.InputTypeDefs.DesignModeConfigPluginsImagesConfig" default="undefined"> Plugin(s) related to support for images (defaults to False) </member>
22575
+ /// </container>
22576
+
22577
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfigToolbar">
22578
+ /// <description> Toolbar buttons enabled in DesignMode </description>
22579
+ /// <member name="Formatting" type="boolean" default="undefined"> Enable text formatting (bold, italic, underline) (defaults to True) </member>
22580
+ /// <member name="Justify" type="boolean" default="undefined"> Enable text alignment (defaults to True) </member>
22581
+ /// <member name="Lists" type="boolean" default="undefined"> Enable ordered and unordered lists with indentation (defaults to True) </member>
22582
+ /// <member name="Links" type="boolean" default="undefined"> Enable links (defaults to True) </member>
22583
+ /// <member name="Emojis" type="boolean" default="undefined"> Enable emoji button (defaults to False) </member>
22584
+ /// <member name="Images" type="boolean" default="undefined"> Enable image button (defaults to false) </member>
22585
+ /// </container>
22586
+
22587
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfigInfoPanel">
22588
+ /// <description> Information panel at the bottom of the editor </description>
22589
+ /// <member name="Text" type="string" default="undefined"> Text to display </member>
22590
+ /// <member name="Alignment" type="'Left' | 'Center' | 'Right'" default="undefined"> Text alignment - defaults to Center </member>
22591
+ /// </container>
22592
+
22593
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeTagsOnRequestEventHandlerArgs">
22594
+ /// <description> Request handler event arguments </description>
22595
+ /// <member name="Sender" type="Fit.Controls.Input"> Instance of control </member>
22596
+ /// <member name="Request" type="Fit.Http.JsonRequest | Fit.Http.JsonpRequest"> Instance of JsonRequest or JsonpRequest </member>
22597
+ /// <member name="Query" type="{ Marker: string, Query: string }"> Query information </member>
22598
+ /// </container>
22599
+ /// <function container="Fit.Controls.InputTypeDefs" name="DesignModeTagsOnRequest" returns="boolean | void">
22600
+ /// <description> Cancelable request event handler </description>
22601
+ /// <param name="sender" type="Fit.Controls.Input"> Instance of control </param>
22602
+ /// <param name="eventArgs" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnRequestEventHandlerArgs"> Event arguments </param>
22603
+ /// </function>
22604
+
22605
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponseJsonTag">
22606
+ /// <description> JSON object representing tag </description>
22607
+ /// <member name="Value" type="string"> Unique value </member>
22608
+ /// <member name="Title" type="string"> Title </member>
22609
+ /// <member name="Icon" type="string" default="undefined"> Optional URL to icon/image </member>
22610
+ /// <member name="Url" type="string" default="undefined"> Optional URL to associate with tag </member>
22611
+ /// <member name="Data" type="string" default="undefined"> Optional data to associate with tag </member>
22612
+ /// </container>
22613
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponseEventHandlerArgs">
22614
+ /// <description> Response handler event arguments </description>
22615
+ /// <member name="Sender" type="Fit.Controls.Input"> Instance of control </member>
22616
+ /// <member name="Request" type="Fit.Http.JsonRequest | Fit.Http.JsonpRequest"> Instance of JsonRequest or JsonpRequest </member>
22617
+ /// <member name="Query" type="{ Marker: string, Query: string }"> Query information </member>
22618
+ /// <member name="Tags" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponseJsonTag[]"> Tags received from WebService </member>
22619
+ /// </container>
22620
+ /// <function container="Fit.Controls.InputTypeDefs" name="DesignModeTagsOnResponse">
22621
+ /// <description> Response event handler </description>
22622
+ /// <param name="sender" type="Fit.Controls.Input"> Instance of control </param>
22623
+ /// <param name="eventArgs" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponseEventHandlerArgs"> Event arguments </param>
22624
+ /// </function>
22625
+
22626
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeTagsTagCreatorReturnType">
22627
+ /// <description> JSON object representing tag to be inserted into editor </description>
22628
+ /// <member name="Title" type="string"> Tag title </member>
22629
+ /// <member name="Value" type="string"> Tag value (ID) </member>
22630
+ /// <member name="Type" type="string"> Tag type (marker) </member>
22631
+ /// <member name="Url" type="string" default="undefined"> Optional tag URL </member>
22632
+ /// <member name="Data" type="string" default="undefined"> Optional tag data </member>
22633
+ /// </container>
22634
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeTagsTagCreatorCallbackArgs">
22635
+ /// <description> TagCreator event arguments </description>
22636
+ /// <member name="Sender" type="Fit.Controls.Input"> Instance of control </member>
22637
+ /// <member name="QueryMarker" type="string"> Query marker </member>
22638
+ /// <member name="Tag" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponseJsonTag"> Tag received from WebService </member>
22639
+ /// </container>
22640
+ /// <function container="Fit.Controls.InputTypeDefs" name="DesignModeTagsTagCreator" returns="Fit.Controls.InputTypeDefs.DesignModeTagsTagCreatorReturnType | null | void">
22641
+ /// <description>
22642
+ /// Function producing JSON object representing tag to be inserted into editor.
22643
+ /// Returning nothing or Null results in default tag being inserted into editor.
22644
+ /// </description>
22645
+ /// <param name="sender" type="Fit.Controls.Input"> Instance of control </param>
22646
+ /// <param name="eventArgs" type="Fit.Controls.InputTypeDefs.DesignModeTagsTagCreatorCallbackArgs"> Event arguments </param>
22647
+ /// </function>
22648
+
22649
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfigTags">
22650
+ /// <description> Configuration for tags in DesignMode </description>
22651
+ /// <member name="Triggers" type="{ Marker: string, MinimumCharacters?: integer, DebounceQuery?: integer }[]"> Markers triggering tags request and context menu </member>
22652
+ /// <member name="QueryUrl" type="string">
22653
+ /// URL to request data from. Endpoint receives the following payload:
22654
+ /// { Marker: "@", Query: "search" }
22655
+ ///
22656
+ /// Data is expected to be returned in the following format:
22657
+ /// [
22658
+ /// { Value: "t-1", Title: "Tag 1", Icon: "images/img1.jpeg", Url: "show/1", Data: "..." },
22659
+ /// { Value: "t-2", Title: "Tag 2", Icon: "images/img2.jpeg", Url: "show/2", Data: "..." }, ...
22660
+ /// ]
22661
+ ///
22662
+ /// The Value and Title properties are required. The Icon property is optional and must specify the path to an image.
22663
+ /// The Url property is optional and must specify a path to a related page/resource.
22664
+ /// The Data property is optional and allows for additional data to be associated with the tag.
22665
+ /// To hold multiple values, consider using a base64 encoded JSON object:
22666
+ /// btoa(JSON.stringify({ creationDate: new Date(), active: true }))
22667
+ ///
22668
+ /// The data eventuelly results in a tag being added to the editor with the following format:
22669
+ /// <a data-tag-type="@" data-tag-id="unique id 1" data-tag-data="..." href="show/1">Tag name 1</a>
22670
+ /// The data-tag-data attribute is only declared if the corresponding Data property is defined in data.
22671
+ /// </member>
22672
+ /// <member name="JsonpCallback" type="string" default="undefined"> Name of URL parameter receiving name of JSONP callback function (only for JSONP services) </member>
22673
+ /// <member name="JsonpTimeout" type="integer" default="undefined"> Number of milliseconds to allow JSONP request to wait for a response before aborting (only for JSONP services) </member>
22674
+ /// <member name="OnRequest" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnRequest" default="undefined">
22675
+ /// Event handler invoked when tags are requested. Request may be canceled by returning False.
22676
+ /// Function receives two arguments:
22677
+ /// Sender (Fit.Controls.Input) and EventArgs object.
22678
+ /// EventArgs object contains the following properties:
22679
+ /// - Sender: Fit.Controls.Input instance
22680
+ /// - Request: Fit.Http.JsonpRequest or Fit.Http.JsonRequest instance
22681
+ /// - Query: Contains query information in its Marker and Query property
22682
+ /// </member>
22683
+ /// <member name="OnResponse" type="Fit.Controls.InputTypeDefs.DesignModeTagsOnResponse" default="undefined">
22684
+ /// Event handler invoked when tags data is received, allowing for data transformation.
22685
+ /// Function receives two arguments:
22686
+ /// Sender (Fit.Controls.Input) and EventArgs object.
22687
+ /// EventArgs object contains the following properties:
22688
+ /// - Sender: Fit.Controls.Input instance
22689
+ /// - Request: Fit.Http.JsonpRequest or Fit.Http.JsonRequest instance
22690
+ /// - Query: Contains query information in its Marker and Query property
22691
+ /// - Tags: JSON tags array received from WebService
22692
+ /// </member>
22693
+ /// <member name="TagCreator" type="Fit.Controls.InputTypeDefs.DesignModeTagsTagCreator" default="undefined">
22694
+ /// Callback invoked when a tag is being inserted into editor, allowing
22695
+ /// for customization to the title and attributes associated with the tag.
22696
+ /// Function receives two arguments:
22697
+ /// Sender (Fit.Controls.Input) and EventArgs object.
22698
+ /// EventArgs object contains the following properties:
22699
+ /// - Sender: Fit.Controls.Input instance
22700
+ /// - QueryMarker: String containing query marker
22701
+ /// - Tag: JSON tag received from WebService
22702
+ /// </member>
22703
+ /// </container>
22704
+
22705
+ /// <container name="Fit.Controls.InputTypeDefs.DesignModeConfig">
22706
+ /// <description> Configuration for DesignMode </description>
22707
+ /// <member name="Plugins" type="Fit.Controls.InputTypeDefs.DesignModeConfigPlugins" default="undefined"> Plugins configuration </member>
22708
+ /// <member name="Toolbar" type="Fit.Controls.InputTypeDefs.DesignModeConfigToolbar" default="undefined"> Toolbar configuration </member>
22709
+ /// <member name="InfoPanel" type="Fit.Controls.InputTypeDefs.DesignModeConfigInfoPanel" default="undefined"> Information panel configuration </member>
22710
+ /// <member name="Tags" type="Fit.Controls.InputTypeDefs.DesignModeConfigTags" default="undefined"> Tags configuration </member>
22711
+ /// </container>
22712
+
22308
22713
  /// <function container="Fit.Controls.Input" name="DesignMode" access="public" returns="boolean">
22309
22714
  /// <description>
22310
22715
  /// Get/set value indicating whether control is in Design Mode allowing for rich HTML content.
22311
22716
  /// Notice that this control type requires dimensions (Width/Height) to be specified in pixels.
22312
22717
  /// </description>
22313
22718
  /// <param name="val" type="boolean" default="undefined"> If defined, True enables Design Mode, False disables it </param>
22719
+ /// <param name="editorConfig" type="Fit.Controls.InputTypeDefs.DesignModeConfig" default="undefined">
22720
+ /// If provided and DesignMode is enabled, configuration is applied when editor is created.
22721
+ /// </param>
22314
22722
  /// </function>
22315
- this.DesignMode = function(val)
22723
+ this.DesignMode = function(val, editorConfig)
22316
22724
  {
22317
22725
  Fit.Validation.ExpectBoolean(val, true);
22726
+ Fit.Validation.ExpectObject(editorConfig, true);
22727
+ Fit.Validation.ExpectObject((editorConfig || {}).Plugins, true);
22728
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Plugins || {}).Emojis, true);
22729
+ Fit.Validation.ExpectObject(((editorConfig || {}).Plugins || {}).Images, true);
22730
+ Fit.Validation.ExpectBoolean((((editorConfig || {}).Plugins || {}).Images || {}).Enabled, true);
22731
+ Fit.Validation.ExpectStringValue((((editorConfig || {}).Plugins || {}).Images || {}).EmbedType, true);
22732
+ Fit.Validation.ExpectStringValue((((editorConfig || {}).Plugins || {}).Images || {}).RevokeBlobUrlsOnDispose, true);
22733
+ Fit.Validation.ExpectBoolean((((editorConfig || {}).Plugins || {}).Images || {}).RevokeExternalBlobUrlsOnDispose, true);
22734
+ Fit.Validation.ExpectObject((editorConfig || {}).Toolbar, true);
22735
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Formatting, true);
22736
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Justify, true);
22737
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Lists, true);
22738
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Links, true);
22739
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Emojis, true);
22740
+ Fit.Validation.ExpectBoolean(((editorConfig || {}).Toolbar || {}).Images, true);
22741
+ Fit.Validation.ExpectObject((editorConfig || {}).InfoPanel, true);
22742
+ Fit.Validation.ExpectString(((editorConfig || {}).InfoPanel || {}).Text, true);
22743
+ Fit.Validation.ExpectString(((editorConfig || {}).InfoPanel || {}).Alignment, true);
22744
+ Fit.Validation.ExpectObject((editorConfig || {}).Tags, true);
22745
+
22746
+ if (editorConfig && editorConfig.Tags)
22747
+ {
22748
+ Fit.Validation.ExpectTypeArray(editorConfig.Tags.Triggers, function(trigger)
22749
+ {
22750
+ Fit.Validation.ExpectStringValue(trigger.Marker);
22751
+ Fit.Validation.ExpectInteger(trigger.MinimumCharacters, true);
22752
+ Fit.Validation.ExpectInteger(trigger.DebounceQuery, true);
22753
+ });
22754
+ Fit.Validation.ExpectStringValue(editorConfig.Tags.QueryUrl);
22755
+ Fit.Validation.ExpectStringValue(editorConfig.Tags.JsonpCallback, true);
22756
+ Fit.Validation.ExpectInteger(editorConfig.Tags.JsonpTimeout, true);
22757
+ Fit.Validation.ExpectFunction(editorConfig.Tags.OnRequest, true);
22758
+ Fit.Validation.ExpectFunction(editorConfig.Tags.OnResponse, true);
22759
+ Fit.Validation.ExpectFunction(editorConfig.Tags.TagCreator, true);
22760
+ }
22318
22761
 
22319
22762
  if (Fit.Validation.IsSet(val) === true)
22320
22763
  {
@@ -22325,6 +22768,11 @@ Fit.Controls.Input = function(ctlId)
22325
22768
 
22326
22769
  if (val === true && designMode === false)
22327
22770
  {
22771
+ if (Fit.Validation.IsSet(editorConfig) === true)
22772
+ {
22773
+ designEditorConfig = editorConfig;
22774
+ }
22775
+
22328
22776
  if (Fit._internal.Controls.Input.ActiveEditorForDialog === me)
22329
22777
  {
22330
22778
  // Control is actually already in Design Mode, but waiting
@@ -22356,6 +22804,16 @@ Fit.Controls.Input = function(ctlId)
22356
22804
  CKEDITOR.config.skin = Fit._internal.Controls.Input.Editor.Skin;
22357
22805
  }
22358
22806
 
22807
+ CKEDITOR.on("instanceReady", function(ev)
22808
+ {
22809
+ // Do not produce XHTML self-closing tags such as <br /> and <img src="img.jpg" />
22810
+ // https://ckeditor.com/docs/ckeditor4/latest/features/output_format.html
22811
+ // NOTICE: The htmlwriter plugin is required for this to work!
22812
+ // Output produced is now both HTML4 and HTML5 compatible, but is not valid
22813
+ // XHTML anymore! Self-closing tags are allowed in HTML5 but not valid in HTML4.
22814
+ ev.editor.dataProcessor.writer.selfClosingEnd = ">"; // Defaults to ' />'
22815
+ });
22816
+
22359
22817
  // Register OnShow and OnHide event handlers when a dialog is opened for the first time.
22360
22818
  // IMPORTANT: These event handlers are shared by all input control instances in Design Mode,
22361
22819
  // so we cannot use 'me' to access the current control for which a dialog is opened.
@@ -22429,23 +22887,26 @@ Fit.Controls.Input = function(ctlId)
22429
22887
  return;
22430
22888
  }
22431
22889
 
22432
- // Move dialog to control - otherwise placed in the root of the document where it pollutes,
22433
- // and makes it impossible to interact with the dialog in light dismissable panels and callouts.
22434
- // Dialog is placed alongside control and not within the control's container, to prevent Fit.UI
22435
- // styling from affecting the dialog.
22436
- // DISABLED: It breaks file picker controls in dialogs which are hosted in iframes.
22437
- // When an iframe is re-rooted in DOM it reloads, and any dynamically created content is lost.
22438
- // We will have to increase the z-index to make sure dialogs open on top of modal layers.
22439
- // EDIT 2021-08-20: Enabled again. The base64image plugin has now been altered so it no longer
22440
- // uses CKEditor's built-in file picker which is wrapped in an iFrame. Therefore the dialog can
22441
- // once again be mounted next to the Input control.
22442
-
22443
- var ckeDialogElement = this.getElement().$;
22444
- Fit.Dom.InsertAfter(Fit._internal.Controls.Input.ActiveEditorForDialog.GetDomElement(), ckeDialogElement);
22445
-
22446
- // 2nd+ time dialog is opened it remains invisible - make it appear and position it
22447
- ckeDialogElement.style.display = !CKEDITOR.env.ie || CKEDITOR.env.edge ? "flex" : ""; // https://github.com/ckeditor/ckeditor4/blob/8b208d05d1338d046cdc8f971c9faf21604dd75d/plugins/dialog/plugin.js#L152
22448
- this.layout(); // 'this' is the dialog instance - layout() positions dialog
22890
+ if (Fit._internal.ControlBase.ReduceDocumentRootPollution === true)
22891
+ {
22892
+ // Move dialog to control - otherwise placed in the root of the document where it pollutes,
22893
+ // and makes it impossible to interact with the dialog in light dismissable panels and callouts.
22894
+ // Dialog is placed alongside control and not within the control's container, to prevent Fit.UI
22895
+ // styling from affecting the dialog.
22896
+ // DISABLED: It breaks file picker controls in dialogs which are hosted in iframes.
22897
+ // When an iframe is re-rooted in DOM it reloads, and any dynamically created content is lost.
22898
+ // We will have to increase the z-index to make sure dialogs open on top of modal layers.
22899
+ // EDIT 2021-08-20: Enabled again. The base64image plugin has now been altered so it no longer
22900
+ // uses CKEditor's built-in file picker which is wrapped in an iFrame. Therefore the dialog can
22901
+ // once again be mounted next to the Input control.
22902
+
22903
+ var ckeDialogElement = this.getElement().$;
22904
+ Fit.Dom.InsertAfter(Fit._internal.Controls.Input.ActiveEditorForDialog.GetDomElement(), ckeDialogElement);
22905
+
22906
+ // 2nd+ time dialog is opened it remains invisible - make it appear and position it
22907
+ ckeDialogElement.style.display = !CKEDITOR.env.ie || CKEDITOR.env.edge ? "flex" : ""; // https://github.com/ckeditor/ckeditor4/blob/8b208d05d1338d046cdc8f971c9faf21604dd75d/plugins/dialog/plugin.js#L152
22908
+ this.layout(); // 'this' is the dialog instance - layout() positions dialog
22909
+ }
22449
22910
  });
22450
22911
 
22451
22912
  dialog.on("hide", function(ev) // Fires when user closes dialog, or when hide() is called on dialog, or if destroy() is called on editor instance from Dispose() or DesignMode(false)
@@ -22717,6 +23178,277 @@ Fit.Controls.Input = function(ctlId)
22717
23178
  var langSupport = ["da", "de", "en"];
22718
23179
  var locale = Fit.Internationalization.Locale().length === 2 ? Fit.Internationalization.Locale() : Fit.Internationalization.Locale().substring(0, 2);
22719
23180
  var lang = Fit.Array.Contains(langSupport, locale) === true ? locale : "en";
23181
+ var plugins = [];
23182
+ var toolbar = [];
23183
+ var mentions = [];
23184
+
23185
+ var config = designEditorConfig || {};
23186
+
23187
+ // Enable additional plugins not compiled into CKEditor by default
23188
+
23189
+ if ((config.Plugins && config.Plugins.Emojis === true) || (config.Toolbar && config.Toolbar.Emojis === true))
23190
+ {
23191
+ Fit.Array.Add(plugins, "emoji");
23192
+ }
23193
+
23194
+ if ((config.Plugins && config.Plugins.Images && config.Plugins.Images.Enabled === true) || (config.Toolbar && config.Toolbar.Images === true))
23195
+ {
23196
+ if (config.Toolbar && config.Toolbar.Images === true)
23197
+ {
23198
+ Fit.Array.Add(plugins, "base64image");
23199
+ }
23200
+
23201
+ plugins = Fit.Array.Merge(plugins, ["base64imagepaste", "dragresize"]);
23202
+ }
23203
+
23204
+ // Add toolbar buttons
23205
+
23206
+ if (!config.Toolbar || config.Toolbar.Formatting !== false)
23207
+ {
23208
+ Fit.Array.Add(toolbar,
23209
+ {
23210
+ name: "BasicFormatting",
23211
+ items: [ "Bold", "Italic", "Underline" ]
23212
+ });
23213
+ }
23214
+
23215
+ if (!config.Toolbar || config.Toolbar.Justify !== false)
23216
+ {
23217
+ Fit.Array.Add(toolbar,
23218
+ {
23219
+ name: "Justify",
23220
+ items: [ "JustifyLeft", "JustifyCenter", "JustifyRight" ]
23221
+ });
23222
+ }
23223
+
23224
+ if (!config.Toolbar || config.Toolbar.Lists !== false)
23225
+ {
23226
+ Fit.Array.Add(toolbar,
23227
+ {
23228
+ name: "Lists",
23229
+ items: [ "NumberedList", "BulletedList", "Indent", "Outdent" ]
23230
+ });
23231
+ }
23232
+
23233
+ if (!config.Toolbar || config.Toolbar.Links !== false)
23234
+ {
23235
+ Fit.Array.Add(toolbar,
23236
+ {
23237
+ name: "Links",
23238
+ items: [ "Link", "Unlink" ]
23239
+ });
23240
+ }
23241
+
23242
+ if (config.Toolbar)
23243
+ {
23244
+ var insert = [];
23245
+
23246
+ if (config.Toolbar.Emojis === true)
23247
+ {
23248
+ Fit.Array.Add(insert, "EmojiPanel");
23249
+ }
23250
+
23251
+ if (config.Toolbar.Images === true)
23252
+ {
23253
+ Fit.Array.Add(insert, "base64image");
23254
+ }
23255
+
23256
+ if (insert.length > 0)
23257
+ {
23258
+ Fit.Array.Add(toolbar,
23259
+ {
23260
+ name: "Insert",
23261
+ items: insert
23262
+ });
23263
+ }
23264
+ }
23265
+
23266
+ // Configure tags/mentions plugin
23267
+
23268
+ if (config.Tags)
23269
+ {
23270
+ var requestAwaiting = null;
23271
+
23272
+ var createEventArgs = function(marker, query, request) // EventsArgs for OnRequest and OnResponse
23273
+ {
23274
+ return { Sender: me, Query: { Marker: marker, Query: query }, Request: request };
23275
+ };
23276
+
23277
+ Fit.Array.ForEach(config.Tags.Triggers, function(trigger)
23278
+ {
23279
+ var mention =
23280
+ {
23281
+ marker: trigger.Marker,
23282
+ minChars: trigger.MinimumCharacters || 0,
23283
+ throttle: 0, // Throttling is not debouncing - it merely ensures that no more than 1 request is made every X milliseconds when value is changed (defaults to 200ms) - real debouncing implemented further down, which reduce and cancel network calls as user types - also a work around for https://github.com/ckeditor/ckeditor4/issues/5036
23284
+ feed: function(args, resolve)
23285
+ {
23286
+ // WebService is expected to return tag items in an array like so:
23287
+ // [ { Title: string, Value: string, Icon?: string, Url?: string, Data?: string }, { ... }, ... ]
23288
+
23289
+ var req = null;
23290
+
23291
+ if (config.Tags.JsonpCallback)
23292
+ {
23293
+ req = new Fit.Http.JsonpRequest(config.Tags.QueryUrl, config.Tags.JsonpCallback);
23294
+ config.Tags.JsonpTimeout && req.Timeout(config.Tags.JsonpTimeout);
23295
+ req.SetParameter("Marker", args.marker);
23296
+ req.SetParameter("Query", args.query);
23297
+ }
23298
+ else
23299
+ {
23300
+ req = new Fit.Http.JsonRequest(config.Tags.QueryUrl);
23301
+ req.SetData({ Marker: args.marker, Query: args.query });
23302
+ }
23303
+
23304
+ if (config.Tags.OnRequest)
23305
+ {
23306
+ var eventArgs = createEventArgs(args.marker, args.query, req);
23307
+
23308
+ if (config.Tags.OnRequest(me, eventArgs) === false)
23309
+ {
23310
+ resolve([]);
23311
+ return;
23312
+ }
23313
+
23314
+ if (eventArgs.Request !== req)
23315
+ {
23316
+ // Support for changing request instans to
23317
+ // take control over webservice communication.
23318
+
23319
+ // Restrict to support for Fit.Http.Request or classes derived from this
23320
+ Fit.Validation.ExpectInstance(eventArgs.Request, Fit.Http.Request);
23321
+
23322
+ req = eventArgs.Request;
23323
+ }
23324
+ }
23325
+
23326
+ var processDataAndResolve = function(items)
23327
+ {
23328
+ if (config.Tags.OnResponse) // OnResponse is allowed to manipulate tags
23329
+ {
23330
+ var eventArgs = Fit.Core.Merge(createEventArgs(args.marker, args.query, req), { Tags: items });
23331
+ config.Tags.OnResponse(me, eventArgs);
23332
+
23333
+ items = eventArgs.Tags; // In case OnResponse event handler assigned new collection
23334
+ }
23335
+
23336
+ Fit.Array.ForEach(items, function(item)
23337
+ {
23338
+ // Set properties required by mentions plugin
23339
+ item.id = item.Value;
23340
+ item.name = item.Title;
23341
+ });
23342
+
23343
+ resolve(items);
23344
+
23345
+ if (Fit._internal.ControlBase.ReduceDocumentRootPollution === true)
23346
+ {
23347
+ // Calling resolve(..) above immediately opens the context menu from which
23348
+ // a tag can be selected. However, it is placed in the root of the document
23349
+ // where it pollutes the global scope. Move it next to the Fit.UI control.
23350
+ // We do not mount it within the Fit.UI control as it could cause Fit.UI styles
23351
+ // to take effect on the context menu.
23352
+
23353
+ // Get the autocomplete context menu currently open. There can be only one
23354
+ // such menu open at any time. Each editor can declare multiple autocomplete
23355
+ // context menus since each tag marker is associated with its own context menu.
23356
+ var ctm = document.querySelector("ul.cke_autocomplete_opened");
23357
+ Fit.Dom.InsertAfter(me.GetDomElement(), ctm);
23358
+ }
23359
+ };
23360
+
23361
+ if (Fit.Core.InstanceOf(req, Fit.Http.JsonpRequest) === true)
23362
+ {
23363
+ req.OnSuccess(function(sender)
23364
+ {
23365
+ var response = req.GetResponse();
23366
+ var items = ((response instanceof Array) ? response : []);
23367
+
23368
+ processDataAndResolve(items);
23369
+ });
23370
+
23371
+ req.OnTimeout(function(sender)
23372
+ {
23373
+ resolve([]);
23374
+ Fit.Validation.ThrowError("Unable to get tags - request did not return data in time (JSONP timeout reached)");
23375
+ });
23376
+ }
23377
+ else
23378
+ {
23379
+ req.OnSuccess(function(sender)
23380
+ {
23381
+ var response = req.GetResponseJson();
23382
+ var items = ((response instanceof Array) ? response : []);
23383
+
23384
+ processDataAndResolve(items);
23385
+ });
23386
+
23387
+ req.OnFailure(function(sender)
23388
+ {
23389
+ resolve([]);
23390
+ Fit.Validation.ThrowError("Unable to get tags - request failed with HTTP Status code " + req.GetHttpStatus());
23391
+ });
23392
+ }
23393
+
23394
+ if (requestAwaiting !== null)
23395
+ {
23396
+ requestAwaiting.Abort();
23397
+ }
23398
+
23399
+ requestAwaiting = req;
23400
+ req.Start();
23401
+ },
23402
+ itemTemplate: function(item) // Item must define "name" and "id" properties - the {name} placeholder is replaced by "@" + the value of the "name" property - to get rid of "@" simply use an alternative property such as nameWithoutTag:"Some username"
23403
+ {
23404
+ if (item.Icon)
23405
+ {
23406
+ return '<li data-id="' + item.Value + '"><img src="' + item.Icon + '" style="width: 24px; height: 24px; border-radius: 24px; vertical-align: middle" alt=""><span style="display: inline-block; width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; margin-left: 5px">' + item.Title + '</span></li>';
23407
+ }
23408
+ else
23409
+ {
23410
+ return '<li data-id="' + item.Value + '">' + item.Title + '</li>';
23411
+ }
23412
+ },
23413
+ outputTemplate: function(item)
23414
+ {
23415
+ // IMPORTANT: Output produced must respect ACF (Advanced Content Filter).
23416
+ // So the tag produced must be allowed, and any attributes contained must be allowed.
23417
+
23418
+ var alternativeItem = null;
23419
+
23420
+ if (config.Tags.TagCreator)
23421
+ {
23422
+ var callbackArgs = { Sender: me, QueryMarker: trigger.Marker, Tag: Fit.Core.Clone(item) };
23423
+ alternativeItem = config.Tags.TagCreator(me, callbackArgs) || null;
23424
+ }
23425
+
23426
+ // Function should return a link for tags to "just work". Returning a <span> requires the span to be whitelisted in
23427
+ // extraAllowedContent configuration, but even then the editor will continue writing text within the <span> element,
23428
+ // rather than next to it. So one would expect something like: We will assign <span data-tag-type="@" ..>@James Bond</span> to this mission.
23429
+ // But what we get instead is something like: We will assign <span data-tag-type="@" ..>@James Bond to this mission</span>.
23430
+ // The same happens to link tags if the href attribute is removed, which is why we always add it, even when no URL is defined.
23431
+
23432
+ if (alternativeItem !== null)
23433
+ {
23434
+ return '<a data-tag-type="' + (alternativeItem.Type || trigger.Marker) + '" data-tag-id="' + (alternativeItem.Value || item.Value) + '"' + (alternativeItem.Data || item.Data ? ' data-tag-data="' + (alternativeItem.Data || item.Data) + '"' : '') + (alternativeItem.Url || item.Url ? ' href="' + (alternativeItem.Url || item.Url) + '"' : 'href=""') + '>' + (alternativeItem.Title || (trigger.Marker + item.Title)) + '</a>';
23435
+ }
23436
+ else
23437
+ {
23438
+ return '<a data-tag-type="' + trigger.Marker + '" data-tag-id="' + item.Value + '"' + (item.Data ? ' data-tag-data="' + item.Data + '"' : '') + (item.Url ? ' href="' + item.Url + '"' : 'href=""') + '>' + trigger.Marker + item.Title + '</a>';
23439
+ }
23440
+ }
23441
+ };
23442
+
23443
+ if (trigger.DebounceQuery !== 0) // A value of 0 (zero) disables debouncing
23444
+ {
23445
+ // Wrap feed handler in debounce function so that every time it gets invoked, it cancels the previous invocation
23446
+ mention.feed = Fit.Core.CreateDebouncer(mention.feed, trigger.DebounceQuery || 300).Invoke;
23447
+ }
23448
+
23449
+ Fit.Array.Add(mentions, mention)
23450
+ });
23451
+ }
22720
23452
 
22721
23453
  // Prevent control from losing focus when HTML editor is initialized,
22722
23454
  // e.g. if Design Mode is enabled when ordinary input control gains focus.
@@ -22770,49 +23502,30 @@ Fit.Controls.Input = function(ctlId)
22770
23502
  designEditor = CKEDITOR.replace(me.GetId() + "_DesignMode",
22771
23503
  {
22772
23504
  //allowedContent: true, // http://docs.ckeditor.com/#!/guide/dev_allowed_content_rules and http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-allowedContent
23505
+ extraAllowedContent: "a[data-tag-type,data-tag-id,data-tag-data]", // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-extraAllowedContent
22773
23506
  language: lang,
22774
23507
  disableNativeSpellChecker: me.CheckSpelling() === false,
22775
23508
  readOnly: me.Enabled() === false,
22776
23509
  tabIndex: me.Enabled() === false ? -1 : 0,
22777
23510
  title: "",
22778
23511
  startupFocus: focused === true ? "end" : false,
22779
- extraPlugins: Fit._internal.Controls.Input.Editor.Plugins.join(","), // "justify,pastefromword,base64image,base64imagepaste,dragresize",
23512
+ extraPlugins: plugins.join(","),
23513
+ clipboard_handleImages: false, // Disable native support for image pasting - allow base64imagepaste plugin to handle image data if loaded
22780
23514
  base64image: // Custom property used by base64image plugin if loaded
22781
23515
  {
22782
- storage: "blob", // "base64" (default) or "blob" - base64 will always be provided by browsers not supporting blob storage
23516
+ storage: designEditorConfig !== null && designEditorConfig.Plugins && designEditorConfig.Plugins.Images && designEditorConfig.Plugins.Images.EmbedType === "blob" ? "blob" : "base64", // "base64" (default) or "blob" - base64 will always be provided by browsers not supporting blob storage
22783
23517
  onImageAdded: onImageAdded
22784
23518
  },
22785
23519
  base64imagepaste: // Custom property used by base64imagepaste plugin if loaded - notice that IE has native support for image pasting as base64 so plugin is not triggered in IE
22786
23520
  {
22787
- storage: "blob", // "base64" (default) or "blob" - base64 will always be provided by browsers not supporting blob storage
23521
+ storage: designEditorConfig !== null && designEditorConfig.Plugins && designEditorConfig.Plugins.Images && designEditorConfig.Plugins.Images.EmbedType === "blob" ? "blob" : "base64", // "base64" (default) or "blob" - base64 will always be provided by browsers not supporting blob storage
22788
23522
  onImageAdded: onImageAdded
22789
23523
  },
22790
23524
  resize_enabled: resizable !== Fit.Controls.InputResizing.Disabled,
22791
23525
  resize_dir: resizable === Fit.Controls.InputResizing.Enabled ? "both" : resizable === Fit.Controls.InputResizing.Vertical ? "vertical" : resizable === Fit.Controls.InputResizing.Horizontal ? "horizontal" : "none", // Specific to resize plugin (horizontal | vertical | both - https://ckeditor.com/docs/ckeditor4/latest/features/resize.html)
22792
- toolbar: Fit._internal.Controls.Input.Editor.Toolbar,
22793
- /*[
22794
- {
22795
- name: "BasicFormatting",
22796
- items: [ "Bold", "Italic", "Underline" ]
22797
- },
22798
- {
22799
- name: "Justify",
22800
- items: [ "JustifyLeft", "JustifyCenter", "JustifyRight" ]
22801
- },
22802
- {
22803
- name: "Lists",
22804
- items: [ "NumberedList", "BulletedList", "Indent", "Outdent" ]
22805
- },
22806
- {
22807
- name: "Links",
22808
- items: [ "Link", "Unlink" ]
22809
- },
22810
- {
22811
- name: "Insert",
22812
- items: [ "base64image" ]
22813
- }
22814
- ],*/
23526
+ toolbar: toolbar,
22815
23527
  removeButtons: "", // Set to empty string to prevent CKEditor from removing buttons such as Underline
23528
+ mentions: mentions,
22816
23529
  on:
22817
23530
  {
22818
23531
  instanceReady: function()
@@ -22857,6 +23570,60 @@ Fit.Controls.Input = function(ctlId)
22857
23570
  me.Maximized(true);
22858
23571
  }
22859
23572
 
23573
+ if (config.InfoPanel && config.InfoPanel.Text)
23574
+ {
23575
+ var infoPanel = document.createElement("div");
23576
+ infoPanel.className = "FitUiControlInputInfoPanel";
23577
+ infoPanel.innerHTML = config.InfoPanel.Text;
23578
+ infoPanel.style.cssText = "text-align: " + (config.InfoPanel.Alignment ? config.InfoPanel.Alignment.toLowerCase() : "center");
23579
+
23580
+ var ckEditorInner = designEditor.container.$.querySelector(".cke_inner"); // Div in modern browsers, span in legacy IE
23581
+ var ckEditorBottom = designEditor.container.$.querySelector(".cke_inner span.cke_bottom"); // Only present if resize handle is enable
23582
+
23583
+ if (ckEditorInner !== null)
23584
+ {
23585
+ if (ckEditorBottom !== null)
23586
+ {
23587
+ Fit.Dom.InsertBefore(ckEditorBottom, infoPanel);
23588
+ }
23589
+ else
23590
+ {
23591
+ Fit.Dom.Add(ckEditorInner, infoPanel);
23592
+ }
23593
+ }
23594
+ }
23595
+
23596
+ // DISABLED: Doesn't work! Emoji panel contains an iFrame. When it is re-mounted
23597
+ // in DOM, the iframe reloads, and dynamically added content is lost. Also, this makes
23598
+ // CKEditor throw errors and the dialog never appears.
23599
+ /*if (Fit._internal.ControlBase.ReduceDocumentRootPollution === true)
23600
+ {
23601
+ // Move emoji dialog to control - otherwise placed in the root of the document where it pollutes,
23602
+ // and makes it impossible to interact with the dialog in light dismissable panels and callouts.
23603
+ // Dialog is placed alongside control and not within the control's container, to prevent Fit.UI
23604
+ // styling from affecting the dialog.
23605
+ if (config.Toolbar && config.Toolbar.Emojis === true)
23606
+ {
23607
+ var emojiButton = designEditor.container.$.querySelector("a.cke_button__emojipanel");
23608
+
23609
+ if (emojiButton !== null)
23610
+ {
23611
+ Fit.Events.AddHandler(emojiButton, "click", function(e)
23612
+ {
23613
+ setTimeout(function() // Postpone - made visible after click event
23614
+ {
23615
+ var emojiPanel = document.querySelector("div.cke_emoji-panel:not([style*='display: none'])");
23616
+
23617
+ if (emojiPanel !== null)
23618
+ {
23619
+ Fit.Dom.InsertAfter(me.GetDomElement(), emojiPanel);
23620
+ }
23621
+ }, 0);
23622
+ });
23623
+ }
23624
+ }
23625
+ }*/
23626
+
22860
23627
  designEditor._isReadyForInteraction = true;
22861
23628
 
22862
23629
  // Make editor assume configured width and height.
@@ -22868,6 +23635,21 @@ Fit.Controls.Input = function(ctlId)
22868
23635
  },
22869
23636
  change: function() // CKEditor bug: not fired in Opera 12 (possibly other old versions as well)
22870
23637
  {
23638
+ if (me._internal.FireOnChangeSuppressed === true)
23639
+ {
23640
+ // Do not process event - it has been fired by CKEditor when HTML
23641
+ // value was initially assigned in Value(..) which happend through
23642
+ // me._internal.ExecuteWithNoOnChange(function() { .. }).
23643
+ // See Value(..) implementation for details.
23644
+ return;
23645
+ }
23646
+
23647
+ // Assume value was changed by user if control has focus
23648
+ if (designEditorDirty === false && me.Focused() === true)
23649
+ {
23650
+ designEditorDirty = true;
23651
+ }
23652
+
22871
23653
  input.onkeyup();
22872
23654
  },
22873
23655
  resize: function() // Fires when size is changed, not just when resized using resize handle in lower right cornor
@@ -22875,8 +23657,73 @@ Fit.Controls.Input = function(ctlId)
22875
23657
  me._internal.Data("resized", "true");
22876
23658
  repaint();
22877
23659
  },
23660
+ selectionChange: function(ev)
23661
+ {
23662
+ // Disable/enable toolbar buttons, depending on whether a tag/mention is selected
23663
+
23664
+ var elm = ev.data.selection.getStartElement().$;
23665
+
23666
+ if (elm.tagName === "A" && Fit.Dom.Data(elm, "tag-id") !== null)
23667
+ {
23668
+ designEditorSuppressPaste = true;
23669
+ setTimeout(function() // Postpone - otherwise we won't be able to temporarily disable some of the buttons (https://jsfiddle.net/ymv56znq/14/)
23670
+ {
23671
+ disableDesignEditorButtons();
23672
+ }, 0);
23673
+ }
23674
+ else
23675
+ {
23676
+ designEditorSuppressPaste = false;
23677
+ restoreDesignEditorButtons();
23678
+ }
23679
+ },
23680
+ doubleclick: function(ev)
23681
+ {
23682
+ // Suppress link dialog for tags (similar code found in beforeCommandExec handler below)
23683
+ if (Fit.Dom.Data(ev.data.element.$, "tag-id") !== null)
23684
+ {
23685
+ ev.cancel();
23686
+ return;
23687
+ }
23688
+ },
23689
+ paste: function(ev)
23690
+ {
23691
+ // Prevent pasting (especially images) into tags.
23692
+ // OnPaste is suppressed using an OnPaste handler in capture phase, which will prevent the operation entirely
23693
+ // on supported browsers. On legacy browsers we handle this by invoking undo on the editor instance instead.
23694
+ //var path = ev.editor.elementPath(); // Null if dialog button is triggered without placing text cursor in editor first
23695
+ //if (Fit.Dom.Data(path.lastElement.$, "tag-id") !== null)
23696
+ if (designEditorSuppressPaste === true) // Also handled in a native OnPaste event handler (capture phase) for supported browsers, which suppresses the event entirely
23697
+ {
23698
+ setTimeout(function() // Postpone - allow editor to create snapshot
23699
+ {
23700
+ ev.editor.execCommand("undo"); // Undo change - paste event cannot be canceled, as it has already happened
23701
+ }, 0);
23702
+ return;
23703
+ }
23704
+ },
22878
23705
  beforeCommandExec: function(ev)
22879
23706
  {
23707
+ // Suppress any command (formatting, link dialog etc.) for tags (similar code found in doubleclick handler above).
23708
+ // Commmands can be triggered in multiple ways, e.g. using toolbar buttons, using keyboard shortcuts, and programmatically.
23709
+ var path = ev.editor.elementPath(); // Null if dialog button is triggered without placing text cursor in editor first
23710
+ if (path === null && ev.editor.getData().indexOf("<p><a data-tag-id=") === 0)
23711
+ {
23712
+ // Text cursor has not been placed in editor, but a command such as Bold or "insert image"
23713
+ // has been triggered, and editor content starts with a tag. This results in command being
23714
+ // applied to the tag, which we do not want. Usually this is prevented by the toolbar being
23715
+ // disabled when a tag is selected (see selectionChange event handler further up), but that
23716
+ // is not the case when the user has not yet placed the cursor in the editor.
23717
+ ev.cancel();
23718
+ return;
23719
+ }
23720
+ else if (path !== null && Fit.Dom.Data(path.lastElement.$, "tag-id") !== null && ev.data.name !== "undo") // Allow undo within tag, in case user typed something by mistake
23721
+ {
23722
+ // Cursor is currently placed in a tag - do not allow formatting
23723
+ ev.cancel();
23724
+ return;
23725
+ }
23726
+
22880
23727
  if (ev && ev.data && ev.data.command && ev.data.command.dialogName)
22881
23728
  {
22882
23729
  // Command triggered was a dialog
@@ -22933,6 +23780,68 @@ Fit.Controls.Input = function(ctlId)
22933
23780
  });
22934
23781
  }
22935
23782
 
23783
+ function disableDesignEditorButtons() // Might be called multiple times, e.g. if navigating from one tag/mention to another - buttons must be disabled every time since CKEditor itself re-enable buttons when navigating elements in editor
23784
+ {
23785
+ var preserveButtonState = designEditorRestoreButtonState === null;
23786
+
23787
+ if (preserveButtonState === true)
23788
+ {
23789
+ designEditorRestoreButtonState = {};
23790
+ }
23791
+
23792
+ Fit.Array.ForEach(designEditor.toolbar, function(toolbarGroup)
23793
+ {
23794
+ var items = toolbarGroup.items;
23795
+
23796
+ Fit.Array.ForEach(toolbarGroup.items, function(item)
23797
+ {
23798
+ if (item.command) // Buttons have a command identifier which can be used to resolve the actual command instance
23799
+ {
23800
+ var cmd = designEditor.getCommand(item.command);
23801
+
23802
+ if (preserveButtonState === true && cmd.state !== CKEDITOR.TRISTATE_DISABLED) // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_command.html#property-state
23803
+ {
23804
+ designEditorRestoreButtonState[item.command] = true;
23805
+ }
23806
+
23807
+ cmd.disable();
23808
+ }
23809
+ else if (item.setState) // MenuButtons allow for direct manipulation of enabled/disabled state
23810
+ {
23811
+ if (preserveButtonState === true && item.getState() !== CKEDITOR.TRISTATE_DISABLED) // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_command.html#property-state
23812
+ {
23813
+ designEditorRestoreButtonState[item.name] = item;
23814
+ }
23815
+
23816
+ item.setState(CKEDITOR.TRISTATE_DISABLED);
23817
+ }
23818
+ });
23819
+ });
23820
+ }
23821
+
23822
+ function restoreDesignEditorButtons()
23823
+ {
23824
+ console.log("RESTORING toolbar buttons");
23825
+
23826
+ if (designEditorRestoreButtonState !== null)
23827
+ {
23828
+ Fit.Array.ForEach(designEditorRestoreButtonState, function(commandKey)
23829
+ {
23830
+ if (designEditorRestoreButtonState[commandKey] === true) // Command button
23831
+ {
23832
+ var cmd = designEditor.getCommand(commandKey);
23833
+ cmd.enable();
23834
+ }
23835
+ else // MenuButton
23836
+ {
23837
+ designEditorRestoreButtonState[commandKey].setState(CKEDITOR.TRISTATE_OFF); // Enabled but not highlighted/activated like e.g. a bold button would be when selecting bold text
23838
+ }
23839
+ });
23840
+
23841
+ designEditorRestoreButtonState = null;
23842
+ }
23843
+ };
23844
+
22936
23845
  function updateDesignEditorSize()
22937
23846
  {
22938
23847
  if (designEditor !== null)
@@ -23004,7 +23913,9 @@ Fit.Controls.Input = function(ctlId)
23004
23913
 
23005
23914
  if (newVal !== preVal)
23006
23915
  {
23007
- if (designEditor !== null && htmlWrappedInParagraph === false) // A value not wrapped in paragraph(s) was assigned to HTML editor
23916
+ // DISABLED: No longer necessary with the introduction of designEditorDirty which ensures
23917
+ // that we get the initial value set from Value(), unless changed by the user using the editor.
23918
+ /*if (designEditor !== null && htmlWrappedInParagraph === false) // A value not wrapped in paragraph(s) was assigned to HTML editor
23008
23919
  {
23009
23920
  // Do not trigger OnChange if the only difference is that CKEditor
23010
23921
  // wrapped the value initially assigned to control in a paragraph.
@@ -23020,7 +23931,7 @@ Fit.Controls.Input = function(ctlId)
23020
23931
  {
23021
23932
  return; // Do not fire OnChange
23022
23933
  }
23023
- }
23934
+ }*/
23024
23935
 
23025
23936
  preVal = newVal;
23026
23937
  me._internal.FireOnChange();
@@ -23149,66 +24060,9 @@ Fit._internal.Controls.Input.Editor =
23149
24060
  /// <member container="Fit._internal.Controls.Input.Editor" name="Skin" access="public" static="true" type="'bootstrapck' | 'moono-lisa' | null">
23150
24061
  /// <description> Skin used with DesignMode - must be set before an editor is created and cannot be changed for each individual control </description>
23151
24062
  /// </member>
23152
- Skin: null, // Notice: CKEditor does not support multiple different skins on the same page - do not change value once an editor has been created
23153
-
23154
- /// <member container="Fit._internal.Controls.Input.Editor" name="Plugins" access="public" static="true" type="('justify' | 'pastefromword' | 'resize' | 'base64image' | 'base64imagepaste' | 'dragresize')[]">
23155
- /// <description> Additional plugins used with DesignMode </description>
23156
- /// </member>
23157
- Plugins: ["justify", "pastefromword", "resize" /*"base64image", "base64imagepaste", "dragresize"*/], // Regarding base64imagepaste and dragresize: IE11 has native support for pasting images as base64 and IE8+ has native support for image resizing, so plugins are not in effect in IE, even when enabled
23158
-
23159
- /// <member container="Fit._internal.Controls.Input.Editor" name="Toolbar" access="public" static="true" type="( { name: 'BasicFormatting', items: ('Bold' | 'Italic' | 'Underline')[] } | { name: 'Justify', items: ('JustifyLeft' | 'JustifyCenter' | 'JustifyRight')[] } | { name: 'Lists', items: ('NumberedList' | 'BulletedList' | 'Indent' | 'Outdent')[] } | { name: 'Links', items: ('Link' | 'Unlink')[] } | { name: 'Insert', items: ('base64image')[] } )[]">
23160
- /// <description> Toolbar buttons used with DesignMode - make sure necessary plugins are loaded (see Fit._internal.Controls.Input.EditorPlugins) </description>
23161
- /// </member>
23162
- Toolbar:
23163
- [
23164
- {
23165
- name: "BasicFormatting",
23166
- items: [ "Bold", "Italic", "Underline" ]
23167
- },
23168
- {
23169
- name: "Justify",
23170
- items: [ "JustifyLeft", "JustifyCenter", "JustifyRight" ]
23171
- },
23172
- {
23173
- name: "Lists",
23174
- items: [ "NumberedList", "BulletedList", "Indent", "Outdent" ]
23175
- },
23176
- {
23177
- name: "Links",
23178
- items: [ "Link", "Unlink" ]
23179
- }/*,
23180
- {
23181
- name: "Insert",
23182
- items: [ "base64image" ]
23183
- }*/
23184
- ]
24063
+ Skin: null // Notice: CKEditor does not support multiple different skins on the same page - do not change value once an editor has been created
23185
24064
  };
23186
24065
 
23187
- /// <container name="Fit._internal.Controls.Input.BlobManager">
23188
- /// Internal settings related to blob storage management in HTML Editor (Design Mode)
23189
- /// </container>
23190
- Fit._internal.Controls.Input.BlobManager =
23191
- {
23192
- /// <member container="Fit._internal.Controls.Input.BlobManager" name="RevokeBlobUrlsOnDispose" access="public" static="true" type="'All' | 'UnreferencedOnly'">
23193
- /// <description>
23194
- /// Dispose images from blob storage (revoke blob URLs) added though image plugins when control is disposed.
23195
- /// If "UnreferencedOnly" is specified, the component using Fit.UI's input control will be responsible for
23196
- /// disposing referenced blobs. Failing to do so may cause a memory leak.
23197
- /// </description>
23198
- /// </member>
23199
- RevokeBlobUrlsOnDispose: "All", // "All" | "UnreferencedOnly"
23200
-
23201
- /// <member container="Fit._internal.Controls.Input.BlobManager" name="RevokeExternalBlobUrlsOnDispose" access="public" static="true" type="boolean">
23202
- /// <description>
23203
- /// Dispose images from blob storage (revoke blob URLs) added through Value(..)
23204
- /// function when control is disposed. Basically ownership of these blobs are handed
23205
- /// over to the control for the duration of its life time.
23206
- /// These images are furthermore subject to the rule set in RevokeBlobUrlsOnDispose.
23207
- /// </description>
23208
- /// </member>
23209
- RevokeExternalBlobUrlsOnDispose: false
23210
- }
23211
-
23212
24066
  /// <container name="Fit.Controls.InputResizing">
23213
24067
  /// <description> Resizing options </description>
23214
24068
  /// <member name="Enabled" access="public" static="true" type="string" default="Enabled"> Allow for resizing both vertically and horizontally </member>