comfortable_mexican_sofa 1.12.8 → 1.12.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/app/assets/fonts/comfy/admin/cms/lib/redactor-font.eot +0 -0
  4. data/app/assets/javascripts/comfy/admin/cms/base.js.coffee +46 -11
  5. data/app/assets/javascripts/comfy/admin/cms/lib/redactor.js +1807 -570
  6. data/app/assets/stylesheets/comfy/admin/cms/bootstrap_overrides.sass +6 -1
  7. data/app/assets/stylesheets/comfy/admin/cms/lib/redactor.css +79 -52
  8. data/app/controllers/comfy/admin/cms/base_controller.rb +2 -1
  9. data/app/controllers/comfy/admin/cms/pages_controller.rb +4 -1
  10. data/app/controllers/comfy/cms/base_controller.rb +5 -5
  11. data/app/controllers/comfy/cms/content_controller.rb +4 -0
  12. data/app/helpers/comfy/cms_helper.rb +72 -70
  13. data/app/models/comfy/cms/file.rb +0 -1
  14. data/app/views/comfy/admin/cms/files/_file.html.haml +1 -1
  15. data/app/views/comfy/admin/cms/layouts/_index_branch.html.haml +2 -1
  16. data/config/environments/test.rb +2 -0
  17. data/config/initializers/comfortable_mexican_sofa.rb +12 -0
  18. data/config/locales/cs.yml +2 -3
  19. data/config/locales/da.yml +2 -3
  20. data/config/locales/de.yml +2 -3
  21. data/config/locales/en.yml +2 -3
  22. data/config/locales/es.yml +2 -3
  23. data/config/locales/fr.yml +2 -3
  24. data/config/locales/it.yml +2 -3
  25. data/config/locales/ja.yml +2 -3
  26. data/config/locales/nb.yml +2 -3
  27. data/config/locales/nl.yml +2 -3
  28. data/config/locales/pl.yml +40 -41
  29. data/config/locales/pt-BR.yml +2 -3
  30. data/config/locales/ru.yml +2 -3
  31. data/config/locales/sv.yml +2 -3
  32. data/config/locales/uk.yml +2 -3
  33. data/config/locales/zh-CN.yml +2 -3
  34. data/config/locales/zh-TW.yml +16 -17
  35. data/db/migrate/01_create_cms.rb +18 -25
  36. data/db/upgrade_migrations/08_upgrade_to_1_12_0.rb +2 -2
  37. data/doc/preview.png +0 -0
  38. data/lib/comfortable_mexican_sofa.rb +1 -0
  39. data/lib/comfortable_mexican_sofa/access_control/admin_authentication.rb +3 -2
  40. data/lib/comfortable_mexican_sofa/access_control/public_authorization.rb +8 -0
  41. data/lib/comfortable_mexican_sofa/configuration.rb +4 -0
  42. data/lib/comfortable_mexican_sofa/engine.rb +5 -1
  43. data/lib/comfortable_mexican_sofa/fixture/file.rb +22 -22
  44. data/lib/comfortable_mexican_sofa/tags/asset.rb +8 -3
  45. data/lib/comfortable_mexican_sofa/version.rb +1 -1
  46. data/test/controllers/comfy/admin/cms/pages_controller_test.rb +22 -0
  47. data/test/controllers/comfy/cms/content_controller_test.rb +12 -0
  48. data/test/integration/access_control_test.rb +24 -1
  49. data/test/lib/fixtures/files_test.rb +29 -27
  50. data/test/lib/tags/asset_test.rb +42 -9
  51. data/test/test_helper.rb +8 -3
  52. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f933c8018264bf9d49d604db850abedcde229e03
4
- data.tar.gz: 28b5de9bdd3d6db7accafdba10d4a1408ef6bb90
3
+ metadata.gz: 178be176b760152af88ef7cef977ec69828f3dbb
4
+ data.tar.gz: 08daec455e2fbad3665e8a5b2f900992c207a644
5
5
  SHA512:
6
- metadata.gz: b421cc43496c817bc03c01729b3f45147665420343ab92cfcf82166e66e8ebf86976e87f783e4de58797896a2eda17d6dc4c86e95fed30917c3e76e5ed8c1baf
7
- data.tar.gz: 2d31bfe29ac5c3bc8e61ef0794e9c3121440dce69ee5a1b0f71ad1c125889471479fed9057cbd5dbf4f51ebf110ac9d1667494a921f8ebf7b71f90c547b738d2
6
+ metadata.gz: 9a65fa0ac1accefe11d47a9694e18c2b6cd8064cd09249190d44663e1c81f7d78d310c57ae953c50555a6127df99b48d7039f4d077f57022e0c415c6ddeee58c
7
+ data.tar.gz: 8ce4ec49478f8691bdab4ced394f8eee45fbfbcc2d9deee7c83eaadff3fb03746436316fcdd4b237643deca222614d81b4ce350b6f17faa3226f814e0c95e851
@@ -6,10 +6,10 @@ before_script:
6
6
  script:
7
7
  - rake ci
8
8
  rvm:
9
- - 1.9.3
10
9
  - 2.0.0
11
- - 2.1.5
12
- - 2.2.1
10
+ - 2.1.8
11
+ - 2.2.4
12
+ - 2.3.0
13
13
  gemfile:
14
14
  - test/gemfiles/Gemfile.rails.4.0
15
15
  - test/gemfiles/Gemfile.rails.4.1
@@ -23,15 +23,50 @@ window.CMS.init = ->
23
23
 
24
24
  window.CMS.slugify = ->
25
25
  slugify = (str) ->
26
- str = str.replace(/^\s+|\s+$/g, '')
27
- from = "ÀÁÄÂÃÈÉËÊÌÍÏÎÒÓÖÔÕÙÚÜÛàáäâãèéëêìíïîòóöôõùúüûÑñÇç"
28
- to = "aaaaaeeeeiiiiooooouuuuaaaaaeeeeiiiiooooouuuunncc"
29
- for i in [0..from.length - 1]
30
- str = str.replace(new RegExp(from[i], "g"), to[i])
31
- chars_to_replace_with_delimiter = new RegExp('[·/,:;_]', 'g')
32
- str = str.replace(chars_to_replace_with_delimiter, '-')
33
- chars_to_remove = new RegExp('[^a-zA-Z0-9 -]', 'g')
34
- str = str.replace(chars_to_remove, '').replace(/\s+/g, '-').toLowerCase()
26
+ # Trim string and lower case.
27
+ str = str.replace(/^\s+|\s+$/g, '').toLowerCase()
28
+
29
+ # Replace special chars.
30
+ replacements = [
31
+ ['à', 'a'],
32
+ ['á', 'a'],
33
+ ['ä', 'ae'],
34
+ ['â', 'a'],
35
+ ['ã', 'a'],
36
+ ['è', 'e'],
37
+ ['é', 'e'],
38
+ ['ë', 'e'],
39
+ ['ê', 'e'],
40
+ ['ì', 'i'],
41
+ ['í', 'i'],
42
+ ['ï', 'i'],
43
+ ['î', 'i'],
44
+ ['ò', 'o'],
45
+ ['ó', 'o'],
46
+ ['ö', 'oe'],
47
+ ['ô', 'o'],
48
+ ['õ', 'o'],
49
+ ['ù', 'u'],
50
+ ['ú', 'u'],
51
+ ['ü', 'ue'],
52
+ ['û', 'u'],
53
+ ['ñ', 'n'],
54
+ ['ç', 'c'],
55
+ ['ß', 'ss'],
56
+ ['·', '-'],
57
+ ['/', '-'],
58
+ [',', '-'],
59
+ [':', '-'],
60
+ [';', '-'],
61
+ ['_', '-'],
62
+ [' ', '-'],
63
+ ]
64
+
65
+ for replacement in replacements
66
+ str = str.replace(new RegExp(replacement[0], 'g'), replacement[1])
67
+
68
+ # Remove any other URL incompatible characters and replace multiple dashes with just a single one.
69
+ str = str.replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-')
35
70
 
36
71
  $('input[data-slugify=true]').bind 'keyup.cms', ->
37
72
  $('input[data-slug=true]').val(slugify($(this).val()))
@@ -71,7 +106,7 @@ window.CMS.codemirror = ->
71
106
  cm.setSize($(@).width(), $(@).height())
72
107
  cm.refresh()
73
108
 
74
- $('a[data-toggle="tab"]').on 'shown', ->
109
+ $('a[data-toggle="tab"]').on 'shown.bs.tab', ->
75
110
  for cm in CMS.code_mirror_instances
76
111
  cm.refresh()
77
112
 
@@ -124,7 +159,7 @@ window.CMS.page_update_preview = ->
124
159
  $('input[name=commit]').click ->
125
160
  $(this).parents('form').attr('target', '')
126
161
  $('input[name=preview]').click ->
127
- $(this).parents('form').attr('target', '_blank')
162
+ $(this).parents('form').attr('target', 'comfy-cms-preview')
128
163
 
129
164
 
130
165
  window.CMS.page_update_publish = ->
@@ -1,6 +1,6 @@
1
1
  /*
2
- Redactor v10.0.7
3
- Updated: January 31, 2015
2
+ Redactor 10.2.5
3
+ Updated: October 1, 2015
4
4
 
5
5
  http://imperavi.com/redactor/
6
6
 
@@ -12,6 +12,7 @@
12
12
 
13
13
  (function($)
14
14
  {
15
+
15
16
  'use strict';
16
17
 
17
18
  if (!Function.prototype.bind)
@@ -28,9 +29,6 @@
28
29
 
29
30
  var uuid = 0;
30
31
 
31
- var reUrlYoutube = /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig;
32
- var reUrlVimeo = /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/;
33
-
34
32
  // Plugin
35
33
  $.fn.redactor = function(options)
36
34
  {
@@ -94,11 +92,11 @@
94
92
 
95
93
  // Functionality
96
94
  $.Redactor = Redactor;
97
- $.Redactor.VERSION = '10.0.7';
95
+ $.Redactor.VERSION = '10.2.5';
98
96
  $.Redactor.modules = ['alignment', 'autosave', 'block', 'buffer', 'build', 'button',
99
97
  'caret', 'clean', 'code', 'core', 'dropdown', 'file', 'focus',
100
98
  'image', 'indent', 'inline', 'insert', 'keydown', 'keyup',
101
- 'lang', 'line', 'link', 'list', 'modal', 'observe', 'paragraphize',
99
+ 'lang', 'line', 'link', 'linkify', 'list', 'modal', 'observe', 'paragraphize',
102
100
  'paste', 'placeholder', 'progress', 'selection', 'shortcuts',
103
101
  'tabifier', 'tidy', 'toolbar', 'upload', 'utils'];
104
102
 
@@ -135,6 +133,7 @@
135
133
  autosaveName: false,
136
134
  autosaveInterval: 60, // seconds
137
135
  autosaveOnChange: false,
136
+ autosaveFields: false,
138
137
 
139
138
  linkTooltip: true,
140
139
  linkProtocol: 'http',
@@ -191,12 +190,17 @@
191
190
 
192
191
  tabifier: true,
193
192
 
194
- deniedTags: ['html', 'head', 'link', 'body', 'meta', 'script', 'style', 'applet'],
193
+ deniedTags: ['script', 'style'],
195
194
  allowedTags: false, // or array
196
195
 
196
+ paragraphizeBlocks: ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption',
197
+ 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea',
198
+ 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p'],
199
+
197
200
  removeComments: false,
198
201
  replaceTags: [
199
- ['strike', 'del']
202
+ ['strike', 'del'],
203
+ ['b', 'strong']
200
204
  ],
201
205
  replaceStyles: [
202
206
  ['font-weight:\\s?bold', "strong"],
@@ -249,7 +253,10 @@
249
253
  inlineTags: ['strong', 'b', 'u', 'em', 'i', 'code', 'del', 'ins', 'samp', 'kbd', 'sup', 'sub', 'mark', 'var', 'cite', 'small'],
250
254
  alignmentTags: ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DL', 'DT', 'DD', 'DIV', 'TD', 'BLOCKQUOTE', 'OUTPUT', 'FIGCAPTION', 'ADDRESS', 'SECTION', 'HEADER', 'FOOTER', 'ASIDE', 'ARTICLE'],
251
255
  blockLevelElements: ['PRE', 'UL', 'OL', 'LI'],
252
-
256
+ highContrast: false,
257
+ observe: {
258
+ dropdowns: []
259
+ },
253
260
 
254
261
  // lang
255
262
  langs: {
@@ -323,9 +330,21 @@
323
330
  underline: 'Underline',
324
331
  alignment: 'Alignment',
325
332
  filename: 'Name (optional)',
326
- edit: 'Edit'
333
+ edit: 'Edit',
334
+ upload_label: 'Drop file here or '
327
335
  }
328
- }
336
+ },
337
+
338
+ linkify: {
339
+ regexps: {
340
+ youtube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.\-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig,
341
+ vimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,
342
+ image: /((https?|www)[^\s]+\.)(jpe?g|png|gif)(\?[^\s-]+)?/ig,
343
+ url: /(https?:\/\/(?:www\.|(?!www))[^\s\.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/ig,
344
+ }
345
+ },
346
+
347
+ codemirror: false
329
348
  };
330
349
 
331
350
  // Functionality
@@ -334,6 +353,7 @@
334
353
  keyCode: {
335
354
  BACKSPACE: 8,
336
355
  DELETE: 46,
356
+ UP: 38,
337
357
  DOWN: 40,
338
358
  ENTER: 13,
339
359
  SPACE: 32,
@@ -371,6 +391,16 @@
371
391
  // setup allowed and denied tags
372
392
  this.tidy.setupAllowed();
373
393
 
394
+ // setup denied tags
395
+ if (this.opts.deniedTags !== false)
396
+ {
397
+ var tags = ['html', 'head', 'link', 'body', 'meta', 'applet'];
398
+ for (var i = 0; i < tags.length; i++)
399
+ {
400
+ this.opts.deniedTags.push(tags[i]);
401
+ }
402
+ }
403
+
374
404
  // load lang
375
405
  this.lang.load();
376
406
 
@@ -425,7 +455,6 @@
425
455
  this[module][methods[z]] = this[module][methods[z]].bind(this);
426
456
  }
427
457
  },
428
-
429
458
  alignment: function()
430
459
  {
431
460
  return {
@@ -448,15 +477,18 @@
448
477
  set: function(type)
449
478
  {
450
479
  // focus
451
- if (!this.utils.browser('msie')) this.$editor.focus();
452
-
453
- this.buffer.set();
454
- this.selection.save();
480
+ if (!this.utils.browser('msie') && !this.opts.linebreaks)
481
+ {
482
+ this.$editor.focus();
483
+ }
455
484
 
456
485
  // get blocks
457
486
  this.alignment.blocks = this.selection.getBlocks();
458
487
  this.alignment.type = type;
459
488
 
489
+ this.buffer.set();
490
+ this.selection.save();
491
+
460
492
  // set alignment
461
493
  if (this.alignment.isLinebreaksOrNoBlocks())
462
494
  {
@@ -516,11 +548,11 @@
516
548
  autosave: function()
517
549
  {
518
550
  return {
551
+ html: false,
519
552
  enable: function()
520
553
  {
521
554
  if (!this.opts.autosave) return;
522
555
 
523
- this.autosave.html = false;
524
556
  this.autosave.name = (this.opts.autosaveName) ? this.opts.autosaveName : this.$textarea.attr('name');
525
557
 
526
558
  if (this.opts.autosaveOnChange) return;
@@ -533,15 +565,17 @@
533
565
  },
534
566
  load: function()
535
567
  {
568
+ if (!this.opts.autosave) return;
569
+
536
570
  this.autosave.source = this.code.get();
537
571
 
538
572
  if (this.autosave.html === this.autosave.source) return;
539
- if (this.utils.isEmpty(this.autosave.source)) return;
540
573
 
541
574
  // data
542
575
  var data = {};
543
576
  data['name'] = this.autosave.name;
544
- data[this.autosave.name] = escape(encodeURIComponent(this.autosave.source));
577
+ data[this.autosave.name] = this.autosave.source;
578
+ data = this.autosave.getHiddenFields(data);
545
579
 
546
580
  // ajax
547
581
  var jsxhr = $.ajax({
@@ -552,6 +586,23 @@
552
586
 
553
587
  jsxhr.done(this.autosave.success);
554
588
  },
589
+ getHiddenFields: function(data)
590
+ {
591
+ if (this.opts.autosaveFields === false || typeof this.opts.autosaveFields !== 'object')
592
+ {
593
+ return data;
594
+ }
595
+
596
+ $.each(this.opts.autosaveFields, $.proxy(function(k, v)
597
+ {
598
+ if (v !== null && v.toString().indexOf('#') === 0) v = $(v).val();
599
+ data[k] = v;
600
+
601
+ }, this));
602
+
603
+ return data;
604
+
605
+ },
555
606
  success: function(data)
556
607
  {
557
608
  var json;
@@ -586,7 +637,7 @@
586
637
 
587
638
  if (typeof this.formatting[name].data != 'undefined') type = 'data';
588
639
  else if (typeof this.formatting[name].attr != 'undefined') type = 'attr';
589
- else if (typeof this.formatting[name].class != 'undefined') type = 'class';
640
+ else if (typeof this.formatting[name]['class'] != 'undefined') type = 'class';
590
641
 
591
642
  if (typeof this.formatting[name].clear != 'undefined')
592
643
  {
@@ -610,6 +661,23 @@
610
661
  // focus
611
662
  if (!this.utils.browser('msie')) this.$editor.focus();
612
663
 
664
+ var html = $.trim(this.$editor.html());
665
+ this.block.isEmpty = this.utils.isEmpty(html);
666
+
667
+ // FF focus
668
+ if (this.utils.browser('mozilla') && !this.focus.isFocused())
669
+ {
670
+ if (this.block.isEmpty)
671
+ {
672
+ var $first;
673
+ if (!this.opts.linebreaks)
674
+ {
675
+ $first = this.$editor.children().first();
676
+ this.caret.setEnd($first);
677
+ }
678
+ }
679
+ }
680
+
613
681
  this.block.blocks = this.selection.getBlocks();
614
682
 
615
683
  this.block.blocksSize = this.block.blocks.length;
@@ -623,10 +691,12 @@
623
691
 
624
692
  this.selection.restore();
625
693
  this.code.sync();
694
+ this.observe.load();
626
695
 
627
696
  },
628
697
  set: function(tag)
629
698
  {
699
+
630
700
  this.selection.get();
631
701
  this.block.containerTag = this.range.commonAncestorContainer.tagName;
632
702
 
@@ -641,6 +711,15 @@
641
711
  },
642
712
  setCollapsed: function(tag)
643
713
  {
714
+ if (this.opts.linebreaks && this.block.isEmpty && tag != 'p')
715
+ {
716
+ var node = document.createElement(tag);
717
+ this.$editor.html(node);
718
+ this.caret.setEnd(node);
719
+
720
+ return;
721
+ }
722
+
644
723
  var block = this.block.blocks[0];
645
724
  if (block === false) return;
646
725
 
@@ -655,7 +734,6 @@
655
734
  var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH');
656
735
  if (isContainerTable && !this.opts.linebreaks)
657
736
  {
658
-
659
737
  document.execCommand('formatblock', false, '<' + tag + '>');
660
738
 
661
739
  block = this.selection.getBlock();
@@ -666,7 +744,7 @@
666
744
  {
667
745
  if (this.opts.linebreaks && tag == 'p')
668
746
  {
669
- $(block).prepend('<br>').append('<br>');
747
+ $(block).append('<br>');
670
748
  this.utils.replaceWithContents(block);
671
749
  }
672
750
  else
@@ -687,7 +765,7 @@
687
765
  // blockquote off
688
766
  if (this.opts.linebreaks)
689
767
  {
690
- $(block).prepend('<br>').append('<br>');
768
+ $(block).append('<br>');
691
769
  this.utils.replaceWithContents(block);
692
770
  }
693
771
  else
@@ -701,10 +779,16 @@
701
779
  this.block.toggle($(block));
702
780
  }
703
781
 
782
+
783
+ if (typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined')
784
+ {
785
+ $(block).removeAttr('class').removeAttr('style');
786
+ }
704
787
  },
705
788
  setMultiple: function(tag)
706
789
  {
707
790
  var block = this.block.blocks[0];
791
+
708
792
  var isContainerTable = (this.block.containerTag == 'TD' || this.block.containerTag == 'TH');
709
793
 
710
794
  if (block !== false && this.block.blocksSize === 1)
@@ -714,7 +798,7 @@
714
798
  // blockquote off
715
799
  if (this.opts.linebreaks)
716
800
  {
717
- $(block).prepend('<br>').append('<br>');
801
+ $(block).append('<br>');
718
802
  this.utils.replaceWithContents(block);
719
803
  }
720
804
  else
@@ -761,7 +845,6 @@
761
845
  }
762
846
  else
763
847
  {
764
-
765
848
  if (this.opts.linebreaks || tag != 'p')
766
849
  {
767
850
  if (tag == 'blockquote')
@@ -777,14 +860,20 @@
777
860
  {
778
861
  $.each(this.block.blocks, $.proxy(function(i,s)
779
862
  {
863
+ var $formatted = false;
780
864
  if (this.opts.linebreaks)
781
865
  {
782
866
  $(s).prepend('<br>').append('<br>');
783
- this.utils.replaceWithContents(s);
867
+ $formatted = this.utils.replaceWithContents(s);
784
868
  }
785
869
  else
786
870
  {
787
- this.utils.replaceToTag(s, 'p');
871
+ $formatted = this.utils.replaceToTag(s, 'p');
872
+ }
873
+
874
+ if ($formatted && typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined')
875
+ {
876
+ $formatted.removeAttr('class').removeAttr('style');
788
877
  }
789
878
 
790
879
  }, this));
@@ -794,7 +883,6 @@
794
883
 
795
884
  }
796
885
 
797
-
798
886
  this.block.formatWrap(tag);
799
887
  }
800
888
  else
@@ -831,6 +919,11 @@
831
919
  if (this.block.isRemoveInline) this.utils.removeInlineTags($formatted);
832
920
  if (tag == 'p' || this.block.headTag) $formatted.find('p').contents().unwrap();
833
921
 
922
+ if (typeof this.block.type == 'undefined' && typeof this.block.value == 'undefined')
923
+ {
924
+ $formatted.removeAttr('class').removeAttr('style');
925
+ }
926
+
834
927
 
835
928
  }, this));
836
929
  }
@@ -893,7 +986,7 @@
893
986
  },
894
987
  formatListToBlockquote: function()
895
988
  {
896
- var block = $(this.block.blocks[0]).closest('ul, ol');
989
+ var block = $(this.block.blocks[0]).closest('ul, ol', this.$editor[0]);
897
990
 
898
991
  $(block).find('ul, ol').contents().unwrap();
899
992
  $(block).find('li').append($('<br>')).contents().unwrap();
@@ -952,11 +1045,6 @@
952
1045
 
953
1046
  var $elements = $formatted.find(this.opts.blockLevelElements.join(',') + ', td, table, thead, tbody, tfoot, th, tr');
954
1047
 
955
- if ((this.opts.linebreaks && tag == 'p') || tag == 'pre' || tag == 'blockquote')
956
- {
957
- $elements.append('<br />');
958
- }
959
-
960
1048
  $elements.contents().unwrap();
961
1049
 
962
1050
  if (tag != 'p' && tag != 'blockquote') $formatted.find('img').remove();
@@ -983,13 +1071,24 @@
983
1071
  this.utils.replaceWithContents($formatted);
984
1072
  }
985
1073
 
1074
+ if (this.opts.linebreaks)
1075
+ {
1076
+ var $next = $formatted.next().next();
1077
+ if ($next.size() != 0 && $next[0].tagName === 'BR')
1078
+ {
1079
+ $next.remove();
1080
+ }
1081
+ }
1082
+
1083
+
1084
+
986
1085
  },
987
1086
  formatTableWrapping: function($formatted)
988
1087
  {
989
- if ($formatted.closest('table').length === 0) return;
1088
+ if ($formatted.closest('table', this.$editor[0]).length === 0) return;
990
1089
 
991
- if ($formatted.closest('tr').length === 0) $formatted.wrap('<tr>');
992
- if ($formatted.closest('td').length === 0 && $formatted.closest('th').length === 0)
1090
+ if ($formatted.closest('tr', this.$editor[0]).length === 0) $formatted.wrap('<tr>');
1091
+ if ($formatted.closest('td', this.$editor[0]).length === 0 && $formatted.closest('th').length === 0)
993
1092
  {
994
1093
  $formatted.wrap('<td>');
995
1094
  }
@@ -1142,6 +1241,8 @@
1142
1241
  build: function()
1143
1242
  {
1144
1243
  return {
1244
+ focused: false,
1245
+ blured: true,
1145
1246
  run: function()
1146
1247
  {
1147
1248
  this.build.createContainerBox();
@@ -1156,7 +1257,7 @@
1156
1257
  },
1157
1258
  createContainerBox: function()
1158
1259
  {
1159
- this.$box = $('<div class="redactor-box" />');
1260
+ this.$box = $('<div class="redactor-box" role="application" />');
1160
1261
  },
1161
1262
  createTextarea: function()
1162
1263
  {
@@ -1213,6 +1314,7 @@
1213
1314
  callEditor: function()
1214
1315
  {
1215
1316
  this.build.disableMozillaEditing();
1317
+ this.build.disableIeLinks();
1216
1318
  this.build.setEvents();
1217
1319
  this.build.setHelpers();
1218
1320
 
@@ -1252,7 +1354,8 @@
1252
1354
  {
1253
1355
  e.preventDefault();
1254
1356
 
1255
- if (!this.opts.dragImageUpload || !this.opts.dragFileUpload) return;
1357
+ if (!this.opts.dragImageUpload && !this.opts.dragFileUpload) return;
1358
+ if (this.opts.imageUpload === null && this.opts.fileUpload === null) return;
1256
1359
 
1257
1360
  var files = e.dataTransfer.files;
1258
1361
  this.upload.directUpload(files[0], e);
@@ -1266,6 +1369,12 @@
1266
1369
  setEvents: function()
1267
1370
  {
1268
1371
  // drop
1372
+ this.$editor.on('dragover.redactor dragenter.redactor', function(e)
1373
+ {
1374
+ e.preventDefault();
1375
+ e.stopPropagation();
1376
+ });
1377
+
1269
1378
  this.$editor.on('drop.redactor', $.proxy(function(e)
1270
1379
  {
1271
1380
  e = e.originalEvent || e;
@@ -1302,6 +1411,9 @@
1302
1411
  // paste
1303
1412
  this.$editor.on('paste.redactor', $.proxy(this.paste.init, this));
1304
1413
 
1414
+ // cut
1415
+ this.$editor.on('cut.redactor', $.proxy(this.code.sync, this));
1416
+
1305
1417
  // keydown
1306
1418
  this.$editor.on('keydown.redactor', $.proxy(this.keydown.init, this));
1307
1419
 
@@ -1321,35 +1433,58 @@
1321
1433
  }
1322
1434
 
1323
1435
  // focus
1324
- if ($.isFunction(this.opts.focusCallback))
1436
+ this.$editor.on('focus.redactor', $.proxy(function(e)
1325
1437
  {
1326
- this.$editor.on('focus.redactor', $.proxy(this.opts.focusCallback, this));
1327
- }
1438
+ if ($.isFunction(this.opts.focusCallback))
1439
+ {
1440
+ this.core.setCallback('focus', e);
1441
+ }
1442
+
1443
+ this.build.focused = true;
1444
+ this.build.blured = false;
1445
+
1446
+ if (this.selection.getCurrent() === false)
1447
+ {
1448
+ this.selection.get();
1449
+ this.range.setStart(this.$editor[0], 0);
1450
+ this.range.setEnd(this.$editor[0], 0);
1451
+ this.selection.addRange();
1452
+ }
1453
+
1454
+
1455
+ }, this));
1328
1456
 
1329
- var clickedElement;
1330
- $(document).on('mousedown', function(e) { clickedElement = e.target; });
1331
1457
 
1332
1458
  // blur
1333
- this.$editor.on('blur.redactor', $.proxy(function(e)
1459
+ $(document).on('mousedown.redactor-blur.' + this.uuid, $.proxy(function(e)
1334
1460
  {
1461
+ if (this.start) return;
1335
1462
  if (this.rtePaste) return;
1336
- if (!this.build.isBlured(clickedElement)) return;
1463
+
1464
+ if ($(e.target).closest('.redactor-editor, .redactor-toolbar, .redactor-dropdown').size() !== 0)
1465
+ {
1466
+ return;
1467
+ }
1337
1468
 
1338
1469
  this.utils.disableSelectAll();
1339
- if ($.isFunction(this.opts.blurCallback)) this.core.setCallback('blur', e);
1470
+ if (!this.build.blured && $.isFunction(this.opts.blurCallback))
1471
+ {
1472
+ this.core.setCallback('blur', e);
1473
+ }
1474
+
1475
+ this.build.focused = false;
1476
+ this.build.blured = true;
1340
1477
 
1341
1478
  }, this));
1342
- },
1343
- isBlured: function(clickedElement)
1344
- {
1345
- var $el = $(clickedElement);
1346
1479
 
1347
- return (!$el.hasClass('redactor-toolbar, redactor-dropdown') && !$el.is('#redactor-modal') && $el.parents('.redactor-toolbar, .redactor-dropdown, #redactor-modal').length === 0);
1348
1480
  },
1349
1481
  setHelpers: function()
1350
1482
  {
1351
- // autosave
1352
- this.autosave.enable();
1483
+ // linkify
1484
+ if (this.linkify.isEnabled())
1485
+ {
1486
+ this.linkify.format();
1487
+ }
1353
1488
 
1354
1489
  // placeholder
1355
1490
  this.placeholder.enable();
@@ -1362,21 +1497,17 @@
1362
1497
  plugins: function()
1363
1498
  {
1364
1499
  if (!this.opts.plugins) return;
1365
- if (!RedactorPlugins) return;
1366
1500
 
1367
1501
  $.each(this.opts.plugins, $.proxy(function(i, s)
1368
1502
  {
1369
- if (typeof RedactorPlugins[s] === 'undefined') return;
1503
+ var func = (typeof RedactorPlugins !== 'undefined' && typeof RedactorPlugins[s] !== 'undefined') ? RedactorPlugins : Redactor.fn;
1370
1504
 
1371
- if ($.inArray(s, $.Redactor.modules) !== -1)
1505
+ if (!$.isFunction(func[s]))
1372
1506
  {
1373
- $.error('Plugin name "' + s + '" matches the name of the Redactor\'s module.');
1374
1507
  return;
1375
1508
  }
1376
1509
 
1377
- if (!$.isFunction(RedactorPlugins[s])) return;
1378
-
1379
- this[s] = RedactorPlugins[s]();
1510
+ this[s] = func[s]();
1380
1511
 
1381
1512
  // get methods
1382
1513
  var methods = this.getModuleMethods(this[s]);
@@ -1388,7 +1519,10 @@
1388
1519
  this[s][methods[z]] = this[s][methods[z]].bind(this);
1389
1520
  }
1390
1521
 
1391
- if ($.isFunction(this[s].init)) this[s].init();
1522
+ if ($.isFunction(this[s].init))
1523
+ {
1524
+ this[s].init();
1525
+ }
1392
1526
 
1393
1527
 
1394
1528
  }, this));
@@ -1403,6 +1537,13 @@
1403
1537
  document.execCommand('enableObjectResizing', false, false);
1404
1538
  document.execCommand('enableInlineTableEditing', false, false);
1405
1539
  } catch (e) {}
1540
+ },
1541
+ disableIeLinks: function()
1542
+ {
1543
+ if (!this.utils.browser('msie')) return;
1544
+
1545
+ // IE prevent converting links
1546
+ document.execCommand("AutoUrlDetect", false, false);
1406
1547
  }
1407
1548
  };
1408
1549
  },
@@ -1411,7 +1552,7 @@
1411
1552
  return {
1412
1553
  build: function(btnName, btnObject)
1413
1554
  {
1414
- var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr('tabindex', '-1');
1555
+ var $button = $('<a href="#" class="re-icon re-' + btnName + '" rel="' + btnName + '" />').attr({'role': 'button', 'aria-label': btnObject.title, 'tabindex': '-1'});
1415
1556
 
1416
1557
  // click
1417
1558
  if (btnObject.func || btnObject.command || btnObject.dropdown)
@@ -1422,7 +1563,9 @@
1422
1563
  // dropdown
1423
1564
  if (btnObject.dropdown)
1424
1565
  {
1425
- var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + btnName + '" style="display: none;">');
1566
+ $button.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);
1567
+
1568
+ var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + btnName + '" style="display: none;">');
1426
1569
  $button.data('dropdown', $dropdown);
1427
1570
  this.dropdown.build(btnName, $dropdown, btnObject.dropdown);
1428
1571
  }
@@ -1461,20 +1604,24 @@
1461
1604
  },
1462
1605
  createTooltip: function($button, name, title)
1463
1606
  {
1464
- var $tooltip = $('<span>').addClass('redactor-toolbar-tooltip redactor-toolbar-tooltip-' + name).hide().html(title);
1607
+ var $tooltip = $('<span>').addClass('redactor-toolbar-tooltip redactor-toolbar-tooltip-' + this.uuid + ' redactor-toolbar-tooltip-' + name).hide().html(title);
1465
1608
  $tooltip.appendTo('body');
1466
1609
 
1467
1610
  $button.on('mouseover', function()
1468
1611
  {
1469
- if ($(this).hasClass('redactor-button-disabled')) return;
1612
+ if ($(this).hasClass('redactor-button-disabled'))
1613
+ {
1614
+ return;
1615
+ }
1470
1616
 
1471
1617
  var pos = $button.offset();
1472
1618
 
1473
- $tooltip.show();
1474
1619
  $tooltip.css({
1475
1620
  top: (pos.top + $button.innerHeight()) + 'px',
1476
1621
  left: (pos.left + $button.innerWidth()/2 - $tooltip.innerWidth()/2) + 'px'
1477
1622
  });
1623
+ $tooltip.show();
1624
+
1478
1625
  });
1479
1626
 
1480
1627
  $button.on('mouseout', function()
@@ -1489,6 +1636,8 @@
1489
1636
 
1490
1637
  e.preventDefault();
1491
1638
 
1639
+ $(document).find('.redactor-toolbar-tooltip').hide();
1640
+
1492
1641
  if (this.utils.browser('msie')) e.returnValue = false;
1493
1642
 
1494
1643
  if (type == 'command') this.inline.format(callback);
@@ -1525,7 +1674,7 @@
1525
1674
  },
1526
1675
  setInactiveAll: function(key)
1527
1676
  {
1528
- if (typeof key == 'undefined')
1677
+ if (typeof key === 'undefined')
1529
1678
  {
1530
1679
  this.$toolbar.find('a.re-icon').removeClass('redactor-act');
1531
1680
  }
@@ -1536,11 +1685,11 @@
1536
1685
  },
1537
1686
  setActiveInVisual: function()
1538
1687
  {
1539
- this.$toolbar.find('a.re-icon').not('a.re-html').removeClass('redactor-button-disabled');
1688
+ this.$toolbar.find('a.re-icon').not('a.re-html, a.re-fullscreen').removeClass('redactor-button-disabled');
1540
1689
  },
1541
1690
  setInactiveInCode: function()
1542
1691
  {
1543
- this.$toolbar.find('a.re-icon').not('a.re-html').addClass('redactor-button-disabled');
1692
+ this.$toolbar.find('a.re-icon').not('a.re-html, a.re-fullscreen').addClass('redactor-button-disabled');
1544
1693
  },
1545
1694
  changeIcon: function(key, classname)
1546
1695
  {
@@ -1558,6 +1707,8 @@
1558
1707
  },
1559
1708
  addCallback: function($btn, callback)
1560
1709
  {
1710
+ if ($btn == "buffer") return;
1711
+
1561
1712
  var type = (callback == 'dropdown') ? 'dropdown' : 'func';
1562
1713
  var key = $btn.attr('rel');
1563
1714
  $btn.on('touchstart click', $.proxy(function(e)
@@ -1569,10 +1720,12 @@
1569
1720
  },
1570
1721
  addDropdown: function($btn, dropdown)
1571
1722
  {
1723
+ $btn.addClass('redactor-toolbar-link-dropdown').attr('aria-haspopup', true);
1724
+
1572
1725
  var key = $btn.attr('rel');
1573
1726
  this.button.addCallback($btn, 'dropdown');
1574
1727
 
1575
- var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-box-' + key + '" style="display: none;">');
1728
+ var $dropdown = $('<div class="redactor-dropdown redactor-dropdown-' + this.uuid + ' redactor-dropdown-box-' + key + '" style="display: none;">');
1576
1729
  $btn.data('dropdown', $dropdown);
1577
1730
 
1578
1731
  // build dropdown
@@ -1584,6 +1737,8 @@
1584
1737
  {
1585
1738
  if (!this.opts.toolbar) return;
1586
1739
 
1740
+ if (this.button.isMobileUndoRedo(key)) return "buffer";
1741
+
1587
1742
  var btn = this.button.build(key, { title: title });
1588
1743
  btn.addClass('redactor-btn-image');
1589
1744
 
@@ -1595,7 +1750,10 @@
1595
1750
  {
1596
1751
  if (!this.opts.toolbar) return;
1597
1752
 
1753
+ if (this.button.isMobileUndoRedo(key)) return "buffer";
1754
+
1598
1755
  var btn = this.button.build(key, { title: title });
1756
+ btn.addClass('redactor-btn-image');
1599
1757
  this.$toolbar.prepend($('<li>').append(btn));
1600
1758
 
1601
1759
  return btn;
@@ -1604,7 +1762,10 @@
1604
1762
  {
1605
1763
  if (!this.opts.toolbar) return;
1606
1764
 
1765
+ if (this.button.isMobileUndoRedo(key)) return "buffer";
1766
+
1607
1767
  var btn = this.button.build(key, { title: title });
1768
+ btn.addClass('redactor-btn-image');
1608
1769
  var $btn = this.button.get(afterkey);
1609
1770
 
1610
1771
  if ($btn.length !== 0) $btn.parent().after($('<li>').append(btn));
@@ -1616,7 +1777,10 @@
1616
1777
  {
1617
1778
  if (!this.opts.toolbar) return;
1618
1779
 
1780
+ if (this.button.isMobileUndoRedo(key)) return "buffer";
1781
+
1619
1782
  var btn = this.button.build(key, { title: title });
1783
+ btn.addClass('redactor-btn-image');
1620
1784
  var $btn = this.button.get(beforekey);
1621
1785
 
1622
1786
  if ($btn.length !== 0) $btn.parent().before($('<li>').append(btn));
@@ -1627,6 +1791,10 @@
1627
1791
  remove: function(key)
1628
1792
  {
1629
1793
  this.button.get(key).remove();
1794
+ },
1795
+ isMobileUndoRedo: function(key)
1796
+ {
1797
+ return (key == "undo" || key == "redo") && !this.utils.isDesktop();
1630
1798
  }
1631
1799
  };
1632
1800
  },
@@ -1650,7 +1818,14 @@
1650
1818
  },
1651
1819
  setEnd: function(node)
1652
1820
  {
1821
+ node = node[0] || node;
1822
+ if (node.lastChild.nodeType == 1)
1823
+ {
1824
+ return this.caret.setAfter(node.lastChild);
1825
+ }
1826
+
1653
1827
  this.caret.set(node, 1, node, 1);
1828
+
1654
1829
  },
1655
1830
  set: function(orgn, orgo, focn, foco)
1656
1831
  {
@@ -1840,11 +2015,11 @@
1840
2015
 
1841
2016
  // replace dollar sign to entity
1842
2017
  html = html.replace(/\$/g, '&#36;');
1843
- html = html.replace(/”/g, '"');
1844
- html = html.replace(/‘/g, '\'');
1845
- html = html.replace(/’/g, '\'');
1846
2018
 
1847
- if (this.opts.replaceDivs) html = this.clean.replaceDivs(html);
2019
+ // replace special characters in links
2020
+ html = html.replace(/<a href="(.*?[^>]?)®(.*?[^>]?)">/gi, '<a href="$1&reg$2">');
2021
+
2022
+ if (this.opts.replaceDivs && !this.opts.linebreaks) html = this.clean.replaceDivs(html);
1848
2023
  if (this.opts.linebreaks) html = this.clean.replaceParagraphsToBr(html);
1849
2024
 
1850
2025
  // save form tag
@@ -1865,10 +2040,11 @@
1865
2040
 
1866
2041
  html = $div.html();
1867
2042
  }
2043
+
1868
2044
  $div.remove();
1869
2045
 
1870
2046
  // remove font tag
1871
- html = html.replace(/<font(.*?[^<])>/gi, '');
2047
+ html = html.replace(/<font(.*?)>/gi, '');
1872
2048
  html = html.replace(/<\/font>/gi, '');
1873
2049
 
1874
2050
  // tidy html
@@ -1883,12 +2059,14 @@
1883
2059
  // convert inline tags
1884
2060
  html = this.clean.convertInline(html);
1885
2061
 
2062
+ html = html.replace(/&amp;/g, '&');
2063
+
1886
2064
  return html;
1887
2065
  },
1888
2066
  onSync: function(html)
1889
2067
  {
1890
2068
  // remove spaces
1891
- html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
2069
+ html = html.replace(/\u200B/g, '');
1892
2070
  html = html.replace(/&#x200b;/gi, '');
1893
2071
 
1894
2072
  if (this.opts.cleanSpaces)
@@ -1920,17 +2098,41 @@
1920
2098
  html = html.replace(new RegExp(i, 'g'), s);
1921
2099
  });
1922
2100
 
1923
- // remove br in the of li
2101
+ // remove last br in FF
2102
+ if (this.utils.browser('mozilla'))
2103
+ {
2104
+ html = html.replace(/<br\s?\/?>$/gi, '');
2105
+ }
2106
+
2107
+ // remove br in|of li tags
1924
2108
  html = html.replace(new RegExp('<br\\s?/?></li>', 'gi'), '</li>');
1925
2109
  html = html.replace(new RegExp('</li><br\\s?/?>', 'gi'), '</li>');
2110
+
2111
+ // remove empty attributes
2112
+ html = html.replace(/<(.*?)rel="\s*?"(.*?[^>]?)>/gi, '<$1$2">');
2113
+ html = html.replace(/<(.*?)style="\s*?"(.*?[^>]?)>/gi, '<$1$2">');
2114
+ html = html.replace(/="">/gi, '>');
2115
+ html = html.replace(/""">/gi, '">');
2116
+ html = html.replace(/"">/gi, '">');
2117
+
1926
2118
  // remove verified
1927
- html = html.replace(new RegExp('<div(.*?[^>]) data-tagblock="redactor"(.*?[^>])>', 'gi'), '<div$1$2>');
1928
- html = html.replace(new RegExp('<(.*?) data-verified="redactor"(.*?[^>])>', 'gi'), '<$1$2>');
1929
- html = html.replace(new RegExp('<span(.*?[^>])\srel="(.*?[^>])"(.*?[^>])>', 'gi'), '<span$1$3>');
1930
- html = html.replace(new RegExp('<img(.*?[^>])\srel="(.*?[^>])"(.*?[^>])>', 'gi'), '<img$1$3>');
1931
- html = html.replace(new RegExp('<img(.*?[^>])\sstyle="" (.*?[^>])>', 'gi'), '<img$1 $2>');
1932
- html = html.replace(new RegExp('<img(.*?[^>])\sstyle (.*?[^>])>', 'gi'), '<img$1 $2>');
1933
- html = html.replace(new RegExp('<span class="redactor-invisible-space">(.*?)</span>', 'gi'), '$1');
2119
+ html = html.replace(/<div(.*?)data-tagblock="redactor"(.*?[^>])>/gi, '<div$1$2>');
2120
+ html = html.replace(/<(.*?) data-verified="redactor"(.*?[^>])>/gi, '<$1$2>');
2121
+
2122
+ var $div = $("<div/>").html($.parseHTML(html, document, true));
2123
+ $div.find("span").removeAttr("rel");
2124
+
2125
+ $div.find('pre .redactor-invisible-space').each(function()
2126
+ {
2127
+ $(this).contents().unwrap();
2128
+ });
2129
+
2130
+ html = $div.html();
2131
+
2132
+ // remove rel attribute from img
2133
+ html = html.replace(/<img(.*?[^>])rel="(.*?[^>])"(.*?[^>])>/gi, '<img$1$3>');
2134
+ html = html.replace(/<span class="redactor-invisible-space">(.*?)<\/span>/gi, '$1');
2135
+
1934
2136
  html = html.replace(/ data-save-url="(.*?[^>])"/gi, '');
1935
2137
 
1936
2138
  // remove image resize
@@ -1939,7 +2141,7 @@
1939
2141
  html = html.replace(/<span(.*?)id="redactor-image-editter"(.*?[^>])>(.*?)<\/span>/gi, '');
1940
2142
 
1941
2143
  // remove font tag
1942
- html = html.replace(/<font(.*?[^<])>/gi, '');
2144
+ html = html.replace(/<font(.*?)>/gi, '');
1943
2145
  html = html.replace(/<\/font>/gi, '');
1944
2146
 
1945
2147
  // tidy html
@@ -1957,18 +2159,17 @@
1957
2159
  html = html.replace(new RegExp('<(.*?) data-verified="redactor"(.*?[^>])>', 'gi'), '<$1$2>');
1958
2160
  html = html.replace(new RegExp('<(.*?) data-verified="redactor">', 'gi'), '<$1>');
1959
2161
 
2162
+ html = html.replace(/&amp;/g, '&');
2163
+
1960
2164
  return html;
1961
2165
  },
1962
2166
  onPaste: function(html, setMode)
1963
2167
  {
1964
2168
  html = $.trim(html);
1965
-
1966
2169
  html = html.replace(/\$/g, '&#36;');
1967
- html = html.replace(/‘/g, '\'');
1968
- html = html.replace(/’/g, '\'');
1969
2170
 
1970
2171
  // convert dirty spaces
1971
- html = html.replace(/<span class="s1">/gi, '<span>');
2172
+ html = html.replace(/<span class="s[0-9]">/gi, '<span>');
1972
2173
  html = html.replace(/<span class="Apple-converted-space">&nbsp;<\/span>/gi, ' ');
1973
2174
  html = html.replace(/<span class="Apple-tab-span"[^>]*>\t<\/span>/gi, '\t');
1974
2175
  html = html.replace(/<span[^>]*>(\s|&nbsp;)<\/span>/gi, ' ');
@@ -1989,6 +2190,8 @@
1989
2190
  {
1990
2191
  html = html.replace(/”/g, '"');
1991
2192
  html = html.replace(/“/g, '"');
2193
+ html = html.replace(/‘/g, '\'');
2194
+ html = html.replace(/’/g, '\'');
1992
2195
 
1993
2196
  return this.clean.getPreCode(html);
1994
2197
  }
@@ -2055,6 +2258,7 @@
2055
2258
  html = this.clean.onPasteRemoveSpans(html);
2056
2259
  html = this.clean.onPasteRemoveEmpty(html);
2057
2260
 
2261
+
2058
2262
  html = this.clean.convertInline(html);
2059
2263
 
2060
2264
  return html;
@@ -2067,20 +2271,108 @@
2067
2271
  // style
2068
2272
  html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
2069
2273
 
2070
- if (/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/.test(html))
2274
+ // op
2275
+ html = html.replace(/<o\:p[^>]*>[\s\S]*?<\/o\:p>/gi, '');
2276
+
2277
+ if (html.match(/class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i))
2071
2278
  {
2279
+ // comments
2280
+ html = html.replace(/<!--[\s\S]+?-->/gi, '');
2281
+
2282
+ // scripts
2283
+ html = html.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '');
2284
+
2285
+ // Convert <s> into <strike>
2286
+ html = html.replace(/<(\/?)s>/gi, "<$1strike>");
2287
+
2288
+ // Replace nbsp entites to char since it's easier to handle
2289
+ html = html.replace(/ /gi, ' ');
2290
+
2291
+ // Convert <span style="mso-spacerun:yes">___</span> to string of alternating
2292
+ // breaking/non-breaking spaces of same length
2293
+ html = html.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, function(str, spaces) {
2294
+ return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : '';
2295
+ });
2296
+
2072
2297
  html = this.clean.onPasteIeFixLinks(html);
2073
2298
 
2074
2299
  // shapes
2075
2300
  html = html.replace(/<img(.*?)v:shapes=(.*?)>/gi, '');
2076
2301
  html = html.replace(/src="file\:\/\/(.*?)"/, 'src=""');
2077
2302
 
2078
- // list
2079
- html = html.replace(/<p(.*?)class="MsoListParagraphCxSpFirst"([\w\W]*?)<\/p>/gi, '<ul><li$2</li>');
2080
- html = html.replace(/<p(.*?)class="MsoListParagraphCxSpMiddle"([\w\W]*?)<\/p>/gi, '<li$2</li>');
2081
- html = html.replace(/<p(.*?)class="MsoListParagraphCxSpLast"([\w\W]*?)<\/p>/gi, '<li$2</li></ul>');
2082
- // one line
2083
- html = html.replace(/<p(.*?)class="MsoListParagraph"([\w\W]*?)<\/p>/gi, '<ul><li$2</li></ul>');
2303
+ // lists
2304
+ var $div = $("<div/>").html(html);
2305
+
2306
+ var lastList = false;
2307
+ var lastLevel = 1;
2308
+ var listsIds = [];
2309
+
2310
+ $div.find("p[style]").each(function()
2311
+ {
2312
+ var matches = $(this).attr('style').match(/mso\-list\:l([0-9]+)\slevel([0-9]+)/);
2313
+
2314
+ if (matches)
2315
+ {
2316
+ var currentList = parseInt(matches[1]);
2317
+ var currentLevel = parseInt(matches[2]);
2318
+ var listType = $(this).html().match(/^[\w]+\./) ? "ol" : "ul";
2319
+
2320
+ var $li = $("<li/>").html($(this).html());
2321
+
2322
+ $li.html($li.html().replace(/^([\w\.]+)</, '<'));
2323
+ $li.find("span:first").remove();
2324
+
2325
+ if (currentLevel == 1 && $.inArray(currentList, listsIds) == -1)
2326
+ {
2327
+ var $list = $("<" + listType + "/>").attr({"data-level": currentLevel,
2328
+ "data-list": currentList})
2329
+ .html($li);
2330
+
2331
+ $(this).replaceWith($list);
2332
+
2333
+ lastList = currentList;
2334
+ listsIds.push(currentList);
2335
+ }
2336
+ else
2337
+ {
2338
+ if (currentLevel > lastLevel)
2339
+ {
2340
+ var $prevList = $div.find('[data-level="' + lastLevel + '"][data-list="' + lastList + '"]');
2341
+
2342
+ var $lastList = $prevList;
2343
+
2344
+ for(var i = lastLevel; i < currentLevel; i++)
2345
+ {
2346
+ $list = $("<" + listType + "/>");
2347
+
2348
+ $list.appendTo($lastList.find("li").last());
2349
+
2350
+ $lastList = $list;
2351
+ }
2352
+
2353
+ $lastList.attr({"data-level": currentLevel,
2354
+ "data-list": currentList})
2355
+ .html($li);
2356
+
2357
+ }
2358
+ else
2359
+ {
2360
+ var $prevList = $div.find('[data-level="' + currentLevel + '"][data-list="' + currentList + '"]').last();
2361
+
2362
+ $prevList.append($li);
2363
+ }
2364
+
2365
+ lastLevel = currentLevel;
2366
+ lastList = currentList;
2367
+
2368
+ $(this).remove();
2369
+ }
2370
+ }
2371
+ });
2372
+
2373
+ $div.find('[data-level][data-list]').removeAttr('data-level data-list');
2374
+ html = $div.html();
2375
+
2084
2376
  // remove ms word's bullet
2085
2377
  html = html.replace(/·/g, '');
2086
2378
  html = html.replace(/<p class="Mso(.*?)"/gi, '<p');
@@ -2099,12 +2391,6 @@
2099
2391
  html = html.replace(/<p>\n?<li>/gi, '<li>');
2100
2392
  }
2101
2393
 
2102
- // remove nbsp
2103
- if (this.opts.cleanSpaces)
2104
- {
2105
- html = html.replace(/(\s|&nbsp;)+/g, ' ');
2106
- }
2107
-
2108
2394
  return html;
2109
2395
  },
2110
2396
  onPasteExtra: function(html)
@@ -2199,23 +2485,16 @@
2199
2485
  }
2200
2486
 
2201
2487
  var options = {
2202
- deniedTags: false,
2203
- allowedTags: tags,
2488
+ deniedTags: (this.opts.deniedTags) ? this.opts.deniedTags : false,
2489
+ allowedTags: (this.opts.allowedTags) ? this.opts.allowedTags : tags,
2204
2490
  removeComments: true,
2205
2491
  removePhp: true,
2206
- removeAttr: false,
2207
- allowedAttr: attrAllowed,
2492
+ removeAttr: (this.opts.removeAttr) ? this.opts.removeAttr : false,
2493
+ allowedAttr: (this.opts.allowedAttr) ? this.opts.allowedAttr : attrAllowed,
2208
2494
  removeEmpty: tagsEmpty
2209
2495
  };
2210
2496
 
2211
- // denied tags
2212
- if (this.opts.deniedTags)
2213
- {
2214
- options.deniedTags = this.opts.deniedTags;
2215
- }
2216
-
2217
2497
  return this.tidy.load(html, options);
2218
-
2219
2498
  },
2220
2499
  onPasteRemoveEmpty: function(html)
2221
2500
  {
@@ -2259,8 +2538,8 @@
2259
2538
  if (!matchBlocks && (matchContainers === null || (matchContainers && matchContainers.length <= 1)))
2260
2539
  {
2261
2540
  var matchBR = html.match(/<br\s?\/?>/gi);
2262
- var matchIMG = html.match(/<img(.*?[^>])>/gi);
2263
- if (!matchBR && !matchIMG)
2541
+ //var matchIMG = html.match(/<img(.*?[^>])>/gi);
2542
+ if (!matchBR)
2264
2543
  {
2265
2544
  this.clean.singleLine = true;
2266
2545
  html = html.replace(/<\/?(p|div)(.*?)>/gi, '');
@@ -2281,33 +2560,69 @@
2281
2560
  },
2282
2561
  savePreCode: function(html)
2283
2562
  {
2284
- var pre = html.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/gi);
2563
+ html = this.clean.savePreFormatting(html);
2564
+ html = this.clean.saveCodeFormatting(html);
2565
+
2566
+ html = this.clean.restoreSelectionMarker(html);
2567
+
2568
+ return html;
2569
+ },
2570
+ savePreFormatting: function(html)
2571
+ {
2572
+ var pre = html.match(/<pre(.*?)>([\w\W]*?)<\/pre>/gi);
2573
+
2285
2574
  if (pre !== null)
2286
2575
  {
2287
2576
  $.each(pre, $.proxy(function(i,s)
2288
2577
  {
2289
- var arr = s.match(/<(pre|code)(.*?)>([\w\W]*?)<\/(pre|code)>/i);
2578
+ var arr = s.match(/<pre(.*?)>([\w\W]*?)<\/pre>/i);
2290
2579
 
2291
- arr[3] = arr[3].replace(/<br\s?\/?>/g, '\n');
2292
- arr[3] = arr[3].replace(/&nbsp;/g, ' ');
2580
+ arr[2] = arr[2].replace(/<br\s?\/?>/g, '\n');
2581
+ arr[2] = arr[2].replace(/&nbsp;/g, ' ');
2293
2582
 
2294
2583
  if (this.opts.preSpaces)
2295
2584
  {
2296
- arr[3] = arr[3].replace(/\t/g, Array(this.opts.preSpaces + 1).join(' '));
2585
+ arr[2] = arr[2].replace(/\t/g, Array(this.opts.preSpaces + 1).join(' '));
2297
2586
  }
2298
2587
 
2299
- arr[3] = this.clean.encodeEntities(arr[3]);
2588
+ arr[2] = this.clean.encodeEntities(arr[2]);
2300
2589
 
2301
2590
  // $ fix
2302
- arr[3] = arr[3].replace(/\$/g, '&#36;');
2591
+ arr[2] = arr[2].replace(/\$/g, '&#36;');
2592
+
2593
+ html = html.replace(s, '<pre' + arr[1] + '>' + arr[2] + '</pre>');
2594
+
2595
+ }, this));
2596
+ }
2303
2597
 
2304
- html = html.replace(s, '<' + arr[1] + arr[2] + '>' + arr[3] + '</' + arr[1] + '>');
2598
+ return html;
2599
+ },
2600
+ saveCodeFormatting: function(html)
2601
+ {
2602
+ var code = html.match(/<code(.*?)>([\w\W]*?)<\/code>/gi);
2603
+
2604
+ if (code !== null)
2605
+ {
2606
+ $.each(code, $.proxy(function(i,s)
2607
+ {
2608
+ var arr = s.match(/<code(.*?)>([\w\W]*?)<\/code>/i);
2305
2609
 
2610
+ arr[2] = arr[2].replace(/&nbsp;/g, ' ');
2611
+ arr[2] = this.clean.encodeEntities(arr[2]);
2612
+ arr[2] = arr[2].replace(/\$/g, '&#36;');
2613
+
2614
+ html = html.replace(s, '<code' + arr[1] + '>' + arr[2] + '</code>');
2306
2615
  }, this));
2307
2616
  }
2308
2617
 
2309
2618
  return html;
2310
2619
  },
2620
+ restoreSelectionMarker: function(html)
2621
+ {
2622
+ html = html.replace(/&lt;span id=&quot;selection-marker-([0-9])&quot; class=&quot;redactor-selection-marker&quot; data-verified=&quot;redactor&quot;&gt;​&lt;\/span&gt;/g, '<span id="selection-marker-$1" class="redactor-selection-marker" data-verified="redactor">​</span>');
2623
+
2624
+ return html;
2625
+ },
2311
2626
  getTextFromHtml: function(html)
2312
2627
  {
2313
2628
  html = html.replace(/<br\s?\/?>|<\/H[1-6]>|<\/p>|<\/div>|<\/li>|<\/td>/gi, '\n');
@@ -2321,9 +2636,11 @@
2321
2636
  getPlainText: function(html, paragraphize)
2322
2637
  {
2323
2638
  html = this.clean.getTextFromHtml(html);
2639
+ html = html.replace(/\n\s*\n/g, "\n");
2640
+ html = html.replace(/\n\n/g, "\n");
2324
2641
  html = html.replace(/\n/g, '<br />');
2325
2642
 
2326
- if (this.opts.paragraphize && typeof paragraphize == 'undefined')
2643
+ if (this.opts.paragraphize && typeof paragraphize == 'undefined' && !this.utils.browser('mozilla'))
2327
2644
  {
2328
2645
  html = this.paragraphize.load(html);
2329
2646
  }
@@ -2411,23 +2728,30 @@
2411
2728
  $s.attr('style', $s.attr('rel'));
2412
2729
  });
2413
2730
 
2731
+ },
2732
+ cleanEmptyParagraph: function()
2733
+ {
2734
+
2414
2735
  },
2415
2736
  setVerified: function(html)
2416
2737
  {
2417
2738
  if (this.utils.browser('msie')) return html;
2418
2739
 
2419
2740
  html = html.replace(new RegExp('<img(.*?[^>])>', 'gi'), '<img$1 data-verified="redactor">');
2420
- html = html.replace(new RegExp('<span(.*?)>', 'gi'), '<span$1 data-verified="redactor">');
2741
+ html = html.replace(new RegExp('<span(.*?[^>])>', 'gi'), '<span$1 data-verified="redactor">');
2421
2742
 
2422
2743
  var matches = html.match(new RegExp('<(span|img)(.*?)style="(.*?)"(.*?[^>])>', 'gi'));
2744
+
2423
2745
  if (matches)
2424
2746
  {
2425
2747
  var len = matches.length;
2426
2748
  for (var i = 0; i < len; i++)
2427
2749
  {
2428
2750
  try {
2751
+
2429
2752
  var newTag = matches[i].replace(/style="(.*?)"/i, 'style="$1" rel="$1"');
2430
- html = html.replace(new RegExp(matches[i], 'gi'), newTag);
2753
+ html = html.replace(matches[i], newTag);
2754
+
2431
2755
  }
2432
2756
  catch (e) {}
2433
2757
  }
@@ -2482,7 +2806,7 @@
2482
2806
  html = html.replace(/[\s\n]*$/g, ' ');
2483
2807
  html = html.replace( />\s{2,}</g, '> <'); // between inline tags can be only one space
2484
2808
  html = html.replace(/\n\n/g, "\n");
2485
- html = html.replace(/[\u200B-\u200D\uFEFF]/g, '');
2809
+ html = html.replace(/\u200B/g, '');
2486
2810
 
2487
2811
  return html;
2488
2812
  },
@@ -2498,6 +2822,9 @@
2498
2822
  html = html.replace(/<div(.*?)>([\w\W]*?)<\/div>/gi, '<p$1>$2</p>');
2499
2823
  }
2500
2824
 
2825
+ html = html.replace(/<div(.*?[^>])>/gi, '');
2826
+ html = html.replace(/<\/div>/gi, '');
2827
+
2501
2828
  return html;
2502
2829
  },
2503
2830
  replaceDivsToBr: function(html)
@@ -2537,9 +2864,17 @@
2537
2864
  // clean
2538
2865
  html = this.clean.onSet(html);
2539
2866
 
2867
+
2868
+ if (this.utils.browser('msie'))
2869
+ {
2870
+ html = html.replace(/<span(.*?)id="selection-marker-(1|2)"(.*?)><\/span>/gi, '');
2871
+ }
2872
+
2540
2873
  this.$editor.html(html);
2541
2874
  this.code.sync();
2542
2875
 
2876
+ if (html !== '') this.placeholder.remove();
2877
+
2543
2878
  setTimeout($.proxy(this.buffer.add, this), 15);
2544
2879
  if (this.start === false) this.observe.load();
2545
2880
 
@@ -2548,6 +2883,9 @@
2548
2883
  {
2549
2884
  var code = this.$textarea.val();
2550
2885
 
2886
+ if (this.opts.replaceDivs) code = this.clean.replaceDivs(code);
2887
+ if (this.opts.linebreaks) code = this.clean.replaceParagraphsToBr(code);
2888
+
2551
2889
  // indent code
2552
2890
  code = this.tabifier.get(code);
2553
2891
 
@@ -2562,7 +2900,7 @@
2562
2900
  var html = this.$editor.html();
2563
2901
 
2564
2902
  // is there a need to synchronize
2565
- if (this.code.syncCode && this.code.syncCode == html)
2903
+ if (this.code.syncCode && this.code.syncCode == html || (this.start && html == '' ))
2566
2904
  {
2567
2905
  // do not sync
2568
2906
  return;
@@ -2590,11 +2928,25 @@
2590
2928
 
2591
2929
  this.start = false;
2592
2930
 
2593
- // autosave on change
2594
- this.autosave.onChange();
2595
- },
2596
- toggle: function()
2597
- {
2931
+ if (this.autosave.html == false)
2932
+ {
2933
+ this.autosave.html = this.code.get();
2934
+ }
2935
+
2936
+ if (this.opts.codemirror)
2937
+ {
2938
+ this.$textarea.next('.CodeMirror').each(function(i, el)
2939
+ {
2940
+ el.CodeMirror.setValue(html);
2941
+ });
2942
+ }
2943
+
2944
+ //autosave
2945
+ this.autosave.onChange();
2946
+ this.autosave.enable();
2947
+ },
2948
+ toggle: function()
2949
+ {
2598
2950
  if (this.opts.visual)
2599
2951
  {
2600
2952
  this.code.showCode();
@@ -2606,30 +2958,85 @@
2606
2958
  },
2607
2959
  showCode: function()
2608
2960
  {
2961
+ this.selection.save();
2962
+
2609
2963
  this.code.offset = this.caret.getOffset();
2610
2964
  var scroll = $(window).scrollTop();
2611
2965
 
2612
- var height = this.$editor.innerHeight();
2966
+ var width = this.$editor.innerWidth(),
2967
+ height = this.$editor.innerHeight();
2613
2968
 
2614
2969
  this.$editor.hide();
2615
2970
 
2616
2971
  var html = this.$textarea.val();
2972
+
2617
2973
  this.modified = this.clean.removeSpaces(html);
2618
2974
 
2619
2975
  // indent code
2620
2976
  html = this.tabifier.get(html);
2621
2977
 
2622
- this.$textarea.val(html).height(height).show().focus();
2623
- this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting);
2978
+ // caret position sync
2979
+ var start = 0, end = 0;
2980
+ var $editorDiv = $("<div/>").append($.parseHTML(this.clean.onSync(this.$editor.html()), document, true));
2981
+ var $selectionMarkers = $editorDiv.find("span.redactor-selection-marker");
2624
2982
 
2625
- $(window).scrollTop(scroll);
2983
+ if ($selectionMarkers.length > 0)
2984
+ {
2985
+ var editorHtml = this.tabifier.get($editorDiv.html()).replace(/&amp;/g, '&');
2986
+
2987
+ if ($selectionMarkers.length == 1)
2988
+ {
2989
+ start = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-1").prop("outerHTML"));
2990
+ end = start;
2991
+ }
2992
+ else if ($selectionMarkers.length == 2)
2993
+ {
2994
+ start = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-1").prop("outerHTML"));
2995
+ end = this.utils.strpos(editorHtml, $editorDiv.find("#selection-marker-2").prop("outerHTML")) - $editorDiv.find("#selection-marker-1").prop("outerHTML").toString().length;
2996
+ }
2997
+ }
2998
+
2999
+ this.selection.removeMarkers();
3000
+ this.$textarea.val(html);
2626
3001
 
2627
- if (this.$textarea[0].setSelectionRange)
3002
+ if (this.opts.codemirror)
2628
3003
  {
2629
- this.$textarea[0].setSelectionRange(0, 0);
3004
+ this.$textarea.next('.CodeMirror').each(function(i, el)
3005
+ {
3006
+ $(el).show();
3007
+ el.CodeMirror.setValue(html);
3008
+ el.CodeMirror.setSize('100%', height);
3009
+ el.CodeMirror.refresh();
3010
+
3011
+ if (start == end)
3012
+ {
3013
+ el.CodeMirror.setCursor(el.CodeMirror.posFromIndex(start).line, el.CodeMirror.posFromIndex(end).ch);
3014
+ }
3015
+ else
3016
+ {
3017
+ el.CodeMirror.setSelection({line: el.CodeMirror.posFromIndex(start).line,
3018
+ ch: el.CodeMirror.posFromIndex(start).ch},
3019
+ {line: el.CodeMirror.posFromIndex(end).line,
3020
+ ch: el.CodeMirror.posFromIndex(end).ch});
3021
+ }
3022
+
3023
+ el.CodeMirror.focus();
3024
+ });
2630
3025
  }
3026
+ else
3027
+ {
3028
+ this.$textarea.height(height).show().focus();
3029
+ this.$textarea.on('keydown.redactor-textarea-indenting', this.code.textareaIndenting);
3030
+
3031
+ $(window).scrollTop(scroll);
3032
+
3033
+ if (this.$textarea[0].setSelectionRange)
3034
+ {
3035
+ this.$textarea[0].setSelectionRange(start, end);
3036
+ }
2631
3037
 
2632
- this.$textarea[0].scrollTop = 0;
3038
+ this.$textarea[0].scrollTop = 0;
3039
+ }
2633
3040
 
2634
3041
  this.opts.visual = false;
2635
3042
 
@@ -2639,13 +3046,67 @@
2639
3046
  },
2640
3047
  showVisual: function()
2641
3048
  {
3049
+ var html;
3050
+
2642
3051
  if (this.opts.visual) return;
2643
3052
 
2644
- var html = this.$textarea.hide().val();
3053
+ var start = 0, end = 0;
3054
+
3055
+ if (this.opts.codemirror)
3056
+ {
3057
+ var selection;
3058
+
3059
+ this.$textarea.next('.CodeMirror').each(function(i, el)
3060
+ {
3061
+ selection = el.CodeMirror.listSelections();
3062
+
3063
+ start = el.CodeMirror.indexFromPos(selection[0].anchor);
3064
+ end = el.CodeMirror.indexFromPos(selection[0].head);
3065
+
3066
+ html = el.CodeMirror.getValue();
3067
+ });
3068
+ }
3069
+ else
3070
+ {
3071
+ start = this.$textarea.get(0).selectionStart;
3072
+ end = this.$textarea.get(0).selectionEnd;
3073
+
3074
+ html = this.$textarea.hide().val();
3075
+ }
3076
+
3077
+ // if selection starts from end
3078
+ if (start > end && end > 0)
3079
+ {
3080
+ var tempStart = end;
3081
+ var tempEnd = start;
3082
+
3083
+ start = tempStart;
3084
+ end = tempEnd;
3085
+ }
3086
+
3087
+ start = this.code.enlargeOffset(html, start);
3088
+ end = this.code.enlargeOffset(html, end);
3089
+
3090
+ html = html.substr(0, start) + this.selection.getMarkerAsHtml(1) + html.substr(start);
3091
+
3092
+ if (end > start)
3093
+ {
3094
+ var markerLength = this.selection.getMarkerAsHtml(1).toString().length;
3095
+
3096
+ html = html.substr(0, end + markerLength) + this.selection.getMarkerAsHtml(2) + html.substr(end + markerLength);
3097
+ }
3098
+
3099
+
2645
3100
 
2646
3101
  if (this.modified !== this.clean.removeSpaces(html))
2647
3102
  {
2648
3103
  this.code.set(html);
3104
+
3105
+ }
3106
+
3107
+ if (this.opts.codemirror)
3108
+ {
3109
+ this.$textarea.next('.CodeMirror').hide();
2649
3110
  }
2650
3111
 
2651
3112
  this.$editor.show();
@@ -2655,15 +3116,15 @@
2655
3116
  this.placeholder.remove();
2656
3117
  }
2657
3118
 
2658
- this.caret.setOffset(this.code.offset);
3119
+ this.selection.restore();
2659
3120
 
2660
3121
  this.$textarea.off('keydown.redactor-textarea-indenting');
2661
3122
 
2662
3123
  this.button.setActiveInVisual();
2663
3124
  this.button.setInactive('html');
2664
-
2665
3125
  this.observe.load();
2666
3126
  this.opts.visual = true;
3127
+ this.core.setCallback('visual', html);
2667
3128
  },
2668
3129
  textareaIndenting: function(e)
2669
3130
  {
@@ -2675,6 +3136,35 @@
2675
3136
  $el.get(0).selectionStart = $el.get(0).selectionEnd = start + 1;
2676
3137
 
2677
3138
  return false;
3139
+ },
3140
+ enlargeOffset: function(html, offset)
3141
+ {
3142
+ var htmlLength = html.length;
3143
+ var c = 0;
3144
+
3145
+ if (html[offset] == '>')
3146
+ {
3147
+ c++;
3148
+ }
3149
+ else
3150
+ {
3151
+ for(var i = offset; i <= htmlLength; i++)
3152
+ {
3153
+ c++;
3154
+
3155
+ if (html[i] == '>')
3156
+ {
3157
+ break;
3158
+ }
3159
+ else if (html[i] == '<' || i == htmlLength)
3160
+ {
3161
+ c = 0;
3162
+ break;
3163
+ }
3164
+ }
3165
+ }
3166
+
3167
+ return offset + c;
2678
3168
  }
2679
3169
  };
2680
3170
  },
@@ -2715,7 +3205,31 @@
2715
3205
  },
2716
3206
  setCallback: function(type, e, data)
2717
3207
  {
2718
- var callback = this.opts[type + 'Callback'];
3208
+ var eventName = type + 'Callback';
3209
+ var eventNamespace = 'redactor';
3210
+ var callback = this.opts[eventName];
3211
+
3212
+ if (this.$textarea)
3213
+ {
3214
+ var returnValue = false;
3215
+ var events = $._data(this.$textarea[0], 'events');
3216
+
3217
+ if (typeof events != 'undefined' && typeof events[eventName] != 'undefined')
3218
+ {
3219
+ $.each(events[eventName], $.proxy(function(key, value)
3220
+ {
3221
+ if (value['namespace'] == eventNamespace)
3222
+ {
3223
+ var data = (typeof data == 'undefined') ? [e] : [e, data];
3224
+
3225
+ returnValue = (typeof data == 'undefined') ? value.handler.call(this, e) : value.handler.call(this, e, data);
3226
+ }
3227
+ }, this));
3228
+ }
3229
+
3230
+ if (returnValue) return returnValue;
3231
+ }
3232
+
2719
3233
  if ($.isFunction(callback))
2720
3234
  {
2721
3235
  return (typeof data == 'undefined') ? callback.call(this, e) : callback.call(this, e, data);
@@ -2727,18 +3241,42 @@
2727
3241
  },
2728
3242
  destroy: function()
2729
3243
  {
3244
+ this.opts.destroyed = true;
3245
+
2730
3246
  this.core.setCallback('destroy');
2731
3247
 
2732
3248
  // off events and remove data
2733
3249
  this.$element.off('.redactor').removeData('redactor');
2734
3250
  this.$editor.off('.redactor');
2735
3251
 
3252
+ $(document).off('mousedown.redactor-blur.' + this.uuid);
3253
+ $(document).off('mousedown.redactor.' + this.uuid);
3254
+ $(document).off('click.redactor-image-delete.' + this.uuid);
3255
+ $(document).off('click.redactor-image-resize-hide.' + this.uuid);
3256
+ $(document).off('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid);
3257
+ $("body").off('scroll.redactor.' + this.uuid);
3258
+ $(this.opts.toolbarFixedTarget).off('scroll.redactor.' + this.uuid);
3259
+
2736
3260
  // common
2737
3261
  this.$editor.removeClass('redactor-editor redactor-linebreaks redactor-placeholder');
2738
3262
  this.$editor.removeAttr('contenteditable');
2739
3263
 
2740
3264
  var html = this.code.get();
2741
3265
 
3266
+ if (this.opts.toolbar)
3267
+ {
3268
+ // dropdowns off
3269
+ this.$toolbar.find('a').each(function()
3270
+ {
3271
+ var $el = $(this);
3272
+ if ($el.data('dropdown'))
3273
+ {
3274
+ $el.data('dropdown').remove();
3275
+ $el.data('dropdown', {});
3276
+ }
3277
+ });
3278
+ }
3279
+
2742
3280
  if (this.build.isTextarea())
2743
3281
  {
2744
3282
  this.$box.after(this.$element);
@@ -2760,11 +3298,10 @@
2760
3298
  if (this.$modalOverlay) this.$modalOverlay.remove();
2761
3299
 
2762
3300
  // buttons tooltip
2763
- $('.redactor-toolbar-tooltip').remove();
3301
+ $('.redactor-toolbar-tooltip-' + this.uuid).remove();
2764
3302
 
2765
3303
  // autosave
2766
3304
  clearInterval(this.autosaveInterval);
2767
-
2768
3305
  }
2769
3306
  };
2770
3307
  },
@@ -2777,21 +3314,31 @@
2777
3314
  {
2778
3315
  $.each(this.opts.formattingAdd, $.proxy(function(i,s)
2779
3316
  {
2780
- var name = s.tag;
2781
- if (typeof s.class != 'undefined')
3317
+ var name = s.tag,
3318
+ func;
3319
+
3320
+ if (typeof s['class'] != 'undefined')
2782
3321
  {
2783
- name = name + '-' + s.class;
3322
+ name = name + '-' + s['class'];
2784
3323
  }
2785
3324
 
2786
3325
  s.type = (this.utils.isBlockTag(s.tag)) ? 'block' : 'inline';
2787
- var func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting';
3326
+
3327
+ if (typeof s.func !== "undefined")
3328
+ {
3329
+ func = s.func;
3330
+ }
3331
+ else
3332
+ {
3333
+ func = (s.type == 'inline') ? 'inline.formatting' : 'block.formatting';
3334
+ }
2788
3335
 
2789
3336
  if (this.opts.linebreaks && s.type == 'block' && s.tag == 'p') return;
2790
3337
 
2791
3338
  this.formatting[name] = {
2792
3339
  tag: s.tag,
2793
3340
  style: s.style,
2794
- 'class': s.class,
3341
+ 'class': s['class'],
2795
3342
  attr: s.attr,
2796
3343
  data: s.data,
2797
3344
  clear: s.clear
@@ -2803,12 +3350,11 @@
2803
3350
  };
2804
3351
 
2805
3352
  }, this));
2806
-
2807
3353
  }
2808
3354
 
2809
3355
  $.each(dropdownObject, $.proxy(function(btnName, btnObject)
2810
3356
  {
2811
- var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '">' + btnObject.title + '</a>');
3357
+ var $item = $('<a href="#" class="redactor-dropdown-' + btnName + '" role="button">' + btnObject.title + '</a>');
2812
3358
  if (name == 'formatting') $item.addClass('redactor-formatting-' + btnName);
2813
3359
 
2814
3360
  $item.on('click', $.proxy(function(e)
@@ -2828,11 +3374,15 @@
2828
3374
  callback = btnObject.dropdown;
2829
3375
  }
2830
3376
 
3377
+ if ($(e.target).hasClass('redactor-dropdown-link-inactive')) return;
3378
+
2831
3379
  this.button.onClick(e, btnName, type, callback);
2832
3380
  this.dropdown.hideAll();
2833
3381
 
2834
3382
  }, this));
2835
3383
 
3384
+ this.observe.addDropdown($item, btnName, btnObject);
3385
+
2836
3386
  $dropdown.append($item);
2837
3387
 
2838
3388
  }, this));
@@ -2850,10 +3400,9 @@
2850
3400
  // Always re-append it to the end of <body> so it always has the highest sub-z-index.
2851
3401
  var $dropdown = $button.data('dropdown').appendTo(document.body);
2852
3402
 
2853
- // ios keyboard hide
2854
- if (this.utils.isMobile() && !this.utils.browser('msie'))
3403
+ if (this.opts.highContrast)
2855
3404
  {
2856
- document.activeElement.blur();
3405
+ $dropdown.addClass("redactor-dropdown-contrast");
2857
3406
  }
2858
3407
 
2859
3408
  if ($button.hasClass('dropact'))
@@ -2863,6 +3412,8 @@
2863
3412
  else
2864
3413
  {
2865
3414
  this.dropdown.hideAll();
3415
+ this.observe.dropdowns();
3416
+
2866
3417
  this.core.setCallback('dropdownShow', { dropdown: $dropdown, key: key, button: $button });
2867
3418
 
2868
3419
  this.button.setActive(key);
@@ -2898,47 +3449,58 @@
2898
3449
  $dropdown.css({ position: 'absolute', left: left, top: top }).show();
2899
3450
  }
2900
3451
 
2901
-
2902
3452
  this.core.setCallback('dropdownShown', { dropdown: $dropdown, key: key, button: $button });
2903
- }
2904
-
2905
- $(document).one('click', $.proxy(this.dropdown.hide, this));
2906
- this.$editor.one('click', $.proxy(this.dropdown.hide, this));
2907
3453
 
2908
- // disable scroll whan dropdown scroll
2909
- var $body = $(document.body);
2910
- var width = $body.width();
2911
-
2912
- $dropdown.on('mouseover', function() {
2913
-
2914
- $body.addClass('body-redactor-hidden');
2915
- $body.css('margin-right', ($body.width() - width) + 'px');
2916
-
2917
- });
2918
-
2919
- $dropdown.on('mouseout', function() {
3454
+ this.$dropdown = $dropdown;
3455
+ }
2920
3456
 
2921
- $body.removeClass('body-redactor-hidden').css('margin-right', 0);
2922
3457
 
2923
- });
3458
+ $(document).one('click.redactor-dropdown', $.proxy(this.dropdown.hide, this));
3459
+ this.$editor.one('click.redactor-dropdown', $.proxy(this.dropdown.hide, this));
3460
+ $(document).one('keyup.redactor-dropdown', $.proxy(this.dropdown.closeHandler, this));
2924
3461
 
3462
+ // disable scroll whan dropdown scroll
3463
+ $dropdown.on('mouseover.redactor-dropdown', $.proxy(this.utils.disableBodyScroll, this)).on('mouseout.redactor-dropdown', $.proxy(this.utils.enableBodyScroll, this));
2925
3464
 
2926
3465
  e.stopPropagation();
2927
3466
  },
3467
+ closeHandler: function(e)
3468
+ {
3469
+ if (e.which != this.keyCode.ESC) return;
3470
+
3471
+ this.dropdown.hideAll();
3472
+ this.$editor.focus();
3473
+ },
2928
3474
  hideAll: function()
2929
3475
  {
2930
3476
  this.$toolbar.find('a.dropact').removeClass('redactor-act').removeClass('dropact');
2931
3477
 
2932
- $(document.body).removeClass('body-redactor-hidden').css('margin-right', 0);
2933
- $('.redactor-dropdown').hide();
2934
- this.core.setCallback('dropdownHide');
3478
+ this.utils.enableBodyScroll();
3479
+
3480
+ $('.redactor-dropdown-' + this.uuid).hide();
3481
+ $('.redactor-dropdown-link-selected').removeClass('redactor-dropdown-link-selected');
3482
+
3483
+
3484
+ if (this.$dropdown)
3485
+ {
3486
+ this.$dropdown.off('.redactor-dropdown');
3487
+ this.core.setCallback('dropdownHide', this.$dropdown);
3488
+
3489
+ this.$dropdown = false;
3490
+ }
2935
3491
  },
2936
3492
  hide: function (e)
2937
3493
  {
2938
3494
  var $dropdown = $(e.target);
2939
- if (!$dropdown.hasClass('dropact'))
3495
+
3496
+ if (!$dropdown.hasClass('dropact') && !$dropdown.hasClass('redactor-dropdown-link-inactive'))
2940
3497
  {
2941
- $dropdown.removeClass('dropact');
3498
+ if ($dropdown.hasClass('redactor-dropdown'))
3499
+ {
3500
+ $dropdown.removeClass('dropact');
3501
+ $dropdown.off('mouseover mouseout');
3502
+ }
3503
+
2942
3504
  this.dropdown.hideAll();
2943
3505
  }
2944
3506
  }
@@ -3032,8 +3594,7 @@
3032
3594
 
3033
3595
  if (first[0].tagName == 'UL' || first[0].tagName == 'OL')
3034
3596
  {
3035
- first = first.find('li').first();
3036
- var child = first.children().first();
3597
+ var child = first.find('li').first();
3037
3598
  if (!this.utils.isBlock(child) && child.text() === '')
3038
3599
  {
3039
3600
  // empty inline tag in li
@@ -3057,34 +3618,31 @@
3057
3618
  },
3058
3619
  setEnd: function()
3059
3620
  {
3060
- if (this.utils.browser('mozilla') || this.utils.browser('msie'))
3621
+ var last = this.$editor.children().last();
3622
+ this.$editor.focus();
3623
+
3624
+ if (last.size() === 0) return;
3625
+ if (this.utils.isEmpty(this.$editor.html()))
3061
3626
  {
3062
- var last = this.$editor.children().last();
3063
- this.caret.setEnd(last);
3627
+
3628
+ this.selection.get();
3629
+ this.range.collapse(true);
3630
+ this.range.setStartAfter(last[0]);
3631
+ this.range.setEnd(last[0], 0);
3632
+ this.selection.addRange();
3064
3633
  }
3065
3634
  else
3066
3635
  {
3067
3636
  this.selection.get();
3637
+ this.range.selectNodeContents(last[0]);
3638
+ this.range.collapse(false);
3639
+ this.selection.addRange();
3068
3640
 
3069
- try {
3070
- this.range.selectNodeContents(this.$editor[0]);
3071
- this.range.collapse(false);
3072
-
3073
- this.selection.addRange();
3074
- }
3075
- catch (e) {}
3076
3641
  }
3077
-
3078
3642
  },
3079
3643
  isFocused: function()
3080
3644
  {
3081
- var focusNode = document.getSelection().focusNode;
3082
- if (focusNode === null) return false;
3083
-
3084
- if (this.opts.linebreaks && $(focusNode.parentNode).hasClass('redactor-linebreaks')) return true;
3085
- else if (!this.utils.isRedactorParent(focusNode.parentNode)) return false;
3086
-
3087
- return this.$editor.is(':focus');
3645
+ return this.$editor[0] === document.activeElement;
3088
3646
  }
3089
3647
  };
3090
3648
  },
@@ -3102,7 +3660,7 @@
3102
3660
  },
3103
3661
  showEdit: function($image)
3104
3662
  {
3105
- var $link = $image.closest('a');
3663
+ var $link = $image.closest('a', this.$editor[0]);
3106
3664
 
3107
3665
  this.modal.load('imageEdit', this.lang.get('edit'), 705);
3108
3666
 
@@ -3122,6 +3680,9 @@
3122
3680
 
3123
3681
  }, this));
3124
3682
 
3683
+ // hide link's tooltip
3684
+ $('.redactor-link-tooltip').remove();
3685
+
3125
3686
  $('#redactor-image-title').val($image.attr('alt'));
3126
3687
 
3127
3688
  if (!this.opts.imageLink) $('.redactor-image-link-option').hide();
@@ -3145,6 +3706,7 @@
3145
3706
  }
3146
3707
 
3147
3708
  this.modal.show();
3709
+ $('#redactor-image-title').focus();
3148
3710
 
3149
3711
  },
3150
3712
  setFloating: function($image)
@@ -3179,14 +3741,16 @@
3179
3741
  this.image.hideResize();
3180
3742
  this.buffer.set();
3181
3743
 
3182
- var $link = $image.closest('a');
3744
+ var $link = $image.closest('a', this.$editor[0]);
3183
3745
 
3184
- $image.attr('alt', $('#redactor-image-title').val());
3746
+ var title = $('#redactor-image-title').val().replace(/(<([^>]+)>)/ig,"");
3747
+ $image.attr('alt', title);
3185
3748
 
3186
3749
  this.image.setFloating($image);
3187
3750
 
3188
3751
  // as link
3189
3752
  var link = $.trim($('#redactor-image-link').val());
3753
+ var link = link.replace(/(<([^>]+)>)/ig,"");
3190
3754
  if (link !== '')
3191
3755
  {
3192
3756
  // test url (add protocol)
@@ -3240,17 +3804,14 @@
3240
3804
  $image.on('dragstart', $.proxy(this.image.onDrag, this));
3241
3805
  }
3242
3806
 
3243
- $image.on('mousedown', $.proxy(this.image.hideResize, this));
3244
- $image.on('click touchstart', $.proxy(function(e)
3807
+ var handler = $.proxy(function(e)
3245
3808
  {
3246
- this.observe.image = $image;
3247
3809
 
3248
- if (this.$editor.find('#redactor-image-box').length !== 0) return false;
3810
+ this.observe.image = $image;
3249
3811
 
3250
3812
  this.image.resizer = this.image.loadEditableControls($image);
3251
3813
 
3252
- $(document).on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this));
3253
- this.$editor.on('click.redactor-image-resize-hide', $.proxy(this.image.hideResize, this));
3814
+ $(document).on('mousedown.redactor-image-resize-hide.' + this.uuid, $.proxy(this.image.hideResize, this));
3254
3815
 
3255
3816
  // resize
3256
3817
  if (!this.opts.imageResizable) return;
@@ -3260,8 +3821,11 @@
3260
3821
  this.image.setResizable(e, $image);
3261
3822
  }, this));
3262
3823
 
3824
+ }, this);
3825
+
3263
3826
 
3264
- }, this));
3827
+ $image.off('mousedown.redactor').on('mousedown.redactor', $.proxy(this.image.hideResize, this));
3828
+ $image.off('click.redactor touchstart.redactor').on('click.redactor touchstart.redactor', handler);
3265
3829
  },
3266
3830
  setResizable: function(e, $image)
3267
3831
  {
@@ -3307,8 +3871,11 @@
3307
3871
 
3308
3872
  if (height < 50 || width < 100) return;
3309
3873
 
3874
+ var height = Math.round(this.image.resizeHandle.el.width() / this.image.resizeHandle.ratio);
3875
+
3876
+ this.image.resizeHandle.el.attr({width: width, height: height});
3310
3877
  this.image.resizeHandle.el.width(width);
3311
- this.image.resizeHandle.el.height(this.image.resizeHandle.el.width()/this.image.resizeHandle.ratio);
3878
+ this.image.resizeHandle.el.height(height);
3312
3879
 
3313
3880
  this.code.sync();
3314
3881
  },
@@ -3352,7 +3919,7 @@
3352
3919
  },
3353
3920
  hideResize: function(e)
3354
3921
  {
3355
- if (e && $(e.target).closest('#redactor-image-box').length !== 0) return;
3922
+ if (e && $(e.target).closest('#redactor-image-box', this.$editor[0]).length !== 0) return;
3356
3923
  if (e && e.target.tagName == 'IMG')
3357
3924
  {
3358
3925
  var $image = $(e.target);
@@ -3362,12 +3929,8 @@
3362
3929
  var imageBox = this.$editor.find('#redactor-image-box');
3363
3930
  if (imageBox.length === 0) return;
3364
3931
 
3365
- if (this.opts.imageEditable)
3366
- {
3367
- this.image.editter.remove();
3368
- }
3369
-
3370
- $(this.image.resizer).remove();
3932
+ $('#redactor-image-editter').remove();
3933
+ $('#redactor-image-resizer').remove();
3371
3934
 
3372
3935
  imageBox.find('img').css({
3373
3936
  marginTop: imageBox[0].style.marginTop,
@@ -3383,8 +3946,8 @@
3383
3946
  return $(this).contents();
3384
3947
  });
3385
3948
 
3386
- $(document).off('click.redactor-image-resize-hide');
3387
- this.$editor.off('click.redactor-image-resize-hide');
3949
+ $(document).off('mousedown.redactor-image-resize-hide.' + this.uuid);
3950
+
3388
3951
 
3389
3952
  if (typeof this.image.resizeHandle !== 'undefined')
3390
3953
  {
@@ -3464,8 +4027,8 @@
3464
4027
  remove: function(image)
3465
4028
  {
3466
4029
  var $image = $(image);
3467
- var $link = $image.closest('a');
3468
- var $figure = $image.closest('figure');
4030
+ var $link = $image.closest('a', this.$editor[0]);
4031
+ var $figure = $image.closest('figure', this.$editor[0]);
3469
4032
  var $parent = $image.parent();
3470
4033
  if ($('#redactor-image-box').length !== 0)
3471
4034
  {
@@ -3560,7 +4123,12 @@
3560
4123
  }
3561
4124
  else if (this.opts.linebreaks)
3562
4125
  {
3563
- $image.before('<br>').after('<br>');
4126
+ if (!this.utils.isEmpty(this.code.get()))
4127
+ {
4128
+ $image.before('<br>');
4129
+ }
4130
+
4131
+ $image.after('<br>');
3564
4132
  }
3565
4133
 
3566
4134
  if (typeof json == 'string') return;
@@ -3644,18 +4212,12 @@
3644
4212
  this.selection.restore();
3645
4213
  this.code.sync();
3646
4214
  },
3647
- decreaseLists: function ()
4215
+ decreaseLists: function()
3648
4216
  {
3649
4217
  document.execCommand('outdent');
3650
4218
 
3651
4219
  var current = this.selection.getCurrent();
3652
-
3653
- var $item = $(current).closest('li');
3654
- var $parent = $item.parent();
3655
- if ($item.length !== 0 && $parent.length !== 0 && $parent[0].tagName == 'LI')
3656
- {
3657
- $parent.after($item);
3658
- }
4220
+ var $item = $(current).closest('li', this.$editor[0]);
3659
4221
 
3660
4222
  this.indent.fixEmptyIndent();
3661
4223
 
@@ -3714,7 +4276,7 @@
3714
4276
  var type, value;
3715
4277
 
3716
4278
  if (typeof this.formatting[name].style != 'undefined') type = 'style';
3717
- else if (typeof this.formatting[name].class != 'undefined') type = 'class';
4279
+ else if (typeof this.formatting[name]['class'] != 'undefined') type = 'class';
3718
4280
 
3719
4281
  if (type) value = this.formatting[name][type];
3720
4282
 
@@ -3723,6 +4285,9 @@
3723
4285
  },
3724
4286
  format: function(tag, type, value)
3725
4287
  {
4288
+ var current = this.selection.getCurrent();
4289
+ if (current && current.tagName === 'TR') return;
4290
+
3726
4291
  // Stop formatting pre and headers
3727
4292
  if (this.utils.isCurrentOrParent('PRE') || this.utils.isCurrentOrParentHeader()) return;
3728
4293
 
@@ -3734,11 +4299,25 @@
3734
4299
  if (tag == tags[i]) tag = replaced[i];
3735
4300
  }
3736
4301
 
4302
+
4303
+ if (this.opts.allowedTags)
4304
+ {
4305
+ if ($.inArray(tag, this.opts.allowedTags) == -1) return;
4306
+ }
4307
+ else
4308
+ {
4309
+ if ($.inArray(tag, this.opts.deniedTags) !== -1) return;
4310
+ }
4311
+
3737
4312
  this.inline.type = type || false;
3738
4313
  this.inline.value = value || false;
3739
4314
 
3740
4315
  this.buffer.set();
3741
- this.$editor.focus();
4316
+
4317
+ if (!this.utils.browser('msie') && !this.opts.linebreaks)
4318
+ {
4319
+ this.$editor.focus();
4320
+ }
3742
4321
 
3743
4322
  this.selection.get();
3744
4323
 
@@ -3754,19 +4333,23 @@
3754
4333
  formatCollapsed: function(tag)
3755
4334
  {
3756
4335
  var current = this.selection.getCurrent();
3757
- var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']');
4336
+ var $parent = $(current).closest(tag + '[data-redactor-tag=' + tag + ']', this.$editor[0]);
3758
4337
 
3759
4338
  // inline there is
3760
4339
  if ($parent.length !== 0 && (this.inline.type != 'style' && $parent[0].tagName != 'SPAN'))
3761
4340
  {
3762
- this.caret.setAfter($parent[0]);
3763
-
3764
4341
  // remove empty
3765
- if ( this.utils.isEmpty($parent.text()))
4342
+ if (this.utils.isEmpty($parent.text()))
3766
4343
  {
4344
+ this.caret.setAfter($parent[0]);
4345
+
3767
4346
  $parent.remove();
3768
4347
  this.code.sync();
3769
4348
  }
4349
+ else if (this.utils.isEndOfElement($parent))
4350
+ {
4351
+ this.caret.setAfter($parent[0]);
4352
+ }
3770
4353
 
3771
4354
  return;
3772
4355
  }
@@ -3807,11 +4390,17 @@
3807
4390
  }
3808
4391
 
3809
4392
  $el.replaceWith($span.html($el.contents()));
4393
+ var $parent = $span.parent();
4394
+
4395
+ // remove U tag if selected link + node
4396
+ if ($span[0].tagName === 'A' && $parent && $parent[0].tagName === 'U')
4397
+ {
4398
+ $span.parent().replaceWith($span);
4399
+ }
3810
4400
 
3811
4401
  if (tag == 'span')
3812
4402
  {
3813
- var $parent = $span.parent();
3814
- if ($parent && $parent[0].tagName == 'SPAN' && this.inline.type == 'style')
4403
+ if ($parent && $parent[0].tagName === 'SPAN' && this.inline.type === 'style')
3815
4404
  {
3816
4405
  var arr = this.inline.value.split(';');
3817
4406
 
@@ -3839,8 +4428,16 @@
3839
4428
  this.$editor.find(this.opts.inlineTags.join(', ')).each($.proxy(function(i,s)
3840
4429
  {
3841
4430
  var $el = $(s);
4431
+
4432
+
4433
+ if (s.tagName === 'U' && s.attributes.length === 0)
4434
+ {
4435
+ $el.replaceWith($el.contents());
4436
+ return;
4437
+ }
4438
+
3842
4439
  var property = $el.css('text-decoration');
3843
- if (property == 'line-through')
4440
+ if (property === 'line-through')
3844
4441
  {
3845
4442
  $el.css('text-decoration', '');
3846
4443
  this.utils.removeEmptyAttr($el, 'style');
@@ -3857,6 +4454,15 @@
3857
4454
  });
3858
4455
  }
3859
4456
 
4457
+ if (tag != 'u')
4458
+ {
4459
+ var _this = this;
4460
+ this.$editor.find('unline').each(function(i,s)
4461
+ {
4462
+ _this.utils.replaceToTag(s, 'u');
4463
+ });
4464
+ }
4465
+
3860
4466
  this.selection.restore();
3861
4467
  this.code.sync();
3862
4468
 
@@ -3916,6 +4522,14 @@
3916
4522
  });
3917
4523
  }
3918
4524
 
4525
+ if (tag != 'u')
4526
+ {
4527
+ this.$editor.find('u').each(function(i,s)
4528
+ {
4529
+ self.utils.replaceToTag(s, 'unline');
4530
+ });
4531
+ }
4532
+
3919
4533
  if (tag != 'span')
3920
4534
  {
3921
4535
  this.$editor.find(tag).each(function()
@@ -4155,7 +4769,10 @@
4155
4769
 
4156
4770
  if (typeof clean == 'undefined') clean = true;
4157
4771
 
4158
- this.$editor.focus();
4772
+ if (!this.opts.linebreaks)
4773
+ {
4774
+ this.$editor.focus();
4775
+ }
4159
4776
 
4160
4777
  html = this.clean.setVerified(html);
4161
4778
 
@@ -4250,6 +4867,7 @@
4250
4867
  {
4251
4868
  node = node[0] || node;
4252
4869
 
4870
+ var offset = this.caret.getOffset();
4253
4871
  var html = this.utils.getOuterHtml(node);
4254
4872
  html = this.clean.setVerified(html);
4255
4873
 
@@ -4269,6 +4887,8 @@
4269
4887
  this.range.collapse(false);
4270
4888
  this.selection.addRange();
4271
4889
 
4890
+ this.caret.setOffset(offset);
4891
+
4272
4892
  return node;
4273
4893
  },
4274
4894
  nodeToPoint: function(node, x, y)
@@ -4341,6 +4961,7 @@
4341
4961
  var parHtml = $(parent).html();
4342
4962
 
4343
4963
  parHtml = '<p>' + parHtml.replace(/<span class="redactor-ie-paste"><\/span>/gi, '</p>' + html + '<p>') + '</p>';
4964
+ parHtml = parHtml.replace(/<p><\/p>/gi, '');
4344
4965
  $(parent).replaceWith(parHtml);
4345
4966
  },
4346
4967
  ie11PasteFrag: function(html)
@@ -4358,6 +4979,8 @@
4358
4979
  }
4359
4980
 
4360
4981
  this.range.insertNode(frag);
4982
+ this.range.collapse(false);
4983
+ this.selection.addRange();
4361
4984
  }
4362
4985
  };
4363
4986
  },
@@ -4384,8 +5007,12 @@
4384
5007
  // shortcuts setup
4385
5008
  this.shortcuts.init(e, key);
4386
5009
 
4387
- this.keydown.checkEvents(arrow, key);
4388
- this.keydown.setupBuffer(e, key);
5010
+ if (this.utils.isDesktop())
5011
+ {
5012
+ this.keydown.checkEvents(arrow, key);
5013
+ this.keydown.setupBuffer(e, key);
5014
+ }
5015
+
4389
5016
  this.keydown.addArrowsEvent(arrow);
4390
5017
  this.keydown.setupSelectAll(e, key);
4391
5018
 
@@ -4404,7 +5031,7 @@
4404
5031
  var $table = false;
4405
5032
  if (this.keydown.block && this.keydown.block.tagName === 'TD')
4406
5033
  {
4407
- $table = $(this.keydown.block).closest('table');
5034
+ $table = $(this.keydown.block).closest('table', this.$editor[0]);
4408
5035
  }
4409
5036
 
4410
5037
  if ($table && $table.find('td').last()[0] === this.keydown.block)
@@ -4478,41 +5105,63 @@
4478
5105
  current = this.selection.getCurrent();
4479
5106
  $next = $(this.keydown.current).next();
4480
5107
 
4481
- if (current !== false && $(current).hasClass('redactor-invisible-space'))
5108
+ if ($next.length !== 0 && $next[0].tagName == 'BR')
5109
+ {
5110
+ return this.keydown.insertBreakLine(e);
5111
+ }
5112
+ else if (current !== false && $(current).hasClass('redactor-invisible-space'))
4482
5113
  {
4483
5114
  this.caret.setAfter(current);
4484
5115
  $(current).contents().unwrap();
5116
+
4485
5117
  return this.keydown.insertDblBreakLine(e);
4486
5118
  }
4487
5119
  else
4488
5120
  {
4489
- if ($next.length === 0 && current === false && typeof $next.context != 'undefined')
5121
+ if (this.utils.isEndOfEditor())
4490
5122
  {
4491
5123
  return this.keydown.insertDblBreakLine(e);
4492
5124
  }
4493
- else if (this.utils.isEndOfEditor())
5125
+ else if ($next.length === 0 && current === false && typeof $next.context != 'undefined')
4494
5126
  {
4495
- return this.keydown.insertDblBreakLine(e);
5127
+ return this.keydown.insertBreakLine(e);
4496
5128
  }
4497
5129
 
4498
5130
  return this.keydown.insertBreakLine(e);
4499
5131
  }
5132
+
4500
5133
  }
4501
5134
  else if (this.opts.linebreaks && this.keydown.block)
4502
5135
  {
4503
5136
  setTimeout($.proxy(this.keydown.replaceDivToBreakLine, this), 1);
4504
5137
  }
4505
5138
  // paragraphs
4506
- else if (!this.opts.linebreaks && this.keydown.block && this.keydown.block.tagName !== 'LI')
5139
+ else if (!this.opts.linebreaks && this.keydown.block)
4507
5140
  {
4508
5141
  setTimeout($.proxy(this.keydown.replaceDivToParagraph, this), 1);
5142
+
5143
+ if (this.keydown.block.tagName === 'LI')
5144
+ {
5145
+ current = this.selection.getCurrent();
5146
+ var $parent = $(current).closest('li', this.$editor[0]);
5147
+ var $list = $parent.closest('ul,ol', this.$editor[0]);
5148
+
5149
+ if ($parent.length !== 0 && this.utils.isEmpty($parent.html()) && $list.next().length === 0 && this.utils.isEmpty($list.find("li").last().html()))
5150
+ {
5151
+ $list.find("li").last().remove();
5152
+
5153
+ var node = $(this.opts.emptyHtml);
5154
+ $list.after(node);
5155
+ this.caret.setStart(node);
5156
+
5157
+ return false;
5158
+ }
5159
+ }
4509
5160
  }
4510
5161
  else if (!this.opts.linebreaks && !this.keydown.block)
4511
5162
  {
4512
5163
  return this.keydown.insertParagraph(e);
4513
5164
  }
4514
-
4515
-
4516
5165
  }
4517
5166
 
4518
5167
  // Shift+Enter or Ctrl+Enter
@@ -4532,6 +5181,7 @@
4532
5181
  if (key === this.keyCode.BACKSPACE || key === this.keyCode.DELETE)
4533
5182
  {
4534
5183
  var nodes = this.selection.getNodes();
5184
+
4535
5185
  if (nodes)
4536
5186
  {
4537
5187
  var len = nodes.length;
@@ -4568,9 +5218,28 @@
4568
5218
  // backspace
4569
5219
  if (key === this.keyCode.BACKSPACE)
4570
5220
  {
4571
- this.keydown.removeInvisibleSpace();
4572
- this.keydown.removeEmptyListInTable(e);
4573
- }
5221
+ // backspace as outdent
5222
+ var block = this.selection.getBlock();
5223
+ var indented = ($(block).css('margin-left') !== '0px');
5224
+ if (block && indented && this.range.collapsed && this.utils.isStartOfElement())
5225
+ {
5226
+ this.indent.decrease();
5227
+ e.preventDefault();
5228
+ return;
5229
+ }
5230
+
5231
+ // remove hr in FF
5232
+ if (this.utils.browser('mozilla'))
5233
+ {
5234
+ var prev = this.selection.getPrev();
5235
+ var prev2 = $(prev).prev()[0];
5236
+ if (prev && prev.tagName === 'HR') $(prev).remove();
5237
+ if (prev2 && prev2.tagName === 'HR') $(prev2).remove();
5238
+ }
5239
+
5240
+ this.keydown.removeInvisibleSpace();
5241
+ this.keydown.removeEmptyListInTable(e);
5242
+ }
4574
5243
 
4575
5244
  this.code.sync();
4576
5245
  },
@@ -4589,7 +5258,7 @@
4589
5258
  checkKeyEvents: function(key)
4590
5259
  {
4591
5260
  var k = this.keyCode;
4592
- var keys = [k.BACKSPACE, k.DELETE, k.ENTER, k.SPACE, k.ESC, k.TAB, k.CTRL, k.META, k.ALT, k.SHIFT];
5261
+ var keys = [k.BACKSPACE, k.DELETE, k.ENTER, k.ESC, k.TAB, k.CTRL, k.META, k.ALT, k.SHIFT];
4593
5262
 
4594
5263
  return ($.inArray(key, keys) == -1) ? true : false;
4595
5264
 
@@ -4623,7 +5292,7 @@
4623
5292
  }
4624
5293
  else if (!this.keydown.ctrl)
4625
5294
  {
4626
- if (key == this.keyCode.BACKSPACE || key == this.keyCode.DELETE || (key == this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey) || key == this.keyCode.SPACE)
5295
+ if (key == this.keyCode.BACKSPACE || key == this.keyCode.DELETE || (key == this.keyCode.ENTER && !e.ctrlKey && !e.shiftKey))
4627
5296
  {
4628
5297
  this.buffer.set();
4629
5298
  }
@@ -4714,7 +5383,7 @@
4714
5383
  {
4715
5384
  var blockElem = this.selection.getBlock();
4716
5385
  var blockHtml = blockElem.innerHTML.replace(/<br\s?\/?>/gi, '');
4717
- if (blockElem.tagName === 'DIV' && blockHtml === '' && !$(blockElem).hasClass('redactor-editor'))
5386
+ if (blockElem.tagName === 'DIV' && this.utils.isEmpty(blockHtml) && !$(blockElem).hasClass('redactor-editor'))
4718
5387
  {
4719
5388
  var p = document.createElement('p');
4720
5389
  p.innerHTML = this.opts.invisibleSpace;
@@ -4841,24 +5510,71 @@
4841
5510
  e.stopPropagation();
4842
5511
 
4843
5512
  this.selection.get();
5513
+
4844
5514
  var br1 = document.createElement('br');
4845
5515
 
4846
- this.range.deleteContents();
5516
+ if (this.utils.browser('msie'))
5517
+ {
5518
+ this.range.collapse(false);
5519
+ this.range.setEnd(this.range.endContainer, this.range.endOffset);
5520
+ }
5521
+ else
5522
+ {
5523
+ this.range.deleteContents();
5524
+ }
5525
+
4847
5526
  this.range.insertNode(br1);
4848
5527
 
5528
+ // move br outside A tag
5529
+ var $parentA = $(br1).parent("a");
5530
+
5531
+ if ($parentA.length > 0)
5532
+ {
5533
+ $parentA.find(br1).remove();
5534
+ $parentA.after(br1);
5535
+ }
5536
+
4849
5537
  if (dbl === true)
4850
5538
  {
5539
+ var $next = $(br1).next();
5540
+ if ($next.length !== 0 && $next[0].tagName === 'BR' && this.utils.isEndOfEditor())
5541
+ {
5542
+ this.caret.setAfter(br1);
5543
+ this.code.sync();
5544
+ return false;
5545
+ }
5546
+
4851
5547
  var br2 = document.createElement('br');
5548
+
4852
5549
  this.range.insertNode(br2);
4853
5550
  this.caret.setAfter(br2);
4854
5551
  }
4855
5552
  else
4856
5553
  {
4857
- this.caret.setAfter(br1);
5554
+ // caret does not move after the br visual
5555
+ if (this.utils.browser('msie'))
5556
+ {
5557
+ var space = document.createElement('span');
5558
+ space.innerHTML = '&#x200b;';
5559
+
5560
+ $(br1).after(space);
5561
+
5562
+ this.range.setStartAfter(space);
5563
+ this.range.setEndAfter(space);
5564
+ $(space).remove();
5565
+ }
5566
+ else
5567
+ {
5568
+ var range = document.createRange();
5569
+ range.setStartAfter(br1);
5570
+ range.collapse(true);
5571
+ var selection = window.getSelection();
5572
+ selection.removeAllRanges();
5573
+ selection.addRange(range);
5574
+ }
4858
5575
  }
4859
5576
 
4860
5577
  this.code.sync();
4861
-
4862
5578
  return false;
4863
5579
  },
4864
5580
  removeInvisibleSpace: function()
@@ -4873,9 +5589,9 @@
4873
5589
  {
4874
5590
  var $current = $(this.keydown.current);
4875
5591
  var $parent = $(this.keydown.parent);
4876
- var td = $current.closest('td');
5592
+ var td = $current.closest('td', this.$editor[0]);
4877
5593
 
4878
- if (td.length !== 0 && $current.closest('li') && $parent.children('li').length === 1)
5594
+ if (td.length !== 0 && $current.closest('li', this.$editor[0]) && $parent.children('li').length === 1)
4879
5595
  {
4880
5596
  if (!this.utils.isEmpty($current.text())) return;
4881
5597
 
@@ -4894,6 +5610,7 @@
4894
5610
  return {
4895
5611
  init: function(e)
4896
5612
  {
5613
+
4897
5614
  if (this.rtePaste) return;
4898
5615
 
4899
5616
  var key = e.which;
@@ -4902,7 +5619,6 @@
4902
5619
  this.keyup.parent = this.selection.getParent();
4903
5620
  var $parent = this.utils.isRedactorParent($(this.keyup.parent).parent());
4904
5621
 
4905
-
4906
5622
  // callback
4907
5623
  var keyupStop = this.core.setCallback('keyup', e);
4908
5624
  if (keyupStop === false)
@@ -4912,7 +5628,7 @@
4912
5628
  }
4913
5629
 
4914
5630
  // replace to p before / after the table or body
4915
- if (!this.opts.linebreaks && this.keyup.current.nodeType == 3 && this.keyup.current.length <= 1 && (this.keyup.parent === false || this.keyup.parent.tagName == 'BODY'))
5631
+ if (!this.opts.linebreaks && this.keyup.current.nodeType === 3 && this.keyup.current.length <= 1 && (this.keyup.parent === false || this.keyup.parent.tagName == 'BODY'))
4916
5632
  {
4917
5633
  this.keyup.replaceToParagraph();
4918
5634
  }
@@ -4931,16 +5647,20 @@
4931
5647
  }
4932
5648
 
4933
5649
  // linkify
4934
- if (this.keyup.isLinkify(key))
4935
- {
4936
- this.formatLinkify(this.opts.linkProtocol, this.opts.convertLinks, this.opts.convertUrlLinks, this.opts.convertImageLinks, this.opts.convertVideoLinks, this.opts.linkSize);
4937
-
4938
- this.observe.load();
4939
- this.code.sync();
4940
- }
5650
+ if (this.linkify.isEnabled() && this.linkify.isKey(key)) this.linkify.format();
4941
5651
 
4942
5652
  if (key === this.keyCode.DELETE || key === this.keyCode.BACKSPACE)
4943
5653
  {
5654
+ if (this.utils.browser('mozilla'))
5655
+ {
5656
+ var td = $(this.keydown.current).closest('td', this.$editor[0]);
5657
+ if (td.size() !== 0 && td.text() !== '')
5658
+ {
5659
+ e.preventDefault();
5660
+ return false;
5661
+ }
5662
+ }
5663
+
4944
5664
  // clear unverified
4945
5665
  this.clean.clearUnverified();
4946
5666
 
@@ -4958,7 +5678,10 @@
4958
5678
  }
4959
5679
 
4960
5680
  // remove empty paragraphs
4961
- this.$editor.find('p').each($.proxy(this.utils.removeEmpty, this));
5681
+ this.$editor.find('p').each($.proxy(function(i, s)
5682
+ {
5683
+ this.utils.removeEmpty(i, $(s).html());
5684
+ }, this));
4962
5685
 
4963
5686
  // remove invisible space
4964
5687
  if (this.opts.linebreaks && this.keyup.current && this.keyup.current.tagName == 'DIV' && this.utils.isEmpty(this.keyup.current.innerHTML))
@@ -4968,14 +5691,12 @@
4968
5691
  $(this.keyup.current).remove();
4969
5692
  }
4970
5693
 
5694
+ this.keyup.removeEmptyLists();
5695
+
4971
5696
  // if empty
4972
5697
  return this.keyup.formatEmpty(e);
4973
5698
  }
4974
5699
  },
4975
- isLinkify: function(key)
4976
- {
4977
- return this.opts.convertLinks && (this.opts.convertUrlLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && key === this.keyCode.ENTER && !this.utils.isCurrentOrParent('PRE');
4978
- },
4979
5700
  replaceToParagraph: function(clone)
4980
5701
  {
4981
5702
  var $current = $(this.keyup.current);
@@ -4999,6 +5720,20 @@
4999
5720
 
5000
5721
  this.caret.setEnd(node);
5001
5722
  },
5723
+ removeEmptyLists: function()
5724
+ {
5725
+ var removeIt = function()
5726
+ {
5727
+ var html = $.trim(this.innerHTML).replace(/\/t\/n/g, '');
5728
+ if (html === '')
5729
+ {
5730
+ $(this).remove();
5731
+ }
5732
+ };
5733
+
5734
+ this.$editor.find('li').each(removeIt);
5735
+ this.$editor.find('ul, ol').each(removeIt);
5736
+ },
5002
5737
  formatEmpty: function(e)
5003
5738
  {
5004
5739
  var html = $.trim(this.$editor.html());
@@ -5014,9 +5749,7 @@
5014
5749
  }
5015
5750
  else
5016
5751
  {
5017
- html = '<p><br /></p>';
5018
-
5019
- this.$editor.html(html);
5752
+ this.$editor.html(this.opts.emptyHtml);
5020
5753
  this.focus.setStart();
5021
5754
  }
5022
5755
 
@@ -5096,7 +5829,7 @@
5096
5829
  var extra = '<p id="redactor-insert-line"><br /></p>';
5097
5830
  if (this.opts.linebreaks) extra = '<br id="redactor-insert-line">';
5098
5831
 
5099
- document.execCommand('insertHTML', false, '<hr>' + extra);
5832
+ document.execCommand('insertHtml', false, '<hr>' + extra);
5100
5833
 
5101
5834
  this.line.setFocus();
5102
5835
  this.code.sync();
@@ -5105,16 +5838,43 @@
5105
5838
  {
5106
5839
  var node = this.$editor.find('#redactor-insert-line');
5107
5840
  var next = $(node).next()[0];
5841
+ var target = next;
5842
+ if (this.utils.browser('mozilla') && next && next.innerHTML === '')
5843
+ {
5844
+ target = $(next).next()[0];
5845
+ $(next).remove();
5846
+ }
5108
5847
 
5109
- if (next)
5848
+ if (target)
5110
5849
  {
5111
- this.caret.setAfter(node);
5112
5850
  node.remove();
5851
+
5852
+ if (!this.opts.linebreaks)
5853
+ {
5854
+ this.$editor.focus();
5855
+ this.line.setStart(target);
5856
+ }
5857
+
5113
5858
  }
5114
5859
  else
5115
5860
  {
5861
+
5116
5862
  node.removeAttr('id');
5863
+ this.line.setStart(node[0]);
5117
5864
  }
5865
+ },
5866
+ setStart: function(node)
5867
+ {
5868
+ if (typeof node === 'undefined') return;
5869
+
5870
+ var textNode = document.createTextNode('\u200B');
5871
+
5872
+ this.selection.get();
5873
+ this.range.setStart(node, 0);
5874
+ this.range.insertNode(textNode);
5875
+ this.range.collapse(true);
5876
+ this.selection.addRange();
5877
+
5118
5878
  }
5119
5879
  };
5120
5880
  },
@@ -5125,10 +5885,20 @@
5125
5885
  {
5126
5886
  if (typeof e != 'undefined' && e.preventDefault) e.preventDefault();
5127
5887
 
5128
- this.modal.load('link', this.lang.get('link_insert'), 600);
5888
+ if (!this.observe.isCurrent('a'))
5889
+ {
5890
+ this.modal.load('link', this.lang.get('link_insert'), 600);
5891
+ }
5892
+ else
5893
+ {
5894
+ this.modal.load('link', this.lang.get('link_edit'), 600);
5895
+ }
5129
5896
 
5130
5897
  this.modal.createCancelButton();
5131
- this.link.buttonInsert = this.modal.createActionButton(this.lang.get('insert'));
5898
+
5899
+ var buttonText = !this.observe.isCurrent('a') ? this.lang.get('insert') : this.lang.get('edit');
5900
+
5901
+ this.link.buttonInsert = this.modal.createActionButton(buttonText);
5132
5902
 
5133
5903
  this.selection.get();
5134
5904
 
@@ -5156,23 +5926,26 @@
5156
5926
  cleanUrl: function()
5157
5927
  {
5158
5928
  var thref = self.location.href.replace(/\/$/i, '');
5159
- this.link.url = this.link.url.replace(thref, '');
5160
- this.link.url = this.link.url.replace(/^\/#/, '#');
5161
- this.link.url = this.link.url.replace('mailto:', '');
5162
5929
 
5163
- // remove host from href
5164
- if (!this.opts.linkProtocol)
5930
+ if (typeof this.link.url !== "undefined")
5165
5931
  {
5166
- var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i');
5167
- this.link.url = this.link.url.replace(re, '');
5168
- }
5932
+ this.link.url = this.link.url.replace(thref, '');
5933
+ this.link.url = this.link.url.replace(/^\/#/, '#');
5934
+ this.link.url = this.link.url.replace('mailto:', '');
5169
5935
 
5936
+ // remove host from href
5937
+ if (!this.opts.linkProtocol)
5938
+ {
5939
+ var re = new RegExp('^(http|ftp|https)://' + self.location.host, 'i');
5940
+ this.link.url = this.link.url.replace(re, '');
5941
+ }
5942
+ }
5170
5943
  },
5171
5944
  getData: function()
5172
5945
  {
5173
5946
  this.link.$node = false;
5174
5947
 
5175
- var $el = $(this.selection.getCurrent()).closest('a');
5948
+ var $el = $(this.selection.getCurrent()).closest('a', this.$editor[0]);
5176
5949
  if ($el.length !== 0 && $el[0].tagName === 'A')
5177
5950
  {
5178
5951
  this.link.$node = $el;
@@ -5191,9 +5964,11 @@
5191
5964
  },
5192
5965
  insert: function()
5193
5966
  {
5967
+ this.placeholder.remove();
5968
+
5194
5969
  var target = '';
5195
5970
  var link = this.link.$inputUrl.val();
5196
- var text = this.link.$inputText.val();
5971
+ var text = this.link.$inputText.val().replace(/(<([^>]+)>)/ig,"");
5197
5972
 
5198
5973
  if ($.trim(link) === '')
5199
5974
  {
@@ -5210,6 +5985,7 @@
5210
5985
  // mailto
5211
5986
  if (link.search('@') != -1 && /(http|ftp|https):\/\//i.test(link) === false)
5212
5987
  {
5988
+ link = link.replace('mailto:', '');
5213
5989
  link = 'mailto:' + link;
5214
5990
  }
5215
5991
  // url, not anchor
@@ -5239,6 +6015,7 @@
5239
6015
  text = $.trim(text.replace(/<|>/g, ''));
5240
6016
 
5241
6017
  this.selection.restore();
6018
+ var blocks = this.selection.getBlocks();
5242
6019
 
5243
6020
  if (text === '' && link === '') return;
5244
6021
  if (text === '' && link !== '') text = link;
@@ -5247,16 +6024,37 @@
5247
6024
  {
5248
6025
  this.buffer.set();
5249
6026
 
5250
- this.link.$node.text(text).attr('href', link);
6027
+ var $link = this.link.$node,
6028
+ $el = $link.children();
6029
+
6030
+ if ($el.length > 0)
6031
+ {
6032
+ while ($el.length)
6033
+ {
6034
+ $el = $el.children();
6035
+ }
6036
+
6037
+ $el = $el.end();
6038
+ }
6039
+ else
6040
+ {
6041
+ $el = $link;
6042
+ }
6043
+
6044
+ $link.attr('href', link);
6045
+ $el.text(text);
6046
+
5251
6047
  if (target !== '')
5252
6048
  {
5253
- this.link.$node.attr('target', target);
6049
+ $link.attr('target', target);
5254
6050
  }
5255
6051
  else
5256
6052
  {
5257
- this.link.$node.removeAttr('target');
6053
+ $link.removeAttr('target');
5258
6054
  }
5259
6055
 
6056
+ this.selection.selectElement($link);
6057
+
5260
6058
  this.code.sync();
5261
6059
  }
5262
6060
  else
@@ -5266,7 +6064,13 @@
5266
6064
  var $a = $('<a />').attr('href', link).text(text);
5267
6065
  if (target !== '') $a.attr('target', target);
5268
6066
 
5269
- this.insert.node($a);
6067
+ $a = $(this.insert.node($a));
6068
+
6069
+ if (this.opts.linebreaks)
6070
+ {
6071
+ $a.after('&nbsp;');
6072
+ }
6073
+
5270
6074
  this.selection.selectElement($a);
5271
6075
  }
5272
6076
  else
@@ -5278,20 +6082,43 @@
5278
6082
  if (target !== '') $a.attr('target', target);
5279
6083
 
5280
6084
  $a = $(this.insert.node($a));
6085
+
6086
+ if (this.selection.getText().match(/\s$/))
6087
+ {
6088
+ $a.after(" ");
6089
+ }
6090
+
5281
6091
  this.selection.selectElement($a);
5282
6092
  }
5283
6093
  else
5284
6094
  {
5285
6095
  document.execCommand('createLink', false, link);
5286
6096
 
5287
- $a = $(this.selection.getCurrent()).closest('a');
6097
+ $a = $(this.selection.getCurrent()).closest('a', this.$editor[0]);
6098
+ if (this.utils.browser('mozilla'))
6099
+ {
6100
+ $a = $('a[_moz_dirty=""]');
6101
+ }
5288
6102
 
5289
6103
  if (target !== '') $a.attr('target', target);
5290
- $a.removeAttr('style');
6104
+ $a.removeAttr('style').removeAttr('_moz_dirty');
5291
6105
 
5292
- if (this.link.text === '' || this.link.text != text)
6106
+ if (this.selection.getText().match(/\s$/))
5293
6107
  {
5294
- $a.text(text);
6108
+ $a.after(" ");
6109
+ }
6110
+
6111
+ if (this.link.text !== '' || this.link.text != text)
6112
+ {
6113
+ if (!this.opts.linebreaks && blocks && blocks.length <= 1)
6114
+ {
6115
+ $a.text(text);
6116
+ }
6117
+ else if (this.opts.linebreaks)
6118
+ {
6119
+ $a.text(text);
6120
+ }
6121
+
5295
6122
  this.selection.selectElement($a);
5296
6123
  }
5297
6124
  }
@@ -5311,7 +6138,10 @@
5311
6138
  },
5312
6139
  unlink: function(e)
5313
6140
  {
5314
- if (typeof e != 'undefined' && e.preventDefault) e.preventDefault();
6141
+ if (typeof e != 'undefined' && e.preventDefault)
6142
+ {
6143
+ e.preventDefault();
6144
+ }
5315
6145
 
5316
6146
  var nodes = this.selection.getNodes();
5317
6147
  if (!nodes) return;
@@ -5319,20 +6149,201 @@
5319
6149
  this.buffer.set();
5320
6150
 
5321
6151
  var len = nodes.length;
6152
+ var links = [];
5322
6153
  for (var i = 0; i < len; i++)
5323
6154
  {
5324
- if (nodes[i].tagName == 'A')
6155
+ if (nodes[i].tagName === 'A')
5325
6156
  {
5326
- var $node = $(nodes[i]);
5327
- $node.replaceWith($node.contents());
6157
+ links.push(nodes[i]);
5328
6158
  }
6159
+
6160
+ var $node = $(nodes[i]).closest('a', this.$editor[0]);
6161
+ $node.replaceWith($node.contents());
5329
6162
  }
5330
6163
 
6164
+ this.core.setCallback('deletedLink', links);
6165
+
5331
6166
  // hide link's tooltip
5332
6167
  $('.redactor-link-tooltip').remove();
5333
6168
 
5334
6169
  this.code.sync();
5335
6170
 
6171
+ },
6172
+ toggleClass: function(className)
6173
+ {
6174
+ this.link.setClass(className, 'toggleClass');
6175
+ },
6176
+ addClass: function(className)
6177
+ {
6178
+ this.link.setClass(className, 'addClass');
6179
+ },
6180
+ removeClass: function(className)
6181
+ {
6182
+ this.link.setClass(className, 'removeClass');
6183
+ },
6184
+ setClass: function(className, func)
6185
+ {
6186
+ var links = this.selection.getInlinesTags(['a']);
6187
+ if (links === false) return;
6188
+
6189
+ $.each(links, function()
6190
+ {
6191
+ $(this)[func](className);
6192
+ });
6193
+ }
6194
+ };
6195
+ },
6196
+ linkify: function()
6197
+ {
6198
+ return {
6199
+ isKey: function(key)
6200
+ {
6201
+ return key == this.keyCode.ENTER || key == this.keyCode.SPACE;
6202
+ },
6203
+ isEnabled: function()
6204
+ {
6205
+ return this.opts.convertLinks && (this.opts.convertUrlLinks || this.opts.convertImageLinks || this.opts.convertVideoLinks) && !this.utils.isCurrentOrParent('PRE');
6206
+ },
6207
+ format: function()
6208
+ {
6209
+ var linkify = this.linkify,
6210
+ opts = this.opts;
6211
+
6212
+ this.$editor
6213
+ .find(":not(iframe,img,a,pre)")
6214
+ .addBack()
6215
+ .contents()
6216
+ .filter(function()
6217
+ {
6218
+ return this.nodeType === 3 && $.trim(this.nodeValue) != "" && !$(this).parent().is("pre") && (this.nodeValue.match(opts.linkify.regexps.youtube) || this.nodeValue.match(opts.linkify.regexps.vimeo) || this.nodeValue.match(opts.linkify.regexps.image) || this.nodeValue.match(opts.linkify.regexps.url));
6219
+ })
6220
+ .each(function()
6221
+ {
6222
+ var text = $(this).text(),
6223
+ html = text;
6224
+
6225
+ if (opts.convertVideoLinks && (html.match(opts.linkify.regexps.youtube) || html.match(opts.linkify.regexps.vimeo)) )
6226
+ {
6227
+ html = linkify.convertVideoLinks(html);
6228
+ }
6229
+ else if (opts.convertImageLinks && html.match(opts.linkify.regexps.image))
6230
+ {
6231
+ html = linkify.convertImages(html);
6232
+ }
6233
+ else if (opts.convertUrlLinks)
6234
+ {
6235
+ html = linkify.convertLinks(html);
6236
+ }
6237
+
6238
+ $(this).before(text.replace(text, html))
6239
+ .remove();
6240
+ });
6241
+
6242
+
6243
+ var objects = this.$editor.find('.redactor-linkify-object').each(function()
6244
+ {
6245
+ var $el = $(this);
6246
+ $el.removeClass('redactor-linkify-object');
6247
+ if ($el.attr('class') === '') $el.removeAttr('class');
6248
+
6249
+ return $el[0];
6250
+
6251
+ });
6252
+
6253
+ // callback
6254
+ setTimeout($.proxy(function()
6255
+ {
6256
+ this.observe.load();
6257
+ this.core.setCallback('linkify', objects);
6258
+ }, this), 100);
6259
+
6260
+ // sync
6261
+ this.code.sync();
6262
+ },
6263
+ convertVideoLinks: function(html)
6264
+ {
6265
+ var iframeStart = '<iframe class="redactor-linkify-object" width="500" height="281" src="',
6266
+ iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
6267
+
6268
+ if (html.match(this.opts.linkify.regexps.youtube))
6269
+ {
6270
+ html = html.replace(this.opts.linkify.regexps.youtube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
6271
+ }
6272
+
6273
+ if (html.match(this.opts.linkify.regexps.vimeo))
6274
+ {
6275
+ html = html.replace(this.opts.linkify.regexps.vimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
6276
+ }
6277
+
6278
+ return html;
6279
+ },
6280
+ convertImages: function(html)
6281
+ {
6282
+ var matches = html.match(this.opts.linkify.regexps.image);
6283
+
6284
+ if (matches)
6285
+ {
6286
+ html = html.replace(html, '<img src="' + matches + '" class="redactor-linkify-object" />');
6287
+
6288
+ if (this.opts.linebreaks)
6289
+ {
6290
+ if (!this.utils.isEmpty(this.code.get()))
6291
+ {
6292
+ html = '<br>' + html;
6293
+ }
6294
+ }
6295
+
6296
+ html += '<br>';
6297
+ }
6298
+
6299
+ return html;
6300
+ },
6301
+ convertLinks: function(html)
6302
+ {
6303
+ var matches = html.match(this.opts.linkify.regexps.url);
6304
+
6305
+ if (matches)
6306
+ {
6307
+ matches = $.grep(matches, function(v, k) { return $.inArray(v, matches) === k; });
6308
+
6309
+ var length = matches.length;
6310
+
6311
+ for (var i = 0; i < length; i++)
6312
+ {
6313
+ var href = matches[i],
6314
+ text = href,
6315
+ linkProtocol = this.opts.linkProtocol + '://';
6316
+
6317
+ if (href.match(/(https?|ftp):\/\//i) !== null)
6318
+ {
6319
+ linkProtocol = "";
6320
+ }
6321
+
6322
+ if (text.length > this.opts.linkSize)
6323
+ {
6324
+ text = text.substring(0, this.opts.linkSize) + '...';
6325
+ }
6326
+
6327
+ if (text.search('%') === -1)
6328
+ {
6329
+ text = decodeURIComponent(text);
6330
+ }
6331
+
6332
+ var regexB = "\\b";
6333
+
6334
+ if ($.inArray(href.slice(-1), ["/", "&", "="]) != -1)
6335
+ {
6336
+ regexB = "";
6337
+ }
6338
+
6339
+ // escaping url
6340
+ var regexp = new RegExp('(' + href.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") + regexB + ')', 'g');
6341
+
6342
+ html = html.replace(regexp, '<a href="' + linkProtocol + $.trim(href) + '" class="redactor-linkify-object">' + $.trim(text) + '</a>');
6343
+ }
6344
+ }
6345
+
6346
+ return html;
5336
6347
  }
5337
6348
  };
5338
6349
  },
@@ -5342,13 +6353,17 @@
5342
6353
  toggle: function(cmd)
5343
6354
  {
5344
6355
  this.placeholder.remove();
5345
- if (!this.utils.browser('msie')) this.$editor.focus();
6356
+
6357
+ if (!this.utils.browser('msie') && !this.opts.linebreaks)
6358
+ {
6359
+ this.$editor.focus();
6360
+ }
5346
6361
 
5347
6362
  this.buffer.set();
5348
6363
  this.selection.save();
5349
6364
 
5350
6365
  var parent = this.selection.getParent();
5351
- var $list = $(parent).closest('ol, ul');
6366
+ var $list = $(parent).closest('ol, ul', this.$editor[0]);
5352
6367
 
5353
6368
  if (!this.utils.isRedactorParent($list) && $list.length !== 0)
5354
6369
  {
@@ -5378,7 +6393,7 @@
5378
6393
  {
5379
6394
  if (remove)
5380
6395
  {
5381
- this.list.remove(cmd);
6396
+ this.list.remove(cmd, $list);
5382
6397
  }
5383
6398
  else
5384
6399
  {
@@ -5386,15 +6401,14 @@
5386
6401
  }
5387
6402
  }
5388
6403
 
5389
-
5390
6404
  this.selection.restore();
5391
6405
  this.code.sync();
6406
+
5392
6407
  },
5393
6408
  insert: function(cmd)
5394
6409
  {
5395
- var parent = this.selection.getParent();
5396
6410
  var current = this.selection.getCurrent();
5397
- var $td = $(current).closest('td, th');
6411
+ var $td = $(current).closest('td, th', this.$editor[0]);
5398
6412
 
5399
6413
  if (this.utils.browser('msie') && this.opts.linebreaks)
5400
6414
  {
@@ -5405,30 +6419,25 @@
5405
6419
  document.execCommand('insert' + cmd);
5406
6420
  }
5407
6421
 
5408
- var $list = $(this.selection.getParent()).closest('ol, ul');
5409
-
6422
+ var parent = this.selection.getParent();
6423
+ var $list = $(parent).closest('ol, ul', this.$editor[0]);
5410
6424
  if ($td.length !== 0)
5411
6425
  {
5412
- var prev = $td.prev();
5413
- var html = $td.html();
5414
- $td.html('');
5415
- if (prev && prev.length === 1 && (prev[0].tagName === 'TD' || prev[0].tagName === 'TH'))
5416
- {
5417
- $(prev).after($td);
5418
- }
5419
- else
5420
- {
5421
- $(parent).prepend($td);
5422
- }
5423
-
5424
- $td.html(html);
6426
+ var newTd = $td.clone();
6427
+ $td.after(newTd).remove('');
5425
6428
  }
5426
6429
 
6430
+
5427
6431
  if (this.utils.isEmpty($list.find('li').text()))
5428
6432
  {
5429
6433
  var $children = $list.children('li');
5430
6434
  $children.find('br').remove();
5431
6435
  $children.append(this.selection.getMarkerAsHtml());
6436
+
6437
+ if (this.opts.linebreaks && this.utils.browser('mozilla') && $children.size() == 2 && this.utils.isEmpty($children.eq(1).text()))
6438
+ {
6439
+ $children.eq(1).remove();
6440
+ }
5432
6441
  }
5433
6442
 
5434
6443
  if ($list.length)
@@ -5446,6 +6455,7 @@
5446
6455
  this.$editor.focus();
5447
6456
  }
5448
6457
 
6458
+
5449
6459
  this.clean.clearUnverified();
5450
6460
  },
5451
6461
  insertInIe: function(cmd)
@@ -5484,21 +6494,22 @@
5484
6494
  $(wrapper).replaceWith(tmpList);
5485
6495
  }
5486
6496
  },
5487
- remove: function(cmd)
6497
+ remove: function(cmd, $list)
5488
6498
  {
6499
+ if ($.inArray('ul', this.selection.getBlocks())) cmd = 'unorderedlist';
6500
+
5489
6501
  document.execCommand('insert' + cmd);
5490
6502
 
5491
6503
  var $current = $(this.selection.getCurrent());
5492
-
5493
6504
  this.indent.fixEmptyIndent();
5494
6505
 
5495
- if (!this.opts.linebreaks && $current.closest('li, th, td').length === 0)
6506
+ if (!this.opts.linebreaks && $current.closest('li, th, td', this.$editor[0]).length === 0)
5496
6507
  {
5497
6508
  document.execCommand('formatblock', false, 'p');
5498
6509
  this.$editor.find('ul, ol, blockquote').each($.proxy(this.utils.removeEmpty, this));
5499
6510
  }
5500
6511
 
5501
- var $table = $(this.selection.getCurrent()).closest('table');
6512
+ var $table = $(this.selection.getCurrent()).closest('table', this.$editor[0]);
5502
6513
  var $prev = $table.prev();
5503
6514
  if (!this.opts.linebreaks && $table.length !== 0 && $prev.length !== 0 && $prev[0].tagName == 'BR')
5504
6515
  {
@@ -5507,6 +6518,7 @@
5507
6518
 
5508
6519
  this.clean.clearUnverified();
5509
6520
 
6521
+
5510
6522
  }
5511
6523
  };
5512
6524
  },
@@ -5522,10 +6534,10 @@
5522
6534
  + '<label>' + this.lang.get('title') + '</label>'
5523
6535
  + '<input type="text" id="redactor-image-title" />'
5524
6536
  + '<label class="redactor-image-link-option">' + this.lang.get('link') + '</label>'
5525
- + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" />'
5526
- + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank"> ' + this.lang.get('link_new_tab') + '</label>'
6537
+ + '<input type="text" id="redactor-image-link" class="redactor-image-link-option" aria-label="' + this.lang.get('link') + '" />'
6538
+ + '<label class="redactor-image-link-option"><input type="checkbox" id="redactor-image-link-blank" aria-label="' + this.lang.get('link_new_tab') + '"> ' + this.lang.get('link_new_tab') + '</label>'
5527
6539
  + '<label class="redactor-image-position-option">' + this.lang.get('image_position') + '</label>'
5528
- + '<select class="redactor-image-position-option" id="redactor-image-align">'
6540
+ + '<select class="redactor-image-position-option" id="redactor-image-align" aria-label="' + this.lang.get('image_position') + '">'
5529
6541
  + '<option value="none">' + this.lang.get('none') + '</option>'
5530
6542
  + '<option value="left">' + this.lang.get('left') + '</option>'
5531
6543
  + '<option value="center">' + this.lang.get('center') + '</option>'
@@ -5542,7 +6554,7 @@
5542
6554
  + '<section id="redactor-modal-file-insert">'
5543
6555
  + '<div id="redactor-modal-file-upload-box">'
5544
6556
  + '<label>' + this.lang.get('filename') + '</label>'
5545
- + '<input type="text" id="redactor-filename" /><br><br>'
6557
+ + '<input type="text" id="redactor-filename" aria-label="' + this.lang.get('filename') + '" /><br><br>'
5546
6558
  + '<div id="redactor-modal-file-upload"></div>'
5547
6559
  + '</div>'
5548
6560
  + '</section>',
@@ -5550,9 +6562,9 @@
5550
6562
  link: String()
5551
6563
  + '<section id="redactor-modal-link-insert">'
5552
6564
  + '<label>URL</label>'
5553
- + '<input type="url" id="redactor-link-url" />'
6565
+ + '<input type="url" id="redactor-link-url" aria-label="URL" />'
5554
6566
  + '<label>' + this.lang.get('text') + '</label>'
5555
- + '<input type="text" id="redactor-link-url-text" />'
6567
+ + '<input type="text" id="redactor-link-url-text" aria-label="' + this.lang.get('text') + '" />'
5556
6568
  + '<label><input type="checkbox" id="redactor-link-blank"> ' + this.lang.get('link_new_tab') + '</label>'
5557
6569
  + '</section>'
5558
6570
  };
@@ -5625,15 +6637,7 @@
5625
6637
  },
5626
6638
  show: function()
5627
6639
  {
5628
- // ios keyboard hide
5629
- if (this.utils.isMobile() && !this.utils.browser('msie'))
5630
- {
5631
- document.activeElement.blur();
5632
- }
5633
-
5634
- $(document.body).removeClass('body-redactor-hidden');
5635
- this.modal.bodyOveflow = $(document.body).css('overflow');
5636
- $(document.body).css('overflow', 'hidden');
6640
+ this.utils.disableBodyScroll();
5637
6641
 
5638
6642
  if (this.utils.isMobile())
5639
6643
  {
@@ -5644,9 +6648,17 @@
5644
6648
  this.modal.showOnDesktop();
5645
6649
  }
5646
6650
 
6651
+ if (this.opts.highContrast)
6652
+ {
6653
+ this.$modalBox.addClass("redactor-modal-contrast");
6654
+ }
6655
+
5647
6656
  this.$modalOverlay.show();
5648
6657
  this.$modalBox.show();
5649
6658
 
6659
+ this.$modal.attr('tabindex', '-1');
6660
+ this.$modal.focus();
6661
+
5650
6662
  this.modal.setButtonsWidth();
5651
6663
 
5652
6664
  this.utils.saveScroll();
@@ -5772,10 +6784,10 @@
5772
6784
  {
5773
6785
  this.modal.buildOverlay();
5774
6786
 
5775
- this.$modalBox = $('<div id="redactor-modal-box" />').hide();
5776
- this.$modal = $('<div id="redactor-modal" />');
5777
- this.$modalHeader = $('<header />');
5778
- this.$modalClose = $('<span id="redactor-modal-close" />').html('&times;');
6787
+ this.$modalBox = $('<div id="redactor-modal-box"/>').hide();
6788
+ this.$modal = $('<div id="redactor-modal" role="dialog" aria-labelledby="redactor-modal-header" />');
6789
+ this.$modalHeader = $('<header id="redactor-modal-header"/>');
6790
+ this.$modalClose = $('<button type="button" id="redactor-modal-close" tabindex="1" aria-label="Close" />').html('&times;');
5779
6791
  this.$modalBody = $('<div id="redactor-modal-body" />');
5780
6792
  this.$modalFooter = $('<footer />');
5781
6793
 
@@ -5824,42 +6836,157 @@
5824
6836
  e.preventDefault();
5825
6837
  }
5826
6838
 
5827
- if (!this.$modalBox) return;
5828
-
5829
- this.modal.disableEvents();
5830
-
5831
- this.$modalOverlay.remove();
5832
-
5833
- this.$modalBox.fadeOut('fast', $.proxy(function()
6839
+ if (!this.$modalBox) return;
6840
+
6841
+ this.modal.disableEvents();
6842
+ this.utils.enableBodyScroll();
6843
+
6844
+ this.$modalOverlay.remove();
6845
+
6846
+ this.$modalBox.fadeOut('fast', $.proxy(function()
6847
+ {
6848
+ this.$modalBox.remove();
6849
+
6850
+ setTimeout($.proxy(this.utils.restoreScroll, this), 0);
6851
+
6852
+ if (e !== undefined) this.selection.restore();
6853
+
6854
+ $(document.body).css('overflow', this.modal.bodyOveflow);
6855
+ this.core.setCallback('modalClosed', this.modal.templateName);
6856
+
6857
+ }, this));
6858
+
6859
+ }
6860
+ };
6861
+ },
6862
+ observe: function()
6863
+ {
6864
+ return {
6865
+ load: function()
6866
+ {
6867
+ if (typeof this.opts.destroyed != "undefined") return;
6868
+
6869
+ if (this.utils.browser('msie'))
6870
+ {
6871
+ var self = this;
6872
+ this.$editor.find('pre, code').on('mouseover',function()
6873
+ {
6874
+ self.$editor.attr('contenteditable', false);
6875
+ $(this).attr('contenteditable', true);
6876
+
6877
+ }).on('mouseout',function()
6878
+ {
6879
+ self.$editor.attr('contenteditable', true);
6880
+ $(this).removeAttr('contenteditable');
6881
+
6882
+ });
6883
+ }
6884
+
6885
+ this.observe.images();
6886
+ this.observe.links();
6887
+ },
6888
+ toolbar: function(e, btnName)
6889
+ {
6890
+ this.observe.buttons(e, btnName);
6891
+ this.observe.dropdowns();
6892
+ },
6893
+ isCurrent: function($el, $current)
6894
+ {
6895
+ if (typeof $current == 'undefined')
6896
+ {
6897
+ var $current = $(this.selection.getCurrent());
6898
+ }
6899
+
6900
+ return $current.is($el) || $current.parents($el).length > 0;
6901
+ },
6902
+ dropdowns: function()
6903
+ {
6904
+ var $current = $(this.selection.getCurrent());
6905
+
6906
+ $.each(this.opts.observe.dropdowns, $.proxy(function(key, value)
6907
+ {
6908
+ var observe = value.observe,
6909
+ element = observe.element,
6910
+ $item = value.item,
6911
+ inValues = typeof observe['in'] != 'undefined' ? observe['in'] : false,
6912
+ outValues = typeof observe['out'] != 'undefined' ? observe['out'] : false;
6913
+
6914
+ if ($current.closest(element).size() > 0)
6915
+ {
6916
+ this.observe.setDropdownProperties($item, inValues, outValues);
6917
+ }
6918
+ else
6919
+ {
6920
+ this.observe.setDropdownProperties($item, outValues, inValues);
6921
+ }
6922
+ }, this));
6923
+ },
6924
+ setDropdownProperties: function($item, addProperties, deleteProperties)
6925
+ {
6926
+ if (deleteProperties && typeof deleteProperties['attr'] != 'undefined')
6927
+ {
6928
+ this.observe.setDropdownAttr($item, deleteProperties.attr, true);
6929
+ }
6930
+
6931
+ if (typeof addProperties['attr'] != 'undefined')
5834
6932
  {
5835
- this.$modalBox.remove();
5836
-
5837
- setTimeout($.proxy(this.utils.restoreScroll, this), 0);
5838
-
5839
- if (e !== undefined) this.selection.restore();
6933
+ this.observe.setDropdownAttr($item, addProperties.attr);
6934
+ }
5840
6935
 
5841
- $(document.body).css('overflow', this.modal.bodyOveflow);
5842
- this.core.setCallback('modalClosed', this.modal.templateName);
6936
+ if (typeof addProperties['title'] != 'undefined')
6937
+ {
6938
+ $item.text(addProperties['title']);
6939
+ }
6940
+ },
6941
+ setDropdownAttr: function($item, properties, isDelete)
6942
+ {
6943
+ $.each(properties, function(key, value)
6944
+ {
6945
+ if (key == 'class')
6946
+ {
6947
+ if (!isDelete)
6948
+ {
6949
+ $item.addClass(value);
6950
+ }
6951
+ else
6952
+ {
6953
+ $item.removeClass(value);
6954
+ }
6955
+ }
6956
+ else
6957
+ {
6958
+ if (!isDelete)
6959
+ {
6960
+ $item.attr(key, value);
6961
+ }
6962
+ else
6963
+ {
6964
+ $item.removeAttr(key);
6965
+ }
6966
+ }
6967
+ });
6968
+ },
6969
+ addDropdown: function($item, btnName, btnObject)
6970
+ {
6971
+ if (typeof btnObject.observe == "undefined") return;
5843
6972
 
5844
- }, this));
6973
+ btnObject.item = $item;
5845
6974
 
5846
- }
5847
- };
5848
- },
5849
- observe: function()
5850
- {
5851
- return {
5852
- load: function()
5853
- {
5854
- this.observe.images();
5855
- this.observe.links();
6975
+ this.opts.observe.dropdowns.push(btnObject);
5856
6976
  },
5857
6977
  buttons: function(e, btnName)
5858
6978
  {
5859
6979
  var current = this.selection.getCurrent();
5860
6980
  var parent = this.selection.getParent();
5861
6981
 
5862
- this.button.setInactiveAll(btnName);
6982
+ if (e !== false)
6983
+ {
6984
+ this.button.setInactiveAll();
6985
+ }
6986
+ else
6987
+ {
6988
+ this.button.setInactiveAll(btnName);
6989
+ }
5863
6990
 
5864
6991
  if (e === false && btnName !== 'html')
5865
6992
  {
@@ -5872,19 +6999,19 @@
5872
6999
 
5873
7000
  $.each(this.opts.activeButtonsStates, $.proxy(function(key, value)
5874
7001
  {
5875
- var parentEl = $(parent).closest(key);
5876
- var currentEl = $(current).closest(key);
7002
+ var parentEl = $(parent).closest(key, this.$editor[0]);
7003
+ var currentEl = $(current).closest(key, this.$editor[0]);
5877
7004
 
5878
7005
  if (parentEl.length !== 0 && !this.utils.isRedactorParent(parentEl)) return;
5879
7006
  if (!this.utils.isRedactorParent(currentEl)) return;
5880
- if (parentEl.length !== 0 || currentEl.closest(key).length !== 0)
7007
+ if (parentEl.length !== 0 || currentEl.closest(key, this.$editor[0]).length !== 0)
5881
7008
  {
5882
7009
  this.button.setActive(value);
5883
7010
  }
5884
7011
 
5885
7012
  }, this));
5886
7013
 
5887
- var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase());
7014
+ var $parent = $(parent).closest(this.opts.alignmentTags.toString().toLowerCase(), this.$editor[0]);
5888
7015
  if (this.utils.isRedactorParent(parent) && $parent.length)
5889
7016
  {
5890
7017
  var align = ($parent.css('text-align') === '') ? 'left' : $parent.css('text-align');
@@ -5903,7 +7030,7 @@
5903
7030
  var $img = $(img);
5904
7031
 
5905
7032
  // IE fix (when we clicked on an image and then press backspace IE does goes to image's url)
5906
- $img.closest('a').on('click', function(e) { e.preventDefault(); });
7033
+ $img.closest('a', this.$editor[0]).on('click', function(e) { e.preventDefault(); });
5907
7034
 
5908
7035
  if (this.utils.browser('msie')) $img.attr('unselectable', 'on');
5909
7036
 
@@ -5911,7 +7038,7 @@
5911
7038
 
5912
7039
  }, this));
5913
7040
 
5914
- $(document).on('click.redactor-image-delete', $.proxy(function(e)
7041
+ $(document).on('click.redactor-image-delete.' + this.uuid, $.proxy(function(e)
5915
7042
  {
5916
7043
  this.observe.image = false;
5917
7044
  if (e.target.tagName == 'IMG' && this.utils.isRedactorParent(e.target))
@@ -5926,9 +7053,9 @@
5926
7053
  {
5927
7054
  if (!this.opts.linkTooltip) return;
5928
7055
 
5929
- this.$editor.find('a').on('touchstart click', $.proxy(this.observe.showTooltip, this));
5930
- this.$editor.on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this));
5931
- $(document).on('touchstart click.redactor', $.proxy(this.observe.closeTooltip, this));
7056
+ this.$editor.find('a').on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.showTooltip, this));
7057
+ this.$editor.on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
7058
+ $(document).on('touchstart.redactor.' + this.uuid + ' click.redactor.' + this.uuid, $.proxy(this.observe.closeTooltip, this));
5932
7059
  },
5933
7060
  getTooltipPosition: function($link)
5934
7061
  {
@@ -5936,20 +7063,18 @@
5936
7063
  },
5937
7064
  showTooltip: function(e)
5938
7065
  {
5939
- var $link = $(e.target);
5940
- var $parent = $link.closest('a');
5941
- var tag = ($link.length !== 0) ? $link[0].tagName : false;
7066
+ var $el = $(e.target);
5942
7067
 
5943
- if ($parent[0].tagName === 'A')
5944
- {
5945
- if (tag === 'IMG') return;
5946
- else if (tag !== 'A') $link = $parent;
5947
- }
7068
+ if ($el[0].tagName == 'IMG')
7069
+ return;
5948
7070
 
5949
- if (tag !== 'A')
5950
- {
7071
+ if ($el[0].tagName !== 'A')
7072
+ $el = $el.closest('a', this.$editor[0]);
7073
+
7074
+ if ($el[0].tagName !== 'A')
5951
7075
  return;
5952
- }
7076
+
7077
+ var $link = $el;
5953
7078
 
5954
7079
  var pos = this.observe.getTooltipPosition($link);
5955
7080
  var tooltip = $('<span class="redactor-link-tooltip"></span>');
@@ -5980,7 +7105,7 @@
5980
7105
  e = e.originalEvent || e;
5981
7106
 
5982
7107
  var target = e.target;
5983
- var $parent = $(target).closest('a');
7108
+ var $parent = $(target).closest('a', this.$editor[0]);
5984
7109
  if ($parent.length !== 0 && $parent[0].tagName === 'A' && target.tagName !== 'A')
5985
7110
  {
5986
7111
  return;
@@ -6003,10 +7128,6 @@
6003
7128
  if (this.opts.linebreaks) return html;
6004
7129
  if (html === '' || html === '<p></p>') return this.opts.emptyHtml;
6005
7130
 
6006
- this.paragraphize.blocks = ['table', 'div', 'pre', 'form', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'dl', 'blockquote', 'figcaption',
6007
- 'address', 'section', 'header', 'footer', 'aside', 'article', 'object', 'style', 'script', 'iframe', 'select', 'input', 'textarea',
6008
- 'button', 'option', 'map', 'area', 'math', 'hr', 'fieldset', 'legend', 'hgroup', 'nav', 'figure', 'details', 'menu', 'summary', 'p'];
6009
-
6010
7131
  html = html + "\n";
6011
7132
 
6012
7133
  this.paragraphize.safes = [];
@@ -6021,7 +7142,7 @@
6021
7142
  html = this.paragraphize.clear(html);
6022
7143
  html = this.paragraphize.restoreSafes(html);
6023
7144
 
6024
- html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.paragraphize.blocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
7145
+ html = html.replace(new RegExp('<br\\s?/?>\n?<(' + this.opts.paragraphizeBlocks.join('|') + ')(.*?[^>])>', 'gi'), '<p><br /></p>\n<$1$2>');
6025
7146
 
6026
7147
  return $.trim(html);
6027
7148
  },
@@ -6037,7 +7158,7 @@
6037
7158
 
6038
7159
  html = $div.html();
6039
7160
 
6040
- $div.find(this.paragraphize.blocks.join(', ')).each($.proxy(function(i,s)
7161
+ $div.find(this.opts.paragraphizeBlocks.join(', ')).each($.proxy(function(i,s)
6041
7162
  {
6042
7163
  this.paragraphize.z++;
6043
7164
  this.paragraphize.safes[this.paragraphize.z] = s.outerHTML;
@@ -6066,7 +7187,9 @@
6066
7187
  {
6067
7188
  $.each(this.paragraphize.safes, function(i,s)
6068
7189
  {
7190
+ s = (typeof s !== 'undefined') ? s.replace(/\$/g, '&#36;') : s;
6069
7191
  html = html.replace('{replace' + i + '}', s);
7192
+
6070
7193
  });
6071
7194
 
6072
7195
  return html;
@@ -6134,7 +7257,11 @@
6134
7257
  return {
6135
7258
  init: function(e)
6136
7259
  {
6137
- if (!this.opts.cleanOnPaste) return;
7260
+ if (!this.opts.cleanOnPaste)
7261
+ {
7262
+ setTimeout($.proxy(this.code.sync, this), 1);
7263
+ return;
7264
+ }
6138
7265
 
6139
7266
  this.rtePaste = true;
6140
7267
 
@@ -6163,15 +7290,36 @@
6163
7290
 
6164
7291
  $(window).off('scroll.redactor-freeze');
6165
7292
 
6166
- }, this), 1);
7293
+ if (this.linkify.isEnabled())
7294
+ {
7295
+ this.linkify.format();
7296
+ }
6167
7297
 
7298
+ }, this), 1);
6168
7299
  },
6169
7300
  createPasteBox: function()
6170
7301
  {
6171
7302
  this.$pasteBox = $('<div>').html('').attr('contenteditable', 'true').css({ position: 'fixed', width: 0, top: 0, left: '-9999px' });
6172
7303
 
6173
- this.$box.parent().append(this.$pasteBox);
6174
- this.$pasteBox.focus();
7304
+ if (this.utils.browser('msie'))
7305
+ {
7306
+ this.$box.append(this.$pasteBox);
7307
+ }
7308
+ else
7309
+ {
7310
+ // bootstrap modal
7311
+ var $visibleModals = $('.modal-body:visible');
7312
+ if ($visibleModals.length > 0)
7313
+ {
7314
+ $visibleModals.append(this.$pasteBox);
7315
+ }
7316
+ else
7317
+ {
7318
+ $('body').append(this.$pasteBox);
7319
+ }
7320
+ }
7321
+
7322
+ this.$pasteBox.get(0).focus();
6175
7323
  },
6176
7324
  insert: function(html)
6177
7325
  {
@@ -6202,12 +7350,13 @@
6202
7350
  var spans = this.$editor.find('span');
6203
7351
  $.each(spans, function(i,s)
6204
7352
  {
6205
- var html = s.innerHTML.replace(/[\u200B-\u200D\uFEFF]/, '');
7353
+ var html = s.innerHTML.replace(/\u200B/, '');
6206
7354
  if (html === '' && s.attributes.length === 0) $(s).remove();
6207
7355
 
6208
7356
  });
6209
7357
 
6210
7358
  }, this), 10);
7359
+
6211
7360
  }
6212
7361
  };
6213
7362
  },
@@ -6221,14 +7370,16 @@
6221
7370
  this.$editor.attr('placeholder', this.$element.attr('placeholder'));
6222
7371
 
6223
7372
  this.placeholder.toggle();
6224
- this.$editor.on('keyup.redactor-placeholder', $.proxy(this.placeholder.toggle, this));
6225
-
7373
+ this.$editor.on('keydown.redactor-placeholder', $.proxy(this.placeholder.toggle, this));
6226
7374
  },
6227
7375
  toggle: function()
6228
7376
  {
6229
- var func = 'removeClass';
6230
- if (this.utils.isEmpty(this.$editor.html(), false)) func = 'addClass';
6231
- this.$editor[func]('redactor-placeholder');
7377
+ setTimeout($.proxy(function()
7378
+ {
7379
+ var func = this.utils.isEmpty(this.$editor.html(), false) ? 'addClass' : 'removeClass';
7380
+ this.$editor[func]('redactor-placeholder');
7381
+
7382
+ }, this), 5);
6232
7383
  },
6233
7384
  remove: function()
6234
7385
  {
@@ -6292,6 +7443,7 @@
6292
7443
  getCurrent: function()
6293
7444
  {
6294
7445
  var el = false;
7446
+
6295
7447
  this.selection.get();
6296
7448
 
6297
7449
  if (this.sel && this.sel.rangeCount > 0)
@@ -6311,6 +7463,14 @@
6311
7463
 
6312
7464
  return false;
6313
7465
  },
7466
+ getPrev: function()
7467
+ {
7468
+ return window.getSelection().anchorNode.previousSibling;
7469
+ },
7470
+ getNext: function()
7471
+ {
7472
+ return window.getSelection().anchorNode.nextSibling;
7473
+ },
6314
7474
  getBlock: function(node)
6315
7475
  {
6316
7476
  node = node || this.selection.getCurrent();
@@ -6327,7 +7487,7 @@
6327
7487
 
6328
7488
  return false;
6329
7489
  },
6330
- getInlines: function(nodes)
7490
+ getInlines: function(nodes, tags)
6331
7491
  {
6332
7492
  this.selection.get();
6333
7493
 
@@ -6337,9 +7497,18 @@
6337
7497
  }
6338
7498
 
6339
7499
  var inlines = [];
6340
- nodes = (typeof nodes == 'undefined') ? this.selection.getNodes() : nodes;
7500
+ nodes = (typeof nodes == 'undefined' || nodes === false) ? this.selection.getNodes() : nodes;
6341
7501
  var inlineTags = this.opts.inlineTags;
6342
7502
  inlineTags.push('span');
7503
+
7504
+ if (typeof tags !== 'undefined')
7505
+ {
7506
+ for (var i = 0; i < tags.length; i++)
7507
+ {
7508
+ inlineTags.push(tags[i]);
7509
+ }
7510
+ }
7511
+
6343
7512
  $.each(nodes, $.proxy(function(i,node)
6344
7513
  {
6345
7514
  if ($.inArray(node.tagName.toLowerCase(), inlineTags) != -1)
@@ -6351,6 +7520,28 @@
6351
7520
 
6352
7521
  return (inlines.length === 0) ? false : inlines;
6353
7522
  },
7523
+ getInlinesTags: function(tags)
7524
+ {
7525
+ this.selection.get();
7526
+
7527
+ if (this.range && this.range.collapsed)
7528
+ {
7529
+ return false;
7530
+ }
7531
+
7532
+ var inlines = [];
7533
+ var nodes = this.selection.getNodes();
7534
+ $.each(nodes, $.proxy(function(i,node)
7535
+ {
7536
+ if ($.inArray(node.tagName.toLowerCase(), tags) != -1)
7537
+ {
7538
+ inlines.push(node);
7539
+ }
7540
+
7541
+ }, this));
7542
+
7543
+ return (inlines.length === 0) ? false : inlines;
7544
+ },
6354
7545
  getBlocks: function(nodes)
6355
7546
  {
6356
7547
  this.selection.get();
@@ -6362,11 +7553,11 @@
6362
7553
 
6363
7554
  var blocks = [];
6364
7555
  nodes = (typeof nodes == 'undefined') ? this.selection.getNodes() : nodes;
7556
+
6365
7557
  $.each(nodes, $.proxy(function(i,node)
6366
7558
  {
6367
7559
  if (this.utils.isBlock(node))
6368
7560
  {
6369
- this.selection.lastBlock = node;
6370
7561
  blocks.push(node);
6371
7562
  }
6372
7563
 
@@ -6384,23 +7575,34 @@
6384
7575
 
6385
7576
  var startNode = this.selection.getNodesMarker(1);
6386
7577
  var endNode = this.selection.getNodesMarker(2);
6387
- var range = this.range.cloneRange();
6388
7578
 
6389
7579
  if (this.range.collapsed === false)
6390
7580
  {
6391
- var startContainer = range.startContainer;
6392
- var startOffset = range.startOffset;
6393
-
6394
- // end marker
6395
- this.selection.setNodesMarker(range, endNode, false);
6396
-
6397
- // start marker
6398
- range.setStart(startContainer, startOffset);
6399
- this.selection.setNodesMarker(range, startNode, true);
7581
+ if (window.getSelection) {
7582
+ var sel = window.getSelection();
7583
+ if (sel.rangeCount > 0) {
7584
+
7585
+ var range = sel.getRangeAt(0);
7586
+ var startPointNode = range.startContainer, startOffset = range.startOffset;
7587
+
7588
+ var boundaryRange = range.cloneRange();
7589
+ boundaryRange.collapse(false);
7590
+ boundaryRange.insertNode(endNode);
7591
+ boundaryRange.setStart(startPointNode, startOffset);
7592
+ boundaryRange.collapse(true);
7593
+ boundaryRange.insertNode(startNode);
7594
+
7595
+ // Reselect the original text
7596
+ range.setStartAfter(startNode);
7597
+ range.setEndBefore(endNode);
7598
+ sel.removeAllRanges();
7599
+ sel.addRange(range);
7600
+ }
7601
+ }
6400
7602
  }
6401
7603
  else
6402
7604
  {
6403
- this.selection.setNodesMarker(range, startNode, true);
7605
+ this.selection.setNodesMarker(this.range, startNode, true);
6404
7606
  endNode = startNode;
6405
7607
  }
6406
7608
 
@@ -6458,6 +7660,8 @@
6458
7660
  },
6459
7661
  setNodesMarker: function(range, node, type)
6460
7662
  {
7663
+ var range = range.cloneRange();
7664
+
6461
7665
  try {
6462
7666
  range.collapse(type);
6463
7667
  range.insertNode(node);
@@ -6487,7 +7691,17 @@
6487
7691
  },
6488
7692
  selectElement: function(node)
6489
7693
  {
6490
- this.caret.set(node, 0, node, 1);
7694
+ if (this.utils.browser('mozilla'))
7695
+ {
7696
+ node = node[0] || node;
7697
+
7698
+ var range = document.createRange();
7699
+ range.selectNodeContents(node);
7700
+ }
7701
+ else
7702
+ {
7703
+ this.caret.set(node, 0, node, 1);
7704
+ }
6491
7705
  },
6492
7706
  selectAll: function()
6493
7707
  {
@@ -6511,7 +7725,6 @@
6511
7725
  var node1 = this.selection.getMarker(1);
6512
7726
 
6513
7727
  this.selection.setMarker(this.range, node1, true);
6514
-
6515
7728
  if (this.range.collapsed === false)
6516
7729
  {
6517
7730
  var node2 = this.selection.getMarker(2);
@@ -6537,17 +7750,24 @@
6537
7750
  try {
6538
7751
  range.collapse(type);
6539
7752
  range.insertNode(node);
7753
+
6540
7754
  }
6541
7755
  catch (e)
6542
7756
  {
6543
7757
  this.focus.setStart();
6544
7758
  }
7759
+
6545
7760
  },
6546
7761
  restore: function()
6547
7762
  {
6548
7763
  var node1 = this.$editor.find('span#selection-marker-1');
6549
7764
  var node2 = this.$editor.find('span#selection-marker-2');
6550
7765
 
7766
+ if (this.utils.browser('mozilla'))
7767
+ {
7768
+ this.$editor.focus();
7769
+ }
7770
+
6551
7771
  if (node1.length !== 0 && node2.length !== 0)
6552
7772
  {
6553
7773
  this.caret.set(node1, 0, node2, 0);
@@ -6567,7 +7787,12 @@
6567
7787
  },
6568
7788
  removeMarkers: function()
6569
7789
  {
6570
- this.$editor.find('span.redactor-selection-marker').remove();
7790
+ this.$editor.find('span.redactor-selection-marker').each(function(i,s)
7791
+ {
7792
+ var text = $(s).text().replace(/\u200B/g, '');
7793
+ if (text === '') $(s).remove();
7794
+ else $(s).replaceWith(function() { return $(this).contents(); });
7795
+ });
6571
7796
  },
6572
7797
  getText: function()
6573
7798
  {
@@ -6593,6 +7818,37 @@
6593
7818
  }
6594
7819
 
6595
7820
  return this.clean.onSync(html);
7821
+ },
7822
+ replaceSelection: function(html)
7823
+ {
7824
+ this.selection.get();
7825
+ this.range.deleteContents();
7826
+ var div = document.createElement("div");
7827
+ div.innerHTML = html;
7828
+ var frag = document.createDocumentFragment(), child;
7829
+ while ((child = div.firstChild)) {
7830
+ frag.appendChild(child);
7831
+ }
7832
+
7833
+ this.range.insertNode(frag);
7834
+ },
7835
+ replaceWithHtml: function(html)
7836
+ {
7837
+ html = this.selection.getMarkerAsHtml(1) + html + this.selection.getMarkerAsHtml(2);
7838
+
7839
+ this.selection.get();
7840
+
7841
+ if (window.getSelection && window.getSelection().getRangeAt)
7842
+ {
7843
+ this.selection.replaceSelection(html);
7844
+ }
7845
+ else if (document.selection && document.selection.createRange)
7846
+ {
7847
+ this.range.pasteHTML(html);
7848
+ }
7849
+
7850
+ this.selection.restore();
7851
+ this.code.sync();
6596
7852
  }
6597
7853
  };
6598
7854
  },
@@ -6710,7 +7966,7 @@
6710
7966
  // clean setup
6711
7967
  var ownLine = ['area', 'body', 'head', 'hr', 'i?frame', 'link', 'meta', 'noscript', 'style', 'script', 'table', 'tbody', 'thead', 'tfoot'];
6712
7968
  var contOwnLine = ['li', 'dt', 'dt', 'h[1-6]', 'option', 'script'];
6713
- var newLevel = ['blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'p', 'pre', 'select', 'td', 'th', 'tr', 'ul'];
7969
+ var newLevel = ['p', 'blockquote', 'div', 'dl', 'fieldset', 'form', 'frameset', 'map', 'ol', 'pre', 'select', 'td', 'th', 'tr', 'ul'];
6714
7970
 
6715
7971
  this.tabifier.lineBefore = new RegExp('^<(/?' + ownLine.join('|/?' ) + '|' + contOwnLine.join('|') + ')[ >]');
6716
7972
  this.tabifier.lineAfter = new RegExp('^<(br|/?' + ownLine.join('|/?' ) + '|/' + contOwnLine.join('|/') + ')[ >]');
@@ -6875,6 +8131,7 @@
6875
8131
  placeTag: function (tag, out)
6876
8132
  {
6877
8133
  var nl = tag.match(this.tabifier.newLevel);
8134
+
6878
8135
  if (tag.match(this.tabifier.lineBefore) || nl)
6879
8136
  {
6880
8137
  out = out.replace(/\s*$/, '');
@@ -6890,7 +8147,7 @@
6890
8147
  if (tag.match(this.tabifier.lineAfter) || tag.match(this.tabifier.newLevel))
6891
8148
  {
6892
8149
  out = out.replace(/ *$/, '');
6893
- out += '\n';
8150
+ //out += '\n';
6894
8151
  }
6895
8152
 
6896
8153
  return out;
@@ -6902,6 +8159,12 @@
6902
8159
  return {
6903
8160
  setupAllowed: function()
6904
8161
  {
8162
+ var index = $.inArray('span', this.opts.removeEmpty);
8163
+ if (index !== -1)
8164
+ {
8165
+ this.opts.removeEmpty.splice(index, 1);
8166
+ }
8167
+
6905
8168
  if (this.opts.allowedTags) this.opts.deniedTags = false;
6906
8169
  if (this.opts.allowedAttr) this.opts.removeAttr = false;
6907
8170
 
@@ -6991,24 +8254,13 @@
6991
8254
  replacement.push(this.tidy.settings.replaceTags[i][0]);
6992
8255
  }
6993
8256
 
6994
- this.tidy.$div.find(replacement.join(',')).each($.proxy(function(n,s)
8257
+ $.each(replacement, $.proxy(function(key, value)
6995
8258
  {
6996
- var tag = rTags[n];
6997
- $(s).replaceWith(function()
8259
+ this.tidy.$div.find(value).replaceWith(function()
6998
8260
  {
6999
- var replaced = $('<' + tag + ' />').append($(this).contents());
7000
-
7001
- for (var i = 0; i < this.attributes.length; i++)
7002
- {
7003
- replaced.attr(this.attributes[i].name, this.attributes[i].value);
7004
- }
7005
-
7006
- return replaced;
8261
+ return $("<" + rTags[key] + " />", {html: $(this).html()});
7007
8262
  });
7008
-
7009
8263
  }, this));
7010
-
7011
- return html;
7012
8264
  },
7013
8265
  replaceStyles: function()
7014
8266
  {
@@ -7050,6 +8302,8 @@
7050
8302
  {
7051
8303
  this.tidy.$div.find(this.tidy.settings.deniedTags.join(',')).each(function(i, s)
7052
8304
  {
8305
+ if ($(s).hasClass('redactor-script-tag') || $(s).hasClass('redactor-selection-marker')) return;
8306
+
7053
8307
  if (s.innerHTML === '') $(s).remove();
7054
8308
  else $(s).contents().unwrap();
7055
8309
  });
@@ -7163,7 +8417,7 @@
7163
8417
  {
7164
8418
  var $el = $(this);
7165
8419
  var text = $el.text();
7166
- text = text.replace(/[\u200B-\u200D\uFEFF]/g, '');
8420
+ text = text.replace(/\u200B/g, '');
7167
8421
  text = text.replace(/&nbsp;/gi, '');
7168
8422
  text = text.replace(/\s/g, '');
7169
8423
 
@@ -7317,12 +8571,30 @@
7317
8571
  link:
7318
8572
  {
7319
8573
  title: this.lang.get('link_insert'),
7320
- func: 'link.show'
8574
+ func: 'link.show',
8575
+ observe: {
8576
+ element: 'a',
8577
+ in: {
8578
+ title: this.lang.get('link_edit'),
8579
+ },
8580
+ out: {
8581
+ title: this.lang.get('link_insert')
8582
+ }
8583
+ }
7321
8584
  },
7322
8585
  unlink:
7323
8586
  {
7324
8587
  title: this.lang.get('unlink'),
7325
- func: 'link.unlink'
8588
+ func: 'link.unlink',
8589
+ observe: {
8590
+ element: 'a',
8591
+ out: {
8592
+ attr: {
8593
+ 'class': 'redactor-dropdown-link-inactive',
8594
+ 'aria-disabled': true
8595
+ }
8596
+ }
8597
+ }
7326
8598
  }
7327
8599
  }
7328
8600
  },
@@ -7379,13 +8651,13 @@
7379
8651
  // buttons response
7380
8652
  if (this.opts.activeButtons)
7381
8653
  {
7382
- this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.buttons, this));
8654
+ this.$editor.on('mouseup.redactor keyup.redactor focus.redactor', $.proxy(this.observe.toolbar, this));
7383
8655
  }
7384
8656
 
7385
8657
  },
7386
8658
  createContainer: function()
7387
8659
  {
7388
- return $('<ul>').addClass('redactor-toolbar').attr('id', 'redactor-toolbar-' + this.uuid);
8660
+ return $('<ul>').addClass('redactor-toolbar').attr({'id': 'redactor-toolbar-' + this.uuid, 'role': 'toolbar'});
7389
8661
  },
7390
8662
  setFormattingTags: function()
7391
8663
  {
@@ -7437,7 +8709,7 @@
7437
8709
  if (!this.opts.toolbarFixed) return;
7438
8710
 
7439
8711
  this.toolbar.observeScroll();
7440
- $(this.opts.toolbarFixedTarget).on('scroll.redactor', $.proxy(this.toolbar.observeScroll, this));
8712
+ $(this.opts.toolbarFixedTarget).on('scroll.redactor.' + this.uuid, $.proxy(this.toolbar.observeScroll, this));
7441
8713
 
7442
8714
  },
7443
8715
  setOverflow: function()
@@ -7489,7 +8761,7 @@
7489
8761
  boxTop = this.$box.offset().top;
7490
8762
  }
7491
8763
 
7492
- if (scrollTop > boxTop)
8764
+ if ((scrollTop + this.opts.toolbarFixedTopOffset) > boxTop)
7493
8765
  {
7494
8766
  this.toolbar.observeScrollEnable(scrollTop, boxTop);
7495
8767
  }
@@ -7502,7 +8774,7 @@
7502
8774
  {
7503
8775
  var top = this.opts.toolbarFixedTopOffset + scrollTop - boxTop;
7504
8776
  var left = 0;
7505
- var end = boxTop + this.$box.height() + 30;
8777
+ var end = boxTop + this.$box.height() - 32;
7506
8778
  var width = this.$box.innerWidth();
7507
8779
 
7508
8780
  this.$toolbar.addClass('toolbar-fixed-box');
@@ -7513,6 +8785,9 @@
7513
8785
  left: left
7514
8786
  });
7515
8787
 
8788
+ if (scrollTop > end)
8789
+ $('.redactor-dropdown-' + this.uuid + ':visible').hide();
8790
+
7516
8791
  this.toolbar.setDropdownsFixed();
7517
8792
  this.$toolbar.css('visibility', (scrollTop < end) ? 'visible' : 'hidden');
7518
8793
  },
@@ -7528,7 +8803,6 @@
7528
8803
 
7529
8804
  this.toolbar.unsetDropdownsFixed();
7530
8805
  this.$toolbar.removeClass('toolbar-fixed-box');
7531
-
7532
8806
  },
7533
8807
  setDropdownsFixed: function()
7534
8808
  {
@@ -7540,7 +8814,7 @@
7540
8814
  position = 'absolute';
7541
8815
  }
7542
8816
 
7543
- $('.redactor-dropdown').each(function()
8817
+ $('.redactor-dropdown-' + this.uuid).each(function()
7544
8818
  {
7545
8819
  $(this).css({ position: position, top: top + 'px' });
7546
8820
  });
@@ -7548,7 +8822,7 @@
7548
8822
  unsetDropdownsFixed: function()
7549
8823
  {
7550
8824
  var top = (this.$toolbar.innerHeight() + this.$toolbar.offset().top);
7551
- $('.redactor-dropdown').each(function()
8825
+ $('.redactor-dropdown-' + this.uuid).each(function()
7552
8826
  {
7553
8827
  $(this).css({ position: 'absolute', top: top + 'px' });
7554
8828
  });
@@ -7566,7 +8840,7 @@
7566
8840
  this.upload.$el = $(id);
7567
8841
  this.upload.$droparea = $('<div id="redactor-droparea" />');
7568
8842
 
7569
- this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text('Drop file here or ');
8843
+ this.upload.$placeholdler = $('<div id="redactor-droparea-placeholder" />').text(this.lang.get('upload_label'));
7570
8844
  this.upload.$input = $('<input type="file" name="file" />');
7571
8845
 
7572
8846
  this.upload.$placeholdler.append(this.upload.$input);
@@ -7627,6 +8901,7 @@
7627
8901
  }
7628
8902
 
7629
8903
  this.progress.show();
8904
+ this.core.setCallback('uploadStart', e, formData);
7630
8905
  this.upload.sendData(formData, e);
7631
8906
  },
7632
8907
  setConfig: function(file)
@@ -7646,6 +8921,12 @@
7646
8921
  {
7647
8922
  this.upload.type = 'file';
7648
8923
  }
8924
+
8925
+ if (this.opts.imageUpload === null && this.opts.fileUpload !== null)
8926
+ {
8927
+ this.upload.type = 'file';
8928
+ }
8929
+
7649
8930
  },
7650
8931
  getHiddenFields: function(obj, fd)
7651
8932
  {
@@ -7677,6 +8958,7 @@
7677
8958
 
7678
8959
  var xhr = new XMLHttpRequest();
7679
8960
  xhr.open('POST', this.upload.url);
8961
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
7680
8962
 
7681
8963
  // complete
7682
8964
  xhr.onreadystatechange = $.proxy(function()
@@ -7775,9 +9057,7 @@
7775
9057
  s3executeOnSignedUrl: function(file, callback)
7776
9058
  {
7777
9059
  var xhr = new XMLHttpRequest();
7778
-
7779
- var mark = '?';
7780
- if (this.opts.s3.search(/\?/) != '-1') mark = '&';
9060
+ var mark = (this.opts.s3.search(/\?/) !== '-1') ? '?' : '&';
7781
9061
 
7782
9062
  xhr.open('GET', this.opts.s3 + mark + 'name=' + file.name + '&type=' + file.type, true);
7783
9063
 
@@ -7867,21 +9147,9 @@
7867
9147
  }
7868
9148
  }, this);
7869
9149
 
7870
- xhr.onerror = function()
7871
- {
7872
- //setProgress(0, 'XHR error.');
7873
- };
9150
+ xhr.onerror = function() {};
7874
9151
 
7875
- xhr.upload.onprogress = function(e)
7876
- {
7877
- /*
7878
- if (e.lengthComputable)
7879
- {
7880
- var percentLoaded = Math.round((e.loaded / e.total) * 100);
7881
- setProgress(percentLoaded, percentLoaded == 100 ? 'Finalizing.' : 'Uploading.');
7882
- }
7883
- */
7884
- };
9152
+ xhr.upload.onprogress = function(e) {};
7885
9153
 
7886
9154
  xhr.setRequestHeader('Content-Type', file.type);
7887
9155
  xhr.setRequestHeader('x-amz-acl', 'public-read');
@@ -7914,6 +9182,7 @@
7914
9182
  html = html.replace(/\s/g, '');
7915
9183
  html = html.replace(/^<p>[^\W\w\D\d]*?<\/p>$/i, '');
7916
9184
  html = html.replace(/<iframe(.*?[^>])>$/i, 'iframe');
9185
+ html = html.replace(/<source(.*?[^>])>$/i, 'source');
7917
9186
 
7918
9187
  // remove empty tags
7919
9188
  if (removeEmptyTags !== false)
@@ -7978,12 +9247,13 @@
7978
9247
  },
7979
9248
  removeEmpty: function(i, s)
7980
9249
  {
7981
- var $s = $(s);
9250
+ var $s = $($.parseHTML(s));
7982
9251
 
7983
9252
  $s.find('.redactor-invisible-space').removeAttr('style').removeAttr('class');
7984
9253
 
7985
- if ($s.find('hr, br, img, iframe').length !== 0) return;
9254
+ if ($s.find('hr, br, img, iframe, source').length !== 0) return;
7986
9255
  var text = $.trim($s.text());
9256
+
7987
9257
  if (this.utils.isEmpty(text, false))
7988
9258
  {
7989
9259
  $s.remove();
@@ -7993,8 +9263,6 @@
7993
9263
  // save and restore scroll
7994
9264
  saveScroll: function()
7995
9265
  {
7996
- if (this.utils.isSelectAll()) return;
7997
-
7998
9266
  this.saveEditorScroll = this.$editor.scrollTop();
7999
9267
  this.saveBodyScroll = $(window).scrollTop();
8000
9268
 
@@ -8039,6 +9307,8 @@
8039
9307
 
8040
9308
  return $(this).contents();
8041
9309
  });
9310
+
9311
+ return $(node);
8042
9312
  },
8043
9313
  replaceToTag: function(node, tag, removeInlineTags)
8044
9314
  {
@@ -8071,22 +9341,31 @@
8071
9341
 
8072
9342
  return (offset === 0) ? true : false;
8073
9343
  },
8074
- isEndOfElement: function()
9344
+ isEndOfElement: function(element)
8075
9345
  {
8076
- var block = this.selection.getBlock();
8077
- if (!block) return false;
9346
+ if (typeof element == 'undefined')
9347
+ {
9348
+ var element = this.selection.getBlock();
9349
+ if (!element) return false;
9350
+ }
8078
9351
 
8079
- var offset = this.caret.getOffsetOfElement(block);
8080
- var text = $.trim($(block).text()).replace(/\n\r\n/g, '');
9352
+ var offset = this.caret.getOffsetOfElement(element);
9353
+ var text = $.trim($(element).text()).replace(/\n\r\n/g, '');
8081
9354
 
8082
9355
  return (offset == text.length) ? true : false;
8083
9356
  },
9357
+ isStartOfEditor: function()
9358
+ {
9359
+ var offset = this.caret.getOffsetOfElement(this.$editor[0]);
9360
+
9361
+ return (offset === 0) ? true : false;
9362
+ },
8084
9363
  isEndOfEditor: function()
8085
9364
  {
8086
9365
  var block = this.$editor[0];
8087
9366
 
8088
9367
  var offset = this.caret.getOffsetOfElement(block);
8089
- var text = $.trim($(block).text()).replace(/\n\r\n/g, '');
9368
+ var text = $.trim($(block).html().replace(/(<([^>]+)>)/gi,''));
8090
9369
 
8091
9370
  return (offset == text.length) ? true : false;
8092
9371
  },
@@ -8108,7 +9387,7 @@
8108
9387
  // tag detection
8109
9388
  isTag: function(current, tag)
8110
9389
  {
8111
- var element = $(current).closest(tag);
9390
+ var element = $(current).closest(tag, this.$editor[0]);
8112
9391
  if (element.length == 1)
8113
9392
  {
8114
9393
  return element[0];
@@ -8209,104 +9488,62 @@
8209
9488
 
8210
9489
  if (browser == 'safari') return (typeof match[3] != 'undefined') ? match[3] == 'safari' : false;
8211
9490
  if (browser == 'version') return match[2];
8212
- if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'webkit');
9491
+ if (browser == 'webkit') return (match[1] == 'chrome' || match[1] == 'opr' || match[1] == 'webkit');
8213
9492
  if (match[1] == 'rv') return browser == 'msie';
8214
9493
  if (match[1] == 'opr') return browser == 'webkit';
8215
9494
 
8216
9495
  return browser == match[1];
8217
- }
8218
- };
8219
- }
8220
- };
8221
-
8222
- $(window).on('load.tools.redactor', function()
8223
- {
8224
- $('[data-tools="redactor"]').redactor();
8225
- });
8226
-
8227
- // constructor
8228
- Redactor.prototype.init.prototype = Redactor.prototype;
8229
-
8230
- // LINKIFY
8231
- $.Redactor.fn.formatLinkify = function(protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize)
8232
- {
8233
- var urlCheck = '((?:http[s]?:\\/\\/(?:www\\.)?|www\\.){1}(?:[0-9A-Za-z\\-%_]+\\.)+[a-zA-Z]{2,}(?::[0-9]+)?(?:(?:/[0-9A-Za-z\\-#\\.%\+_]*)+)?(?:\\?(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?(?:&(?:[0-9A-Za-z\\-\\.%_]+(?:=[0-9A-Za-z\\-\\.%_\\+]*)?)?)*)?(?:#[0-9A-Za-z\\-\\.%_\\+=\\?&;]*)?)';
8234
- var regex = new RegExp(urlCheck, 'gi');
8235
- var rProtocol = /(https?|ftp):\/\//i;
8236
- var urlImage = /(https?:\/\/.*\.(?:png|jpg|jpeg|gif))/gi;
8237
-
8238
- var childNodes = (this.$editor ? this.$editor[0] : this).childNodes, i = childNodes.length;
8239
- while (i--)
8240
- {
8241
- var n = childNodes[i];
8242
-
8243
- if (n.nodeType === 3 && n.parentNode !== 'PRE')
8244
- {
8245
- var html = n.nodeValue;
8246
-
8247
- // youtube & vimeo
8248
- if (convertVideoLinks && html)
9496
+ },
9497
+ strpos: function(haystack, needle, offset)
9498
+ {
9499
+ var i = haystack.indexOf(needle, offset);
9500
+ return i >= 0 ? i : false;
9501
+ },
9502
+ disableBodyScroll: function()
8249
9503
  {
8250
- var iframeStart = '<iframe width="500" height="281" src="',
8251
- iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
8252
9504
 
8253
- if (html.match(reUrlYoutube))
8254
- {
8255
- html = html.replace(reUrlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
8256
- $(n).after(html).remove();
8257
- }
8258
- else if (html.match(reUrlVimeo))
9505
+ var $body = $('html');
9506
+ var windowWidth = window.innerWidth;
9507
+ if (!windowWidth)
8259
9508
  {
8260
- html = html.replace(reUrlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
8261
- $(n).after(html).remove();
9509
+ var documentElementRect = document.documentElement.getBoundingClientRect();
9510
+ windowWidth = documentElementRect.right - Math.abs(documentElementRect.left);
8262
9511
  }
8263
- }
8264
9512
 
8265
- // image
8266
- if (convertImageLinks && html && html.match(urlImage))
8267
- {
8268
- html = html.replace(urlImage, '<img src="$1" />');
9513
+ var isOverflowing = document.body.clientWidth < windowWidth;
9514
+ var scrollbarWidth = this.utils.measureScrollbar();
8269
9515
 
8270
- $(n).after(html).remove();
8271
- return;
8272
- }
9516
+ $body.css('overflow', 'hidden');
9517
+ if (isOverflowing) $body.css('padding-right', scrollbarWidth);
8273
9518
 
8274
- // link
8275
- if (html.search(/\$/g) != -1) html = html.replace(/\$/g, '&#36;');
8276
9519
 
8277
- var matches = html.match(regex);
8278
- if (convertUrlLinks && html && matches)
9520
+ },
9521
+ measureScrollbar: function()
8279
9522
  {
9523
+ var $body = $('body');
9524
+ var scrollDiv = document.createElement('div');
9525
+ scrollDiv.className = 'redactor-scrollbar-measure';
8280
9526
 
8281
- var len = matches.length;
8282
- for (var z = 0; z < len; z++)
8283
- {
8284
- // remove dot in the end
8285
- if (matches[z].match(/\.$/) !== null) matches[z] = matches[z].replace(/\.$/, '');
8286
-
8287
- var href = matches[z];
8288
- var text = href;
8289
-
8290
- var space = '';
8291
- if (href.match(/\s$/) !== null) space = ' ';
8292
-
8293
- var addProtocol = protocol + '://';
8294
- if (href.match(rProtocol) !== null) addProtocol = '';
8295
-
8296
- if (text.length > linkSize) text = text.substring(0, linkSize) + '...';
8297
- text = text.replace(/&#36;/g, '$').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
8298
-
8299
- html = html.replace(href, '<a href=\"' + addProtocol + $.trim(href) + '\">' + $.trim(text) + '</a>' + space);
8300
- }
8301
-
8302
- $(n).after(html).remove();
9527
+ $body.append(scrollDiv);
9528
+ var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
9529
+ $body[0].removeChild(scrollDiv);
9530
+ return scrollbarWidth;
9531
+ },
9532
+ enableBodyScroll: function()
9533
+ {
9534
+ $('html').css({ 'overflow': '', 'padding-right': '' });
9535
+ $('body').remove('redactor-scrollbar-measure');
8303
9536
  }
8304
- }
8305
- else if (n.nodeType === 1 && !/^(pre|a|button|textarea)$/i.test(n.tagName))
8306
- {
8307
- $.Redactor.fn.formatLinkify.call(n, protocol, convertLinks, convertUrlLinks, convertImageLinks, convertVideoLinks, linkSize);
8308
- }
9537
+ };
8309
9538
  }
8310
9539
  };
8311
9540
 
9541
+ $(window).on('load.tools.redactor', function()
9542
+ {
9543
+ $('[data-tools="redactor"]').redactor();
9544
+ });
9545
+
9546
+ // constructor
9547
+ Redactor.prototype.init.prototype = Redactor.prototype;
9548
+
8312
9549
  })(jQuery);