suneditor 2.41.3 → 2.42.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 (45) hide show
  1. package/README.md +107 -24
  2. package/dist/css/suneditor.min.css +1 -1
  3. package/dist/suneditor.min.js +2 -2
  4. package/package.json +1 -1
  5. package/src/assets/css/suneditor.css +25 -6
  6. package/src/assets/defaultIcons.js +2 -0
  7. package/src/lang/Lang.d.ts +3 -1
  8. package/src/lang/ckb.js +2 -0
  9. package/src/lang/da.js +2 -0
  10. package/src/lang/de.js +2 -0
  11. package/src/lang/en.js +2 -0
  12. package/src/lang/es.js +2 -0
  13. package/src/lang/fr.js +10 -8
  14. package/src/lang/he.js +2 -0
  15. package/src/lang/it.js +2 -0
  16. package/src/lang/ja.js +2 -0
  17. package/src/lang/ko.js +2 -0
  18. package/src/lang/lv.js +2 -0
  19. package/src/lang/nl.js +2 -0
  20. package/src/lang/pl.js +2 -0
  21. package/src/lang/pt_br.js +2 -0
  22. package/src/lang/ro.js +2 -0
  23. package/src/lang/ru.js +2 -0
  24. package/src/lang/se.js +2 -0
  25. package/src/lang/ua.js +2 -0
  26. package/src/lang/zh_cn.js +2 -0
  27. package/src/lib/constructor.js +50 -11
  28. package/src/lib/context.js +4 -1
  29. package/src/lib/core.d.ts +86 -11
  30. package/src/lib/core.js +555 -147
  31. package/src/lib/util.d.ts +24 -1
  32. package/src/lib/util.js +64 -15
  33. package/src/options.d.ts +63 -8
  34. package/src/plugins/dialog/audio.js +5 -5
  35. package/src/plugins/dialog/image.js +30 -20
  36. package/src/plugins/dialog/video.js +13 -13
  37. package/src/plugins/fileBrowser/imageGallery.js +2 -3
  38. package/src/plugins/modules/_anchor.js +6 -4
  39. package/src/plugins/modules/component.d.ts +1 -1
  40. package/src/plugins/modules/fileBrowser.js +6 -1
  41. package/src/plugins/modules/fileManager.js +1 -3
  42. package/src/plugins/modules/resizing.js +11 -6
  43. package/src/plugins/submenu/align.js +32 -27
  44. package/src/plugins/submenu/font.js +1 -1
  45. package/src/plugins/submenu/horizontalRule.js +19 -25
package/src/lib/core.js CHANGED
@@ -39,6 +39,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
39
39
  _d: _d,
40
40
  _w: _w,
41
41
  _parser: new _w.DOMParser(),
42
+ _prevRtl: options.rtl,
42
43
 
43
44
  /**
44
45
  * @description Document object of the iframe if created as an iframe || _d
@@ -82,7 +83,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
82
83
  /**
83
84
  * @description Computed style of the wysiwyg area (window.getComputedStyle(context.element.wysiwyg))
84
85
  */
85
- wwComputedStyle: _w.getComputedStyle(context.element.wysiwyg),
86
+ wwComputedStyle: null,
86
87
 
87
88
  /**
88
89
  * @description Notice object
@@ -222,6 +223,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
222
223
  */
223
224
  _htmlCheckWhitelistRegExp: null,
224
225
 
226
+ /**
227
+ * @description Tag blacklist RegExp object used in "_consistencyCheckOfHTML" method
228
+ * @private
229
+ */
230
+ _htmlCheckBlacklistRegExp: null,
231
+
225
232
  /**
226
233
  * @description RegExp when using check disallowd tags. (b, i, ins, strike, s)
227
234
  * @private
@@ -234,12 +241,24 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
234
241
  */
235
242
  editorTagsWhitelistRegExp: null,
236
243
 
244
+ /**
245
+ * @description Editor tags blacklist (RegExp object)
246
+ * util.createTagsBlacklist(options.tagsBlacklist)
247
+ */
248
+ editorTagsBlacklistRegExp: null,
249
+
237
250
  /**
238
251
  * @description Tag whitelist when pasting (RegExp object)
239
252
  * util.createTagsWhitelist(options.pasteTagsWhitelist)
240
253
  */
241
254
  pasteTagsWhitelistRegExp: null,
242
255
 
256
+ /**
257
+ * @description Tag blacklist when pasting (RegExp object)
258
+ * util.createTagsBlacklist(options.pasteTagsBlacklist)
259
+ */
260
+ pasteTagsBlacklistRegExp: null,
261
+
243
262
  /**
244
263
  * @description Boolean value of whether the editor has focus
245
264
  */
@@ -261,12 +280,24 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
261
280
  */
262
281
  _attributesWhitelistRegExp: null,
263
282
 
283
+ /**
284
+ * @description Attributes blacklist used by the cleanHTML method
285
+ * @private
286
+ */
287
+ _attributesBlacklistRegExp: null,
288
+
264
289
  /**
265
290
  * @description Attributes of tags whitelist used by the cleanHTML method
266
291
  * @private
267
292
  */
268
293
  _attributesTagsWhitelist: null,
269
294
 
295
+ /**
296
+ * @description Attributes of tags blacklist used by the cleanHTML method
297
+ * @private
298
+ */
299
+ _attributesTagsBlacklist: null,
300
+
270
301
  /**
271
302
  * @description binded controllersOff method
272
303
  * @private
@@ -414,6 +445,24 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
414
445
  */
415
446
  commandMap: null,
416
447
 
448
+ /**
449
+ * @description CSS properties related to style tags
450
+ * @private
451
+ */
452
+ _commandMapStyles: {
453
+ STRONG: ['font-weight'],
454
+ U: ['text-decoration'],
455
+ EM: ['font-style'],
456
+ DEL: ['text-decoration']
457
+ },
458
+
459
+ /**
460
+ * @description Contains pairs of all "data-commands" and "elements" setted in toolbar over time
461
+ * Used primarily to save and recover button states after the toolbar re-creation
462
+ * Updates each "_cachingButtons()" invocation
463
+ */
464
+ allCommandButtons: null,
465
+
417
466
  /**
418
467
  * @description Style button related to edit area
419
468
  * @property {Element} fullScreen fullScreen button element
@@ -471,6 +520,40 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
471
520
  _lineBreakDir: ''
472
521
  },
473
522
 
523
+ /**
524
+ * @description Save the current buttons states to "allCommandButtons" object
525
+ */
526
+ saveButtonStates: function () {
527
+ if (!this.allCommandButtons) this.allCommandButtons = {};
528
+
529
+ const currentButtons = this.context.element._buttonTray.querySelectorAll('.se-menu-list button[data-display]');
530
+ for (let i = 0, element, command; i < currentButtons.length; i++) {
531
+ element = currentButtons[i];
532
+ command = element.getAttribute('data-command');
533
+
534
+ this.allCommandButtons[command] = element;
535
+ }
536
+ },
537
+
538
+ /**
539
+ * @description Recover the current buttons states from "allCommandButtons" object
540
+ */
541
+ recoverButtonStates: function () {
542
+ if (this.allCommandButtons) {
543
+ const currentButtons = this.context.element._buttonTray.querySelectorAll('.se-menu-list button[data-display]');
544
+ for (let i = 0, button, command, oldButton; i < currentButtons.length; i++) {
545
+ button = currentButtons[i];
546
+ command = button.getAttribute('data-command');
547
+
548
+ oldButton = this.allCommandButtons[command];
549
+ if (oldButton) {
550
+ button.parentElement.replaceChild(oldButton, button);
551
+ if (this.context.tool[command]) this.context.tool[command] = oldButton;
552
+ }
553
+ }
554
+ }
555
+ },
556
+
474
557
  /**
475
558
  * @description If the plugin is not added, add the plugin and call the 'add' function.
476
559
  * If the plugin is added call callBack function.
@@ -559,7 +642,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
559
642
  },
560
643
 
561
644
  /**
562
- * @description Enabled submenu
645
+ * @description Enable submenu
563
646
  * @param {Element} element Submenu's button element to call
564
647
  */
565
648
  submenuOn: function (element) {
@@ -598,7 +681,19 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
598
681
  },
599
682
 
600
683
  /**
601
- * @description Enabled container
684
+ * @description Disable more layer
685
+ */
686
+ moreLayerOff: function() {
687
+ if (this._moreLayerActiveButton) {
688
+ const layer = context.element.toolbar.querySelector('.' + this._moreLayerActiveButton.getAttribute('data-command'));
689
+ layer.style.display = 'none';
690
+ util.removeClass(this._moreLayerActiveButton, 'on');
691
+ this._moreLayerActiveButton = null;
692
+ }
693
+ },
694
+
695
+ /**
696
+ * @description Enable container
602
697
  * @param {Element} element Container's button element to call
603
698
  */
604
699
  containerOn: function (element) {
@@ -868,14 +963,21 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
868
963
  * @description Focus to wysiwyg area using "native focus function"
869
964
  */
870
965
  nativeFocus: function () {
966
+ this.__focus();
967
+ this._editorRange();
968
+ },
969
+
970
+ /**
971
+ * @description Focus method
972
+ * @private
973
+ */
974
+ __focus: function () {
871
975
  const caption = util.getParentElement(this.getSelectionNode(), 'figcaption');
872
976
  if (caption) {
873
977
  caption.focus();
874
978
  } else {
875
979
  context.element.wysiwyg.focus();
876
980
  }
877
-
878
- this._editorRange();
879
981
  },
880
982
 
881
983
  /**
@@ -980,8 +1082,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
980
1082
  }
981
1083
 
982
1084
  selection.addRange(range);
983
- this._editorRange();
984
- if (options.iframe) this.nativeFocus();
1085
+ this._rangeInfo(range, this.getSelection());
1086
+ if (options.iframe) this.__focus();
985
1087
 
986
1088
  return range;
987
1089
  },
@@ -1074,7 +1176,6 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1074
1176
  const selection = this.getSelection();
1075
1177
  if (!selection) return null;
1076
1178
  let range = null;
1077
- let selectionNode = null;
1078
1179
 
1079
1180
  if (selection.rangeCount > 0) {
1080
1181
  range = selection.getRangeAt(0);
@@ -1082,6 +1183,20 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1082
1183
  range = this._createDefaultRange();
1083
1184
  }
1084
1185
 
1186
+ if (util.isFormatElement(range.endContainer) && range.endOffset === 0) {
1187
+ range = this.setRange(range.startContainer, range.startOffset, range.startContainer, range.startContainer.length);
1188
+ }
1189
+
1190
+ this._rangeInfo(range, selection);
1191
+ },
1192
+
1193
+ /**
1194
+ * @description Set "range" and "selection" info.
1195
+ * @param {Object} range range object.
1196
+ * @param {Object} selection selection object.
1197
+ */
1198
+ _rangeInfo: function (range, selection) {
1199
+ let selectionNode = null;
1085
1200
  this._variable._range = range;
1086
1201
 
1087
1202
  if (range.collapsed) {
@@ -1145,19 +1260,19 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1145
1260
 
1146
1261
  if (util.isFormatElement(startCon)) {
1147
1262
  if (!startCon.childNodes[startOff]) {
1148
- startCon = startCon.lastChild;
1263
+ startCon = startCon.lastChild || startCon;
1149
1264
  startOff = startCon.textContent.length;
1150
1265
  } else {
1151
- startCon = startCon.childNodes[startOff];
1266
+ startCon = startCon.childNodes[startOff] || startCon;
1152
1267
  startOff = 0;
1153
1268
  }
1154
1269
  while (startCon && startCon.nodeType === 1 && startCon.firstChild) {
1155
- startCon = startCon.firstChild;
1270
+ startCon = startCon.firstChild || startCon;
1156
1271
  startOff = 0;
1157
1272
  }
1158
1273
  }
1159
1274
  if (util.isFormatElement(endCon)) {
1160
- endCon = endCon.childNodes[endOff] || endCon.lastChild;
1275
+ endCon = endCon.childNodes[endOff] || endCon.lastChild || endCon;
1161
1276
  while (endCon && endCon.nodeType === 1 && endCon.lastChild) {
1162
1277
  endCon = endCon.lastChild;
1163
1278
  }
@@ -1255,7 +1370,6 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1255
1370
  if (util.isWysiwygDiv(range.startContainer)) {
1256
1371
  const children = context.element.wysiwyg.children;
1257
1372
  if (children.length === 0) return [];
1258
-
1259
1373
  this.setRange(children[0], 0, children[children.length - 1], children[children.length - 1].textContent.trim().length);
1260
1374
  range = this.getRange();
1261
1375
  }
@@ -1397,7 +1511,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1397
1511
  * @returns {Element}
1398
1512
  */
1399
1513
  appendFormatTag: function (element, formatNode) {
1400
- if (!element.parentNode) return null;
1514
+ if (!element || !element.parentNode) return null;
1401
1515
 
1402
1516
  const currentFormatEl = util.getFormatElement(this.getSelectionNode(), null);
1403
1517
  let oFormat = null;
@@ -1451,9 +1565,9 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1451
1565
  if (formatEl && util.onlyZeroWidthSpace(formatEl)) util.removeItem(formatEl);
1452
1566
  }
1453
1567
 
1454
- this.setRange(element, 0, element, 0);
1455
-
1456
1568
  if (!notSelect) {
1569
+ this.setRange(element, 0, element, 0);
1570
+
1457
1571
  const fileComponentInfo = this.getFileComponent(element);
1458
1572
  if (fileComponentInfo) {
1459
1573
  this.selectComponent(fileComponentInfo.target, fileComponentInfo.pluginName);
@@ -1592,8 +1706,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
1592
1706
  const startOff = range.startOffset;
1593
1707
  const endOff = range.endOffset;
1594
1708
  const formatRange = range.startContainer === commonCon && util.isFormatElement(commonCon);
1595
- const startCon = formatRange ? (commonCon.childNodes[startOff] || commonCon.childNodes[0]) : range.startContainer;
1596
- const endCon = formatRange ? (commonCon.childNodes[endOff] || commonCon.childNodes[commonCon.childNodes.length - 1]) : range.endContainer;
1709
+ const startCon = formatRange ? (commonCon.childNodes[startOff] || commonCon.childNodes[0] || range.startContainer) : range.startContainer;
1710
+ const endCon = formatRange ? (commonCon.childNodes[endOff] || commonCon.childNodes[commonCon.childNodes.length - 1] || range.endContainer) : range.endContainer;
1597
1711
  let parentNode, originAfter = null;
1598
1712
 
1599
1713
  if (!afterNode) {
@@ -2712,7 +2826,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
2712
2826
  for (let i = endLength - 1, newRange; i > 0; i--) {
2713
2827
  newNode = appendNode.cloneNode(false);
2714
2828
  newRange = this._nodeChange_middleLine(lineNodes[i], newNode, validation, isRemoveFormat, isRemoveNode, _removeCheck, end.container);
2715
- if (newRange.endContainer) {
2829
+ if (newRange.endContainer && newRange.ancestor.contains(newRange.endContainer)) {
2716
2830
  end.ancestor = null;
2717
2831
  end.container = newRange.endContainer;
2718
2832
  }
@@ -3987,7 +4101,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
3987
4101
  return {
3988
4102
  ancestor: pNode,
3989
4103
  container: container,
3990
- offset: offset
4104
+ offset: container.nodeType === 1 && offset === 1 ? container.childNodes.length : offset
3991
4105
  };
3992
4106
  },
3993
4107
 
@@ -4000,19 +4114,27 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4000
4114
  actionCall: function (command, display, target) {
4001
4115
  // call plugins
4002
4116
  if (display) {
4003
- if (/more/i.test(display) && target !== this._moreLayerActiveButton) {
4004
- const layer = context.element.toolbar.querySelector('.' + command);
4005
- if (layer) {
4006
- if (this._moreLayerActiveButton) {
4007
- (context.element.toolbar.querySelector('.' + this._moreLayerActiveButton.getAttribute('data-command'))).style.display = 'none';
4008
- util.removeClass(this._moreLayerActiveButton, 'on');
4117
+ if (/more/i.test(display)) {
4118
+ if (target !== this._moreLayerActiveButton) {
4119
+ const layer = context.element.toolbar.querySelector('.' + command);
4120
+ if (layer) {
4121
+ if (this._moreLayerActiveButton) this.moreLayerOff();
4122
+
4123
+ this._moreLayerActiveButton = target;
4124
+ layer.style.display = 'block';
4125
+
4126
+ event._showToolbarBalloon();
4127
+ event._showToolbarInline();
4009
4128
  }
4010
4129
  util.addClass(target, 'on');
4011
- this._moreLayerActiveButton = target;
4012
- layer.style.display = 'block';
4130
+ } else {
4131
+ const layer = context.element.toolbar.querySelector('.' + this._moreLayerActiveButton.getAttribute('data-command'));
4132
+ if (layer) {
4133
+ this.moreLayerOff();
4013
4134
 
4014
- event._showToolbarBalloon();
4015
- event._showToolbarInline();
4135
+ event._showToolbarBalloon();
4136
+ event._showToolbarInline();
4137
+ }
4016
4138
  }
4017
4139
  return;
4018
4140
  }
@@ -4022,7 +4144,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4022
4144
  return;
4023
4145
  }
4024
4146
 
4025
- if (this.isReadOnly) return;
4147
+ if (this.isReadOnly && util.arrayIncludes(this.resizingDisabledButtons, target)) return;
4026
4148
  if (/submenu/.test(display) && (this._menuTray[command] === null || target !== this.submenuActiveButton)) {
4027
4149
  this.callPlugin(command, this.submenuOn.bind(this, target), target);
4028
4150
  return;
@@ -4039,17 +4161,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4039
4161
  this.commandHandler(target, command);
4040
4162
  }
4041
4163
 
4042
- if (/more/i.test(display)) {
4043
- const layer = context.element.toolbar.querySelector('.' + this._moreLayerActiveButton.getAttribute('data-command'));
4044
- if (layer) {
4045
- util.removeClass(this._moreLayerActiveButton, 'on');
4046
- this._moreLayerActiveButton = null;
4047
- layer.style.display = 'none';
4048
-
4049
- event._showToolbarBalloon();
4050
- event._showToolbarInline();
4051
- }
4052
- } else if (/submenu/.test(display)) {
4164
+ if (/submenu/.test(display)) {
4053
4165
  this.submenuOff();
4054
4166
  } else if (!/command/.test(display)) {
4055
4167
  this.submenuOff();
@@ -4073,6 +4185,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4073
4185
  case 'paste':
4074
4186
  break;
4075
4187
  case 'selectAll':
4188
+ this.containerOff();
4076
4189
  const wysiwyg = context.element.wysiwyg;
4077
4190
  let first = util.getChildElement(wysiwyg.firstChild, function (current) { return current.childNodes.length === 0 || current.nodeType === 3; }, false) || wysiwyg.firstChild;
4078
4191
  let last = util.getChildElement(wysiwyg.lastChild, function (current) { return current.childNodes.length === 0 || current.nodeType === 3; }, true) || wysiwyg.lastChild;
@@ -4124,6 +4237,15 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4124
4237
  case 'showBlocks':
4125
4238
  this.toggleDisplayBlocks();
4126
4239
  break;
4240
+ case 'dir':
4241
+ this.setDir(options.rtl ? 'ltr' : 'rtl');
4242
+ break;
4243
+ case 'dir_ltr':
4244
+ this.setDir('ltr');
4245
+ break;
4246
+ case 'dir_rtl':
4247
+ this.setDir('rtl');
4248
+ break;
4127
4249
  case 'save':
4128
4250
  if (typeof options.callBackSave === 'function') {
4129
4251
  options.callBackSave(this.getContents(false), this._variable.isChanged);
@@ -4150,7 +4272,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4150
4272
  removeNode = 'SUB';
4151
4273
  }
4152
4274
 
4153
- this.nodeChange(cmd, null, [removeNode], false);
4275
+ this.nodeChange(cmd, this._commandMapStyles[command] || null, [removeNode], false);
4154
4276
  this.focus();
4155
4277
  }
4156
4278
  },
@@ -4230,7 +4352,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4230
4352
  util.setDisabledButtons(!isCodeView, this.codeViewDisabledButtons);
4231
4353
 
4232
4354
  if (isCodeView) {
4233
- this._setCodeDataToEditor();
4355
+ if (!util.isNonEditable(context.element.wysiwygFrame)) this._setCodeDataToEditor();
4234
4356
  context.element.wysiwygFrame.scrollTop = 0;
4235
4357
  context.element.code.style.display = 'none';
4236
4358
  context.element.wysiwygFrame.style.display = 'block';
@@ -4256,14 +4378,18 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4256
4378
  util.removeClass(this._styleCommandMap.codeView, 'active');
4257
4379
 
4258
4380
  // history stack
4259
- this.history.push(false);
4260
- this.history._resetCachingButton();
4381
+ if (!util.isNonEditable(context.element.wysiwygFrame)) {
4382
+ this.history.push(false);
4383
+ this.history._resetCachingButton();
4384
+ }
4261
4385
  } else {
4262
4386
  this._setEditorDataToCodeView();
4263
4387
  this._variable._codeOriginCssText = this._variable._codeOriginCssText.replace(/(\s?display(\s+)?:(\s+)?)[a-zA-Z]+(?=;)/, 'display: block');
4264
4388
  this._variable._wysiwygOriginCssText = this._variable._wysiwygOriginCssText.replace(/(\s?display(\s+)?:(\s+)?)[a-zA-Z]+(?=;)/, 'display: none');
4265
4389
 
4266
- if (options.height === 'auto' && !options.codeMirrorEditor) context.element.code.style.height = context.element.code.scrollHeight > 0 ? (context.element.code.scrollHeight + 'px') : 'auto';
4390
+ if (this._variable.isFullScreen) context.element.code.style.height = '100%';
4391
+ else if (options.height === 'auto' && !options.codeMirrorEditor) context.element.code.style.height = context.element.code.scrollHeight > 0 ? (context.element.code.scrollHeight + 'px') : 'auto';
4392
+
4267
4393
  if (options.codeMirrorEditor) options.codeMirrorEditor.refresh();
4268
4394
 
4269
4395
  this._variable.isCodeView = true;
@@ -4309,7 +4435,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4309
4435
  }
4310
4436
  }
4311
4437
 
4312
- this._wd.head.innerHTML = parseDocument.head.innerHTML;
4438
+ let headers = parseDocument.head.innerHTML;
4439
+ if (!parseDocument.head.querySelector('link[rel="stylesheet"]') || (this.options.height === 'auto' && !parseDocument.head.querySelector('style'))) {
4440
+ headers += util._setIframeCssTags(this.options);
4441
+ }
4442
+
4443
+ this._wd.head.innerHTML = headers;
4313
4444
  this._wd.body.innerHTML = this.convertContentsForEditor(parseDocument.body.innerHTML);
4314
4445
 
4315
4446
  const attrs = parseDocument.body.attributes;
@@ -4333,7 +4464,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4333
4464
  * @private
4334
4465
  */
4335
4466
  _setEditorDataToCodeView: function () {
4336
- const codeContents = this.convertHTMLForCodeView(context.element.wysiwyg);
4467
+ const codeContents = this.convertHTMLForCodeView(context.element.wysiwyg, false);
4337
4468
  let codeValue = '';
4338
4469
 
4339
4470
  if (options.fullPage) {
@@ -4351,7 +4482,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4351
4482
 
4352
4483
  /**
4353
4484
  * @description Changes to full screen or default screen
4354
- * @param {Element} element full screen button
4485
+ * @param {Element|null} element full screen button
4355
4486
  */
4356
4487
  toggleFullScreen: function (element) {
4357
4488
  const topArea = context.element.topArea;
@@ -4361,6 +4492,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4361
4492
  const code = context.element.code;
4362
4493
  const _var = this._variable;
4363
4494
  this.controllersOff();
4495
+
4496
+ const wasToolbarHidden = (toolbar.style.display === 'none' || (this._isInline && !this._inlineToolbarAttr.isShow));
4364
4497
 
4365
4498
  if (!_var.isFullScreen) {
4366
4499
  _var.isFullScreen = true;
@@ -4406,7 +4539,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4406
4539
  _var.innerHeight_fullScreen = (_w.innerHeight - toolbar.offsetHeight);
4407
4540
  editorArea.style.height = (_var.innerHeight_fullScreen - options.fullScreenOffset) + 'px';
4408
4541
 
4409
- util.changeElement(element.firstElementChild, icons.reduction);
4542
+ if (element) util.changeElement(element.firstElementChild, icons.reduction);
4410
4543
 
4411
4544
  if (options.iframe && options.height === 'auto') {
4412
4545
  editorArea.style.overflow = 'auto';
@@ -4425,6 +4558,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4425
4558
  topArea.style.cssText = _var._originCssText;
4426
4559
  _d.body.style.overflow = _var._bodyOverflow;
4427
4560
 
4561
+ if (options.height === 'auto' && !options.codeMirrorEditor) event._codeViewAutoHeight();
4562
+
4428
4563
  if (!!options.toolbarContainer) options.toolbarContainer.appendChild(toolbar);
4429
4564
 
4430
4565
  if (options.stickyToolbar > -1) {
@@ -4443,12 +4578,14 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4443
4578
  if (!!options.toolbarContainer) util.removeClass(toolbar, 'se-toolbar-balloon');
4444
4579
 
4445
4580
  event.onScroll_window();
4446
- util.changeElement(element.firstElementChild, icons.expansion);
4581
+ if (element) util.changeElement(element.firstElementChild, icons.expansion);
4447
4582
 
4448
4583
  context.element.topArea.style.marginTop = '';
4449
4584
  util.removeClass(this._styleCommandMap.fullScreen, 'active');
4450
4585
  }
4451
4586
 
4587
+ if (wasToolbarHidden) functions.toolbar.hide();
4588
+
4452
4589
  // user event
4453
4590
  if (typeof functions.toggleFullScreen === 'function') functions.toggleFullScreen(this._variable.isFullScreen, this);
4454
4591
  },
@@ -4571,6 +4708,70 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4571
4708
  }
4572
4709
  },
4573
4710
 
4711
+ /**
4712
+ * @description Set direction to "rtl" or "ltr".
4713
+ * @param {String} dir "rtl" or "ltr"
4714
+ */
4715
+ setDir: function (dir) {
4716
+ const rtl = dir === 'rtl';
4717
+ const changeDir = this._prevRtl !== rtl;
4718
+ this._prevRtl = options.rtl = rtl;
4719
+
4720
+ if (changeDir) {
4721
+ // align buttons
4722
+ if (this.plugins.align) {
4723
+ this.plugins.align.exchangeDir.call(this);
4724
+ }
4725
+ // indent buttons
4726
+ if (context.tool.indent) util.changeElement(context.tool.indent.firstElementChild, icons.indent);
4727
+ if (context.tool.outdent) util.changeElement(context.tool.outdent.firstElementChild, icons.outdent);
4728
+ }
4729
+
4730
+ const el = context.element;
4731
+ if (rtl) {
4732
+ util.addClass(el.topArea, 'se-rtl');
4733
+ util.addClass(el.wysiwygFrame, 'se-rtl');
4734
+ } else {
4735
+ util.removeClass(el.topArea, 'se-rtl');
4736
+ util.removeClass(el.wysiwygFrame, 'se-rtl');
4737
+ }
4738
+
4739
+ const lineNodes = util.getListChildren(el.wysiwyg, function (current) {
4740
+ return util.isFormatElement(current) && (current.style.marginRight || current.style.marginLeft || current.style.textAlign);
4741
+ });
4742
+
4743
+ for (let i = 0, len = lineNodes.length, n, l, r; i < len; i++) {
4744
+ n = lineNodes[i];
4745
+ // indent margin
4746
+ r = n.style.marginRight;
4747
+ l = n.style.marginLeft;
4748
+ if (r || l) {
4749
+ n.style.marginRight = l;
4750
+ n.style.marginLeft = r;
4751
+ }
4752
+ // text align
4753
+ r = n.style.textAlign;
4754
+ if (r === 'left') n.style.textAlign = 'right';
4755
+ else if (r === 'right') n.style.textAlign = 'left';
4756
+ }
4757
+
4758
+ const tool = context.tool;
4759
+ if (tool.dir) {
4760
+ util.changeTxt(tool.dir.querySelector('.se-tooltip-text'), lang.toolbar[options.rtl ? 'dir_ltr' : 'dir_rtl']);
4761
+ util.changeElement(tool.dir.firstElementChild, icons[options.rtl ? 'dir_ltr' : 'dir_rtl']);
4762
+ }
4763
+
4764
+ if (tool.dir_ltr) {
4765
+ if (rtl) util.removeClass(tool.dir_ltr, 'active');
4766
+ else util.addClass(tool.dir_ltr, 'active');
4767
+ }
4768
+
4769
+ if (tool.dir_rtl) {
4770
+ if (rtl) util.addClass(tool.dir_rtl, 'active');
4771
+ else util.removeClass(tool.dir_rtl, 'active');
4772
+ }
4773
+ },
4774
+
4574
4775
  /**
4575
4776
  * @description Sets the HTML string
4576
4777
  * @param {String|undefined} html HTML string
@@ -4586,7 +4787,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4586
4787
  // history stack
4587
4788
  this.history.push(false);
4588
4789
  } else {
4589
- const value = this.convertHTMLForCodeView(convertValue);
4790
+ const value = this.convertHTMLForCodeView(convertValue, false);
4590
4791
  this._setCodeView(value);
4591
4792
  }
4592
4793
  },
@@ -4607,7 +4808,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4607
4808
  * @returns {Object}
4608
4809
  */
4609
4810
  getContents: function (onlyContents) {
4610
- const contents = context.element.wysiwyg.innerHTML;
4811
+ const contents = this.convertHTMLForCodeView(context.element.wysiwyg, true);
4611
4812
  const renderHTML = util.createElement('DIV');
4612
4813
  renderHTML.innerHTML = contents;
4613
4814
 
@@ -4690,7 +4891,8 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4690
4891
  .replace(/\n/g, '')
4691
4892
  .replace(/<(script|style)[\s\S]*>[\s\S]*<\/(script|style)>/gi, '')
4692
4893
  .replace(/<[a-z0-9]+\:[a-z0-9]+[^>^\/]*>[^>]*<\/[a-z0-9]+\:[a-z0-9]+>/gi, '')
4693
- .replace(this.editorTagsWhitelistRegExp, '');
4894
+ .replace(this.editorTagsWhitelistRegExp, '')
4895
+ .replace(this.editorTagsBlacklistRegExp, '');
4694
4896
  },
4695
4897
 
4696
4898
  /**
@@ -4705,18 +4907,28 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4705
4907
  if (/^<[a-z0-9]+\:[a-z0-9]+/i.test(m)) return m;
4706
4908
 
4707
4909
  let v = null;
4708
- const tAttr = this._attributesTagsWhitelist[t.match(/(?!<)[a-zA-Z0-9\-]+/)[0].toLowerCase()];
4709
- if (tAttr) v = m.match(tAttr);
4910
+ const tagName = t.match(/(?!<)[a-zA-Z0-9\-]+/)[0].toLowerCase();
4911
+
4912
+ // blacklist
4913
+ const bAttr = this._attributesTagsBlacklist[tagName];
4914
+ if (bAttr) m = m.replace(bAttr, '');
4915
+ else m = m.replace(this._attributesBlacklistRegExp, '');
4916
+
4917
+ // whitelist
4918
+ const wAttr = this._attributesTagsWhitelist[tagName];
4919
+ if (wAttr) v = m.match(wAttr);
4710
4920
  else v = m.match(this._attributesWhitelistRegExp);
4711
4921
 
4922
+ // anchor
4712
4923
  if (!lowLevelCheck || /<a\b/i.test(t)) {
4713
- const sv = m.match(/id\s*=\s*(?:"|')[^"']*(?:"|')/);
4924
+ const sv = m.match(/(?:(?:id|name)\s*=\s*(?:"|')[^"']*(?:"|'))/g);
4714
4925
  if (sv) {
4715
4926
  if (!v) v = [];
4716
4927
  v.push(sv[0]);
4717
4928
  }
4718
4929
  }
4719
4930
 
4931
+ // span
4720
4932
  if ((!lowLevelCheck || /<span/i.test(t)) && (!v || !/style=/i.test(v.toString()))) {
4721
4933
  const sv = m.match(/style\s*=\s*(?:"|')[^"']*(?:"|')/);
4722
4934
  if (sv) {
@@ -4725,6 +4937,29 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4725
4937
  }
4726
4938
  }
4727
4939
 
4940
+ // img
4941
+ if (/<img/i.test(t)) {
4942
+ let w = '', h = '';
4943
+ const sv = m.match(/style\s*=\s*(?:"|')[^"']*(?:"|')/);
4944
+ if (!v) v = [];
4945
+ if (sv) {
4946
+ w = sv[0].match(/width:(.+);/);
4947
+ w = util.getNumber(w ? w[1] : '', -1) || '';
4948
+ h = sv[0].match(/height:(.+);/);
4949
+ h = util.getNumber(h ? h[1] : '', -1) || '';
4950
+ }
4951
+
4952
+ if (!w || !h) {
4953
+ const avw = m.match(/width\s*=\s*((?:"|')[^"']*(?:"|'))/);
4954
+ const avh = m.match(/height\s*=\s*((?:"|')[^"']*(?:"|'))/);
4955
+ if (avw || avh) {
4956
+ w = !w ? util.getNumber(avw ? avw[1] : '') || '' : w;
4957
+ h = !h ? util.getNumber(avh ? avh[1] : '') || '' : h;
4958
+ }
4959
+ }
4960
+ v.push('data-origin="' + (w + ',' + h) + '"');
4961
+ }
4962
+
4728
4963
  if (v) {
4729
4964
  for (let i = 0, len = v.length; i < len; i++) {
4730
4965
  if (lowLevelCheck && /^class="(?!(__se__|se-|katex))/.test(v[i])) continue;
@@ -4740,14 +4975,16 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4740
4975
  * @param {String} html HTML string
4741
4976
  * @param {String|RegExp|null} whitelist Regular expression of allowed tags.
4742
4977
  * RegExp object is create by util.createTagsWhitelist method. (core.pasteTagsWhitelistRegExp)
4978
+ * @param {String|RegExp|null} blacklist Regular expression of disallowed tags.
4979
+ * RegExp object is create by util.createTagsBlacklist method. (core.pasteTagsBlacklistRegExp)
4743
4980
  * @returns {String}
4744
4981
  */
4745
- cleanHTML: function (html, whitelist) {
4982
+ cleanHTML: function (html, whitelist, blacklist) {
4746
4983
  html = this._deleteDisallowedTags(this._parser.parseFromString(html, 'text/html').body.innerHTML).replace(/(<[a-zA-Z0-9\-]+)[^>]*(?=>)/g, this._cleanTags.bind(this, true));
4747
4984
 
4748
4985
  const dom = _d.createRange().createContextualFragment(html);
4749
4986
  try {
4750
- util._consistencyCheckOfHTML(dom, this._htmlCheckWhitelistRegExp, true);
4987
+ util._consistencyCheckOfHTML(dom, this._htmlCheckWhitelistRegExp, this._htmlCheckBlacklistRegExp, true);
4751
4988
  } catch (error) {
4752
4989
  console.warn('[SUNEDITOR.cleanHTML.consistencyCheck.fail] ' + error);
4753
4990
  }
@@ -4783,7 +5020,14 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4783
5020
  }
4784
5021
 
4785
5022
  cleanHTML = util.htmlRemoveWhiteSpace(cleanHTML);
4786
- return this._tagConvertor(!cleanHTML ? html : !whitelist ? cleanHTML : cleanHTML.replace(typeof whitelist === 'string' ? util.createTagsWhitelist(whitelist) : whitelist, ''));
5023
+ if (!cleanHTML) {
5024
+ cleanHTML = html;
5025
+ } else {
5026
+ if (whitelist) cleanHTML = cleanHTML.replace(typeof whitelist === 'string' ? util.createTagsWhitelist(whitelist) : whitelist, '');
5027
+ if (blacklist) cleanHTML = cleanHTML.replace(typeof blacklist === 'string' ? util.createTagsBlacklist(blacklist) : blacklist, '');
5028
+ }
5029
+
5030
+ return this._tagConvertor(cleanHTML);
4787
5031
  },
4788
5032
 
4789
5033
  /**
@@ -4796,7 +5040,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4796
5040
  const dom = _d.createRange().createContextualFragment(contents);
4797
5041
 
4798
5042
  try {
4799
- util._consistencyCheckOfHTML(dom, this._htmlCheckWhitelistRegExp, false);
5043
+ util._consistencyCheckOfHTML(dom, this._htmlCheckWhitelistRegExp, this._htmlCheckBlacklistRegExp, false);
4800
5044
  } catch (error) {
4801
5045
  console.warn('[SUNEDITOR.convertContentsForEditor.consistencyCheck.fail] ' + error);
4802
5046
  }
@@ -4817,8 +5061,20 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4817
5061
 
4818
5062
  const domTree = dom.childNodes;
4819
5063
  let cleanHTML = '';
4820
- for (let i = 0, len = domTree.length; i < len; i++) {
4821
- cleanHTML += this._makeLine(domTree[i], true);
5064
+ for (let i = 0, t, p; i < domTree.length; i++) {
5065
+ t = domTree[i];
5066
+ if (!util.isFormatElement(t) && !util.isComponent(t) && !util.isMedia(t)) {
5067
+ if (!p) p = util.createElement(options.defaultTag);
5068
+ p.appendChild(t);
5069
+ i--;
5070
+ if (domTree[i + 1] && !util.isFormatElement(domTree[i + 1])) {
5071
+ continue;
5072
+ } else {
5073
+ t = p;
5074
+ p = null;
5075
+ }
5076
+ }
5077
+ cleanHTML += this._makeLine(t, true);
4822
5078
  }
4823
5079
 
4824
5080
  if (cleanHTML.length === 0) return '<' + options.defaultTag + '><br></' + options.defaultTag + '>';
@@ -4830,28 +5086,30 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4830
5086
  /**
4831
5087
  * @description Converts wysiwyg area element into a format that can be placed in an editor of code view mode
4832
5088
  * @param {Element|String} html WYSIWYG element (context.element.wysiwyg) or HTML string.
5089
+ * @param {Boolean} comp If true, does not line break and indentation of tags.
4833
5090
  * @returns {String}
4834
5091
  */
4835
- convertHTMLForCodeView: function (html) {
5092
+ convertHTMLForCodeView: function (html, comp) {
4836
5093
  let returnHTML = '';
4837
5094
  const wRegExp = _w.RegExp;
4838
5095
  const brReg = new wRegExp('^(BLOCKQUOTE|PRE|TABLE|THEAD|TBODY|TR|TH|TD|OL|UL|IMG|IFRAME|VIDEO|AUDIO|FIGURE|FIGCAPTION|HR|BR|CANVAS|SELECT)$', 'i');
4839
5096
  const wDoc = typeof html === 'string' ? _d.createRange().createContextualFragment(html) : html;
4840
5097
  const isFormat = function (current) { return this.isFormatElement(current) || this.isComponent(current); }.bind(util);
5098
+ const brChar = comp ? '' : '\n';
4841
5099
 
4842
- let indentSize = this._variable.codeIndent * 1;
5100
+ let indentSize = comp ? 0 : this._variable.codeIndent * 1;
4843
5101
  indentSize = indentSize > 0 ? new _w.Array(indentSize + 1).join(' ') : '';
4844
5102
 
4845
- (function recursionFunc (element, indent, lineBR) {
5103
+ (function recursionFunc (element, indent) {
4846
5104
  const children = element.childNodes;
4847
5105
  const elementRegTest = brReg.test(element.nodeName);
4848
5106
  const elementIndent = (elementRegTest ? indent : '');
4849
5107
 
4850
- for (let i = 0, len = children.length, node, br, nodeRegTest, tag, tagIndent; i < len; i++) {
5108
+ for (let i = 0, len = children.length, node, br, lineBR, nodeRegTest, tag, tagIndent; i < len; i++) {
4851
5109
  node = children[i];
4852
5110
  nodeRegTest = brReg.test(node.nodeName);
4853
- br = nodeRegTest ? '\n' : '';
4854
- lineBR = isFormat(node) && !elementRegTest && !/^(TH|TD)$/i.test(element.nodeName) ? '\n' : '';
5111
+ br = nodeRegTest ? brChar : '';
5112
+ lineBR = isFormat(node) && !elementRegTest && !/^(TH|TD)$/i.test(element.nodeName) ? brChar : '';
4855
5113
 
4856
5114
  if (node.nodeType === 8) {
4857
5115
  returnHTML += '\n<!-- ' + node.textContent.trim() + ' -->' + br;
@@ -4862,7 +5120,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4862
5120
  continue;
4863
5121
  }
4864
5122
  if (node.childNodes.length === 0) {
4865
- returnHTML += (/^HR$/i.test(node.nodeName) ? '\n' : '') + (/^PRE$/i.test(node.parentElement.nodeName) && /^BR$/i.test(node.nodeName) ? '' : elementIndent) + node.outerHTML + br;
5123
+ returnHTML += (/^HR$/i.test(node.nodeName) ? brChar : '') + (/^PRE$/i.test(node.parentElement.nodeName) && /^BR$/i.test(node.nodeName) ? '' : elementIndent) + node.outerHTML + br;
4866
5124
  continue;
4867
5125
  }
4868
5126
 
@@ -4873,12 +5131,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
4873
5131
  tagIndent = elementIndent || nodeRegTest ? indent : '';
4874
5132
  returnHTML += (lineBR || (elementRegTest ? '' : br)) + tagIndent + node.outerHTML.match(wRegExp('<' + tag + '[^>]*>', 'i'))[0] + br;
4875
5133
  recursionFunc(node, indent + indentSize, '');
4876
- returnHTML += (/\n$/.test(returnHTML) ? tagIndent : '') + '</' + tag + '>' + (lineBR || br || elementRegTest ? '\n' : '' || /^(TH|TD)$/i.test(node.nodeName) ? '\n' : '');
5134
+ returnHTML += (/\n$/.test(returnHTML) ? tagIndent : '') + '</' + tag + '>' + (lineBR || br || elementRegTest ? brChar : '' || /^(TH|TD)$/i.test(node.nodeName) ? brChar : '');
4877
5135
  }
4878
5136
  }
4879
- }(wDoc, '', '\n'));
5137
+ }(wDoc, ''));
4880
5138
 
4881
- return returnHTML.trim() + '\n';
5139
+ return returnHTML.trim() + brChar;
4882
5140
  },
4883
5141
 
4884
5142
  /**
@@ -5058,9 +5316,9 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5058
5316
  if (activePlugins.indexOf(key) > -1) {
5059
5317
  plugins[key].active.call(this, null);
5060
5318
  } else if (commandMap.OUTDENT && /^OUTDENT$/i.test(key)) {
5061
- if (!this.isReadOnly) commandMap.OUTDENT.setAttribute('disabled', true);
5319
+ if (!util.isImportantDisabled(commandMap.OUTDENT)) commandMap.OUTDENT.setAttribute('disabled', true);
5062
5320
  } else if (commandMap.INDENT && /^INDENT$/i.test(key)) {
5063
- if (!this.isReadOnly) commandMap.INDENT.removeAttribute('disabled');
5321
+ if (!util.isImportantDisabled(commandMap.INDENT)) commandMap.INDENT.removeAttribute('disabled');
5064
5322
  } else {
5065
5323
  util.removeClass(commandMap[key], 'active');
5066
5324
  }
@@ -5078,6 +5336,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5078
5336
  this._ww = options.iframe ? context.element.wysiwygFrame.contentWindow : _w;
5079
5337
  this._wd = _d;
5080
5338
  this._charTypeHTML = options.charCounterType === 'byte-html';
5339
+ this.wwComputedStyle = _w.getComputedStyle(context.element.wysiwyg);
5081
5340
 
5082
5341
  if (!options.iframe && typeof _w.ShadowRoot === 'function') {
5083
5342
  let child = context.element.wysiwygFrame;
@@ -5103,30 +5362,56 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5103
5362
  this._disallowedTextTagsRegExp = disallowTextTags.length === 0 ? null : new wRegExp('(<\\/?)(' + disallowTextTags.join('|') + ')\\b\\s*([^>^<]+)?\\s*(?=>)', 'gi');
5104
5363
 
5105
5364
  // set whitelist
5365
+ const getRegList = function (str, str2) { return !str ? '^' : (str === '*' ? '[a-z-]+' : (!str2 ? str : (str + '|' + str2))); };
5366
+ // tags
5106
5367
  const defaultAttr = 'contenteditable|colspan|rowspan|target|href|download|rel|src|alt|class|type|controls|data-format|data-size|data-file-size|data-file-name|data-origin|data-align|data-image-link|data-rotate|data-proportion|data-percentage|origin-size|data-exp|data-font-size';
5107
- this._allowHTMLComments = options._editorTagsWhitelist.indexOf('//') > -1;
5108
- this._htmlCheckWhitelistRegExp = new wRegExp('^(' + options._editorTagsWhitelist.replace('|//', '') + ')$', 'i');
5109
- this.editorTagsWhitelistRegExp = util.createTagsWhitelist(options._editorTagsWhitelist.replace('|//', '|<!--|-->'));
5110
- this.pasteTagsWhitelistRegExp = util.createTagsWhitelist(options.pasteTagsWhitelist);
5111
-
5368
+ this._allowHTMLComments = options._editorTagsWhitelist.indexOf('//') > -1 || options._editorTagsWhitelist === '*';
5369
+ // html check
5370
+ this._htmlCheckWhitelistRegExp = new wRegExp('^(' + getRegList(options._editorTagsWhitelist.replace('|//', ''), '') + ')$', 'i');
5371
+ this._htmlCheckBlacklistRegExp = new wRegExp('^(' + (options.tagsBlacklist || '^') + ')$', 'i');
5372
+ // tags
5373
+ this.editorTagsWhitelistRegExp = util.createTagsWhitelist(getRegList(options._editorTagsWhitelist.replace('|//', '|<!--|-->'), ''));
5374
+ this.editorTagsBlacklistRegExp = util.createTagsBlacklist(options.tagsBlacklist.replace('|//', '|<!--|-->'));
5375
+ // paste tags
5376
+ this.pasteTagsWhitelistRegExp = util.createTagsWhitelist(getRegList(options.pasteTagsWhitelist, ''));
5377
+ this.pasteTagsBlacklistRegExp = util.createTagsBlacklist(options.pasteTagsBlacklist);
5378
+ // attributes
5112
5379
  const regEndStr = '\\s*=\\s*(\")[^\"]*\\1';
5113
- const _attr = options.attributesWhitelist;
5114
- const tagsAttr = {};
5380
+ const _wAttr = options.attributesWhitelist;
5381
+ let tagsAttr = {};
5115
5382
  let allAttr = '';
5116
- if (!!_attr) {
5117
- for (let k in _attr) {
5118
- if (!util.hasOwn(_attr, k) || /^on[a-z]+$/i.test(_attr[k])) continue;
5383
+ if (!!_wAttr) {
5384
+ for (let k in _wAttr) {
5385
+ if (!util.hasOwn(_wAttr, k) || /^on[a-z]+$/i.test(_wAttr[k])) continue;
5119
5386
  if (k === 'all') {
5120
- allAttr = _attr[k] + '|';
5387
+ allAttr = getRegList(_wAttr[k], defaultAttr);
5121
5388
  } else {
5122
- tagsAttr[k] = new wRegExp('(?:' + _attr[k] + '|' + defaultAttr + ')' + regEndStr, 'ig');
5389
+ tagsAttr[k] = new wRegExp('\\s(?:' + getRegList(_wAttr[k], '') + ')' + regEndStr, 'ig');
5123
5390
  }
5124
5391
  }
5125
5392
  }
5126
-
5127
- this._attributesWhitelistRegExp = new wRegExp('(?:' + allAttr + defaultAttr + ')' + regEndStr, 'ig');
5393
+
5394
+ this._attributesWhitelistRegExp = new wRegExp('\\s(?:' + (allAttr || defaultAttr) + ')' + regEndStr, 'ig');
5128
5395
  this._attributesTagsWhitelist = tagsAttr;
5129
5396
 
5397
+ // blacklist
5398
+ const _bAttr = options.attributesBlacklist;
5399
+ tagsAttr = {};
5400
+ allAttr = '';
5401
+ if (!!_bAttr) {
5402
+ for (let k in _bAttr) {
5403
+ if (!util.hasOwn(_bAttr, k)) continue;
5404
+ if (k === 'all') {
5405
+ allAttr = getRegList(_bAttr[k], '');
5406
+ } else {
5407
+ tagsAttr[k] = new wRegExp('\\s(?:' + getRegList(_bAttr[k], '') + ')' + regEndStr, 'ig');
5408
+ }
5409
+ }
5410
+ }
5411
+
5412
+ this._attributesBlacklistRegExp = new wRegExp('\\s(?:' + (allAttr || '^') + ')' + regEndStr, 'ig');
5413
+ this._attributesTagsBlacklist = tagsAttr;
5414
+
5130
5415
  // set modes
5131
5416
  this._isInline = /inline/i.test(options.mode);
5132
5417
  this._isBalloon = /balloon|balloon-always/i.test(options.mode);
@@ -5204,6 +5489,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5204
5489
  }
5205
5490
 
5206
5491
  this._initWysiwygArea(reload, _initHTML);
5492
+ this.setDir(options.rtl ? 'rtl' : 'ltr');
5207
5493
  },
5208
5494
 
5209
5495
  /**
@@ -5211,9 +5497,11 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5211
5497
  * @private
5212
5498
  */
5213
5499
  _cachingButtons: function () {
5214
- this.codeViewDisabledButtons = context.element._buttonTray.querySelectorAll('.se-menu-list button[data-display]:not([class~="se-code-view-enabled"])');
5500
+ this.codeViewDisabledButtons = context.element._buttonTray.querySelectorAll('.se-menu-list button[data-display]:not([class~="se-code-view-enabled"]):not([data-display="MORE"])');
5215
5501
  this.resizingDisabledButtons = context.element._buttonTray.querySelectorAll('.se-menu-list button[data-display]:not([class~="se-resizing-enabled"]):not([data-display="MORE"])');
5216
5502
 
5503
+ this.saveButtonStates();
5504
+
5217
5505
  const tool = context.tool;
5218
5506
  this.commandMap = {
5219
5507
  SUB: tool.subscript,
@@ -5311,7 +5599,15 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5311
5599
  let focusNode, offset, format;
5312
5600
 
5313
5601
  const fileComponent = util.getParentElement(commonCon, util.isComponent);
5314
- if (fileComponent && !util.isTable(fileComponent)) return;
5602
+ if (fileComponent && !util.isTable(fileComponent)) {
5603
+ return;
5604
+ } else if (commonCon.nodeType === 1 && commonCon.getAttribute('data-se-embed') === 'true') {
5605
+ let el = commonCon.nextElementSibling;
5606
+ if (!util.isFormatElement(el)) el = this.appendFormatTag(commonCon, options.defaultTag);
5607
+ this.setRange(el.firstChild, 0, el.firstChild, 0);
5608
+ return;
5609
+ }
5610
+
5315
5611
  if ((util.isRangeFormatElement(startCon) || util.isWysiwygDiv(startCon)) && (util.isComponent(startCon.children[range.startOffset]) || util.isComponent(startCon.children[range.startOffset - 1]))) return;
5316
5612
  if (util.getParentElement(commonCon, util.isNotCheckingNode)) return null;
5317
5613
 
@@ -5566,9 +5862,9 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5566
5862
  }
5567
5863
  }
5568
5864
 
5569
- if (!core.isReadOnly && util.isFormatElement(element)) {
5865
+ if (util.isFormatElement(element)) {
5570
5866
  /* Outdent */
5571
- if (commandMapNodes.indexOf('OUTDENT') === -1 && commandMap.OUTDENT) {
5867
+ if (commandMapNodes.indexOf('OUTDENT') === -1 && commandMap.OUTDENT && !util.isImportantDisabled(commandMap.OUTDENT)) {
5572
5868
  if (util.isListCell(element) || (element.style[marginDir] && util.getNumber(element.style[marginDir], 0) > 0)) {
5573
5869
  commandMapNodes.push('OUTDENT');
5574
5870
  commandMap.OUTDENT.removeAttribute('disabled');
@@ -5576,7 +5872,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5576
5872
  }
5577
5873
 
5578
5874
  /* Indent */
5579
- if (commandMapNodes.indexOf('INDENT') === -1 && commandMap.INDENT) {
5875
+ if (commandMapNodes.indexOf('INDENT') === -1 && commandMap.INDENT && !util.isImportantDisabled(commandMap.INDENT)) {
5580
5876
  commandMapNodes.push('INDENT');
5581
5877
  if (util.isListCell(element) && !element.previousElementSibling) {
5582
5878
  commandMap.INDENT.setAttribute('disabled', true);
@@ -5663,6 +5959,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
5663
5959
 
5664
5960
  onMouseDown_wysiwyg: function (e) {
5665
5961
  if (core.isReadOnly || util.isNonEditable(context.element.wysiwyg)) return;
5962
+ core._editorRange();
5666
5963
 
5667
5964
  // user event
5668
5965
  if (typeof functions.onMouseDown === 'function' && functions.onMouseDown(e, core) === false) return;
@@ -6497,6 +6794,44 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
6497
6794
 
6498
6795
  temp = !temp ? newFormat.firstChild : temp.appendChild(newFormat.firstChild);
6499
6796
  core.setRange(temp, 0, temp, 0);
6797
+ break;
6798
+ } else if (options.lineAttrReset && formatEl) {
6799
+ e.preventDefault();
6800
+ e.stopPropagation();
6801
+
6802
+ let newEl;
6803
+ if (!range.collapsed) {
6804
+ const isMultiLine = util.getFormatElement(range.startContainer, null) !== util.getFormatElement(range.endContainer, null);
6805
+ const r = core.removeNode();
6806
+ if (isMultiLine) {
6807
+ newEl = util.getFormatElement(r.container, null);
6808
+
6809
+ if (!r.prevContainer) {
6810
+ const newFormat = formatEl.cloneNode(false);
6811
+ newFormat.innerHTML = '<br>';
6812
+ newEl.parentNode.insertBefore(newFormat, newEl);
6813
+ } else if (newEl !== formatEl && newEl.nextElementSibling === formatEl) {
6814
+ newEl = formatEl;
6815
+ }
6816
+ } else {
6817
+ newEl = util.splitElement(r.container, r.offset, 0);
6818
+ }
6819
+ } else {
6820
+ newEl = util.splitElement(range.endContainer, range.endOffset, 0);
6821
+ }
6822
+
6823
+ const resetAttr = options.lineAttrReset === '*' ? null : options.lineAttrReset;
6824
+ const attrs = newEl.attributes;
6825
+ let i = 0;
6826
+ while (attrs[i]) {
6827
+ if (resetAttr && resetAttr.test(attrs[i].name)) {
6828
+ i++;
6829
+ continue;
6830
+ }
6831
+ newEl.removeAttribute(attrs[i].name);
6832
+ }
6833
+ core.setRange(newEl.firstChild, 0, newEl.firstChild, 0);
6834
+
6500
6835
  break;
6501
6836
  }
6502
6837
 
@@ -6918,6 +7253,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
6918
7253
  },
6919
7254
 
6920
7255
  _codeViewAutoHeight: function () {
7256
+ if (core._variable.isFullScreen) return;
6921
7257
  context.element.code.style.height = context.element.code.scrollHeight + 'px';
6922
7258
  },
6923
7259
 
@@ -6987,6 +7323,14 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
6987
7323
  }
6988
7324
  },
6989
7325
 
7326
+ onSave_wysiwyg: function (content) {
7327
+ // user event
7328
+ if (typeof functions.onSave === 'function') {
7329
+ functions.onSave(content, core);
7330
+ return;
7331
+ }
7332
+ },
7333
+
6990
7334
  onCut_wysiwyg: function (e) {
6991
7335
  const clipboardData = util.isIE ? _w.clipboardData : e.clipboardData;
6992
7336
 
@@ -7088,7 +7432,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7088
7432
  } else {
7089
7433
  cleanData = (plainText === cleanData ? plainText : cleanData).replace(/\n/g, '<br>');
7090
7434
  }
7091
- cleanData = core.cleanHTML(cleanData, core.pasteTagsWhitelistRegExp);
7435
+ cleanData = core.cleanHTML(cleanData, core.pasteTagsWhitelistRegExp, core.pasteTagsBlacklistRegExp);
7092
7436
  } else {
7093
7437
  cleanData = util._HTMLConvertor(plainText).replace(/\n/g, '<br>');
7094
7438
  }
@@ -7239,7 +7583,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7239
7583
 
7240
7584
  /** resizingBar */
7241
7585
  if (context.element.resizingBar) {
7242
- if (/\d+/.test(options.height)) {
7586
+ if (/\d+/.test(options.height) && options.resizeEnable) {
7243
7587
  context.element.resizingBar.addEventListener('mousedown', event.onMouseDown_resizingBar, false);
7244
7588
  } else {
7245
7589
  util.addClass(context.element.resizingBar, 'se-resizing-none');
@@ -7351,6 +7695,13 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7351
7695
  */
7352
7696
  onChange: null,
7353
7697
 
7698
+ /**
7699
+ * @description Event functions
7700
+ * @param {String} contents Current contents
7701
+ * @param {Object} core Core object
7702
+ */
7703
+ onSave: null,
7704
+
7354
7705
  /**
7355
7706
  * @description Event functions (drop, paste)
7356
7707
  * When false is returned, the default behavior is stopped.
@@ -7569,6 +7920,14 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7569
7920
  */
7570
7921
  onResizeEditor: null,
7571
7922
 
7923
+ /**
7924
+ * @description Called after the "setToolbarButtons" invocation.
7925
+ * Can be used to tweak buttons properties (useful for custom buttons)
7926
+ * @param {Array} buttonList Button list
7927
+ * @param {Object} core Core object
7928
+ */
7929
+ onSetToolbarButtons: null,
7930
+
7572
7931
  /**
7573
7932
  * @description Reset the buttons on the toolbar. (Editor is not reloaded)
7574
7933
  * You cannot set a new plugin for the button.
@@ -7577,10 +7936,10 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7577
7936
  setToolbarButtons: function (buttonList) {
7578
7937
  core.submenuOff();
7579
7938
  core.containerOff();
7939
+ core.moreLayerOff();
7580
7940
 
7581
7941
  const newToolbar = _Constructor._createToolBar(_d, buttonList, core.plugins, options);
7582
7942
  _responsiveButtons = newToolbar.responsiveButtons;
7583
- core._moreLayerActiveButton = null;
7584
7943
  event._setResponsiveToolbar();
7585
7944
 
7586
7945
  context.element.toolbar.replaceChild(newToolbar._buttonTray, context.element._buttonTray);
@@ -7589,32 +7948,15 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7589
7948
  context.element = newContext.element;
7590
7949
  context.tool = newContext.tool;
7591
7950
  if (options.iframe) context.element.wysiwyg = core._wd.body;
7951
+
7952
+ core.recoverButtonStates();
7953
+
7592
7954
  core._cachingButtons();
7593
7955
  core.history._resetCachingButton();
7594
7956
 
7595
- core.activePlugins = [];
7596
- const oldCallButtons = pluginCallButtons;
7597
- pluginCallButtons = newToolbar.pluginCallButtons;
7598
- let plugin, button, oldButton;
7599
- for (let key in pluginCallButtons) {
7600
- if (!util.hasOwn(pluginCallButtons, key)) continue;
7601
- plugin = plugins[key];
7602
- button = pluginCallButtons[key];
7603
- if (plugin.active && button) {
7604
- oldButton = oldCallButtons[key];
7605
- core.callPlugin(key, null, oldButton || button);
7606
- if (oldButton) {
7607
- button.parentElement.replaceChild(oldButton, button);
7608
- pluginCallButtons[key] = oldButton;
7609
- }
7610
- }
7611
- }
7612
-
7613
7957
  if (core.hasFocus) event._applyTagEffects();
7614
-
7615
- if (core._variable.isCodeView) util.addClass(core._styleCommandMap.codeView, 'active');
7616
- if (core._variable.isFullScreen) util.addClass(core._styleCommandMap.fullScreen, 'active');
7617
- if (util.hasClass(context.element.wysiwyg, 'se-show-block')) util.addClass(core._styleCommandMap.showBlocks, 'active');
7958
+ if (core.isReadOnly) util.setDisabledButtons(true, core.resizingDisabledButtons);
7959
+ if (typeof functions.onSetToolbarButtons === 'function') functions.onSetToolbarButtons(newToolbar._buttonTray.querySelectorAll('button'), core);
7618
7960
  },
7619
7961
 
7620
7962
  /**
@@ -7650,7 +7992,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7650
7992
  const _initHTML = el.wysiwyg.innerHTML;
7651
7993
 
7652
7994
  // set option
7653
- const cons = _Constructor._setOptions(mergeOptions, context, options);
7995
+ const cons = _Constructor._setOptions(mergeOptions, context, options);
7654
7996
 
7655
7997
  if (cons.callButtons) {
7656
7998
  pluginCallButtons = cons.callButtons;
@@ -7726,10 +8068,12 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7726
8068
  },
7727
8069
 
7728
8070
  /**
7729
- * @description Copying the contents of the editor to the original textarea
8071
+ * @description Copying the contents of the editor to the original textarea and execute onSave callback
7730
8072
  */
7731
8073
  save: function () {
7732
- context.element.originElement.value = core.getContents(false);
8074
+ const contents = core.getContents(false);
8075
+ context.element.originElement.value = contents;
8076
+ event.onSave_wysiwyg(contents, core);
7733
8077
  },
7734
8078
 
7735
8079
  /**
@@ -7823,7 +8167,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7823
8167
  */
7824
8168
  insertHTML: function (html, notCleaningData, checkCharCount, rangeSelection) {
7825
8169
  if (typeof html === 'string') {
7826
- if (!notCleaningData) html = core.cleanHTML(html, null);
8170
+ if (!notCleaningData) html = core.cleanHTML(html, null, null);
7827
8171
  try {
7828
8172
  const dom = _d.createRange().createContextualFragment(html);
7829
8173
  const domTree = dom.childNodes;
@@ -7903,7 +8247,7 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7903
8247
  wysiwyg.appendChild(children[i]);
7904
8248
  }
7905
8249
  } else {
7906
- core._setCodeView(core._getCodeView() + '\n' + core.convertHTMLForCodeView(convertValue));
8250
+ core._setCodeView(core._getCodeView() + '\n' + core.convertHTMLForCodeView(convertValue, false));
7907
8251
  }
7908
8252
 
7909
8253
  // history stack
@@ -7917,44 +8261,52 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
7917
8261
  readOnly: function (value) {
7918
8262
  core.isReadOnly = value;
7919
8263
 
8264
+ util.setDisabledButtons(!!value, core.resizingDisabledButtons);
8265
+
7920
8266
  if (value) {
8267
+ /** off menus */
8268
+ core.controllersOff();
8269
+ if (core.submenuActiveButton && core.submenuActiveButton.disabled) core.submenuOff();
8270
+ if (core._moreLayerActiveButton && core._moreLayerActiveButton.disabled) core.moreLayerOff();
8271
+ if (core.containerActiveButton && core.containerActiveButton.disabled) core.containerOff();
8272
+ if (core.modalForm) core.plugins.dialog.close.call(core);
8273
+
7921
8274
  context.element.code.setAttribute("readOnly", "true");
7922
8275
  } else {
7923
8276
  context.element.code.removeAttribute("readOnly");
7924
8277
  }
7925
8278
 
7926
- util.setDisabledButtons(!!value, core.resizingDisabledButtons);
7927
8279
  if (options.codeMirrorEditor) options.codeMirrorEditor.setOption('readOnly', !!value);
7928
8280
  },
7929
8281
 
7930
8282
  /**
7931
8283
  * @description Disable the suneditor
7932
8284
  */
7933
- disabled: function () {
7934
- context.tool.cover.style.display = 'block';
7935
- context.element.wysiwyg.setAttribute('contenteditable', false);
7936
- core.isDisabled = true;
8285
+ disable: function () {
8286
+ this.toolbar.disable();
8287
+ this.wysiwyg.disable();
8288
+ },
7937
8289
 
7938
- if (options.codeMirrorEditor) {
7939
- options.codeMirrorEditor.setOption('readOnly', true);
7940
- } else {
7941
- context.element.code.setAttribute('disabled', 'disabled');
7942
- }
8290
+ /**
8291
+ * @description Provided for backward compatibility and will be removed in 3.0.0 version
8292
+ */
8293
+ disabled: function () {
8294
+ this.disable();
7943
8295
  },
7944
8296
 
7945
8297
  /**
7946
8298
  * @description Enable the suneditor
7947
8299
  */
7948
- enabled: function () {
7949
- context.tool.cover.style.display = 'none';
7950
- context.element.wysiwyg.setAttribute('contenteditable', true);
7951
- core.isDisabled = false;
8300
+ enable: function () {
8301
+ this.toolbar.enable();
8302
+ this.wysiwyg.enable();
8303
+ },
7952
8304
 
7953
- if (options.codeMirrorEditor) {
7954
- options.codeMirrorEditor.setOption('readOnly', false);
7955
- } else {
7956
- context.element.code.removeAttribute('disabled');
7957
- }
8305
+ /**
8306
+ * @description Provided for backward compatibility and will be removed in 3.0.0 version
8307
+ */
8308
+ enabled: function () {
8309
+ this.enable();
7958
8310
  },
7959
8311
 
7960
8312
  /**
@@ -8011,17 +8363,36 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
8011
8363
  /**
8012
8364
  * @description Disable the toolbar
8013
8365
  */
8014
- disabled: function () {
8366
+ disable: function () {
8367
+ /** off menus */
8368
+ core.submenuOff();
8369
+ core.moreLayerOff();
8370
+ core.containerOff();
8371
+
8015
8372
  context.tool.cover.style.display = 'block';
8016
8373
  },
8017
8374
 
8375
+ /**
8376
+ * @description Provided for backward compatibility and will be removed in 3.0.0 version
8377
+ */
8378
+ disabled: function () {
8379
+ this.disable();
8380
+ },
8381
+
8018
8382
  /**
8019
8383
  * @description Enable the toolbar
8020
8384
  */
8021
- enabled: function () {
8385
+ enable: function () {
8022
8386
  context.tool.cover.style.display = 'none';
8023
8387
  },
8024
8388
 
8389
+ /**
8390
+ * @description Provided for backward compatibility and will be removed in 3.0.0 version
8391
+ */
8392
+ enabled: function () {
8393
+ this.enable();
8394
+ },
8395
+
8025
8396
  /**
8026
8397
  * @description Show the toolbar
8027
8398
  */
@@ -8045,7 +8416,44 @@ export default function (context, pluginCallButtons, plugins, lang, options, _re
8045
8416
  context.element._stickyDummy.style.display = 'none';
8046
8417
  }
8047
8418
  },
8048
- }
8419
+ },
8420
+
8421
+ /**
8422
+ * @description Wysiwyg methods
8423
+ */
8424
+ wysiwyg: {
8425
+ /**
8426
+ * @description Disable the wysiwyg area
8427
+ */
8428
+ disable: function () {
8429
+ /** off menus */
8430
+ core.controllersOff();
8431
+ if (core.modalForm) core.plugins.dialog.close.call(core);
8432
+
8433
+ context.element.wysiwyg.setAttribute('contenteditable', false);
8434
+ core.isDisabled = true;
8435
+
8436
+ if (options.codeMirrorEditor) {
8437
+ options.codeMirrorEditor.setOption('readOnly', true);
8438
+ } else {
8439
+ context.element.code.setAttribute('disabled', 'disabled');
8440
+ }
8441
+ },
8442
+
8443
+ /**
8444
+ * @description Enable the wysiwyg area
8445
+ */
8446
+ enable: function () {
8447
+ context.element.wysiwyg.setAttribute('contenteditable', true);
8448
+ core.isDisabled = false;
8449
+
8450
+ if (options.codeMirrorEditor) {
8451
+ options.codeMirrorEditor.setOption('readOnly', false);
8452
+ } else {
8453
+ context.element.code.removeAttribute('disabled');
8454
+ }
8455
+ },
8456
+ }
8049
8457
  };
8050
8458
 
8051
8459
  /************ Core init ************/