comfortable_mexican_sofa 1.12.8 → 1.12.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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);