trumbowyg_rails 1.1.3 → 2.1.0.2

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/README.md +9 -7
  3. data/VERSION +1 -1
  4. data/lib/{trumbowyg_rails.rb → trumbowyg2-rails.rb} +0 -0
  5. data/trumbowyg_rails.gemspec +5 -5
  6. data/vendor/assets/images/trumbowyg/images/icons.svg +1 -0
  7. data/vendor/assets/javascripts/trumbowyg/langs/ar.js +56 -0
  8. data/vendor/assets/javascripts/trumbowyg/langs/ca.js +33 -34
  9. data/vendor/assets/javascripts/trumbowyg/langs/cs.js +54 -0
  10. data/vendor/assets/javascripts/trumbowyg/langs/da.js +55 -0
  11. data/vendor/assets/javascripts/trumbowyg/langs/de.js +33 -34
  12. data/vendor/assets/javascripts/trumbowyg/langs/el.js +58 -0
  13. data/vendor/assets/javascripts/trumbowyg/langs/es.js +33 -34
  14. data/vendor/assets/javascripts/trumbowyg/langs/es_ar.js +34 -34
  15. data/vendor/assets/javascripts/trumbowyg/langs/fa.js +33 -34
  16. data/vendor/assets/javascripts/trumbowyg/langs/fi.js +33 -34
  17. data/vendor/assets/javascripts/trumbowyg/langs/fr.js +40 -34
  18. data/vendor/assets/javascripts/trumbowyg/langs/he.js +57 -0
  19. data/vendor/assets/javascripts/trumbowyg/langs/hu.js +58 -0
  20. data/vendor/assets/javascripts/trumbowyg/langs/id.js +33 -34
  21. data/vendor/assets/javascripts/trumbowyg/langs/it.js +33 -34
  22. data/vendor/assets/javascripts/trumbowyg/langs/ja.js +56 -0
  23. data/vendor/assets/javascripts/trumbowyg/langs/ko.js +33 -34
  24. data/vendor/assets/javascripts/trumbowyg/langs/my.js +54 -0
  25. data/vendor/assets/javascripts/trumbowyg/langs/nl.js +56 -0
  26. data/vendor/assets/javascripts/trumbowyg/langs/pl.js +33 -34
  27. data/vendor/assets/javascripts/trumbowyg/langs/pt.js +33 -34
  28. data/vendor/assets/javascripts/trumbowyg/langs/ro.js +33 -35
  29. data/vendor/assets/javascripts/trumbowyg/langs/rs.js +53 -0
  30. data/vendor/assets/javascripts/trumbowyg/langs/rs_latin.js +54 -0
  31. data/vendor/assets/javascripts/trumbowyg/langs/ru.js +33 -34
  32. data/vendor/assets/javascripts/trumbowyg/langs/sk.js +54 -0
  33. data/vendor/assets/javascripts/trumbowyg/langs/sv.js +55 -0
  34. data/vendor/assets/javascripts/trumbowyg/langs/tr.js +34 -36
  35. data/vendor/assets/javascripts/trumbowyg/langs/ua.js +54 -0
  36. data/vendor/assets/javascripts/trumbowyg/langs/vi.js +55 -0
  37. data/vendor/assets/javascripts/trumbowyg/langs/zh_cn.js +34 -34
  38. data/vendor/assets/javascripts/trumbowyg/langs/zh_tw.js +57 -0
  39. data/vendor/assets/javascripts/trumbowyg/plugins/base64/trumbowyg.base64.js +90 -0
  40. data/vendor/assets/javascripts/trumbowyg/plugins/colors/trumbowyg.colors.js +164 -0
  41. data/vendor/assets/javascripts/trumbowyg/plugins/noembed/trumbowyg.noembed.js +96 -0
  42. data/vendor/assets/javascripts/trumbowyg/plugins/pasteimage/trumbowyg.pasteimage.js +39 -0
  43. data/vendor/assets/javascripts/trumbowyg/plugins/preformatted/trumbowyg.preformatted.js +117 -0
  44. data/vendor/assets/javascripts/trumbowyg/plugins/upload/trumbowyg.upload.js +204 -0
  45. data/vendor/assets/javascripts/trumbowyg/trumbowyg.js +1126 -700
  46. data/vendor/assets/stylesheets/trumbowyg/plugins/colors/trumbowyg.colors.scss +49 -0
  47. data/vendor/assets/stylesheets/trumbowyg/trumbowyg.scss +351 -159
  48. metadata +33 -11
  49. data/vendor/assets/images/trumbowyg/images/icons-2x.png +0 -0
  50. data/vendor/assets/images/trumbowyg/images/icons.png +0 -0
  51. data/vendor/assets/stylesheets/trumbowyg/_sprite-2x.scss +0 -28
  52. data/vendor/assets/stylesheets/trumbowyg/_sprite.scss +0 -28
@@ -0,0 +1,204 @@
1
+ /* ===========================================================
2
+ * trumbowyg.upload.js v1.1
3
+ * Upload plugin for Trumbowyg
4
+ * http://alex-d.github.com/Trumbowyg
5
+ * ===========================================================
6
+ * Author : Alexandre Demode (Alex-D)
7
+ * Twitter : @AlexandreDemode
8
+ * Website : alex-d.fr
9
+ */
10
+
11
+ (function ($) {
12
+ 'use strict';
13
+
14
+ var defaultOptions = {
15
+ serverPath: './src/plugins/upload/trumbowyg.upload.php',
16
+ fileFieldName: 'fileToUpload',
17
+ data: [],
18
+ headers: {},
19
+ xhrFields: {},
20
+ urlPropertyName: 'file',
21
+ statusPropertyName: 'success',
22
+ success: undefined,
23
+ error: undefined
24
+ };
25
+
26
+ function getDeep(object, propertyParts) {
27
+ var mainProperty = propertyParts.shift(),
28
+ otherProperties = propertyParts;
29
+
30
+ if (object !== null) {
31
+ if (otherProperties.length === 0) {
32
+ return object[mainProperty];
33
+ }
34
+
35
+ if (typeof object === 'object') {
36
+ return getDeep(object[mainProperty], otherProperties);
37
+ }
38
+ }
39
+ return object;
40
+ }
41
+
42
+ addXhrProgressEvent();
43
+
44
+ $.extend(true, $.trumbowyg, {
45
+ langs: {
46
+ // jshint camelcase:false
47
+ en: {
48
+ upload: 'Upload',
49
+ file: 'File',
50
+ uploadError: 'Error'
51
+ },
52
+ sk: {
53
+ upload: 'Nahrať',
54
+ file: 'Súbor',
55
+ uploadError: 'Chyba'
56
+ },
57
+ fr: {
58
+ upload: 'Envoi',
59
+ file: 'Fichier',
60
+ uploadError: 'Erreur'
61
+ },
62
+ cs: {
63
+ upload: 'Nahrát obrázek',
64
+ file: 'Soubor',
65
+ uploadError: 'Chyba'
66
+ },
67
+ zh_cn: {
68
+ upload: '上传',
69
+ file: '文件',
70
+ uploadError: '错误'
71
+ }
72
+ },
73
+ // jshint camelcase:true
74
+
75
+ plugins: {
76
+ upload: {
77
+ init: function (trumbowyg) {
78
+ trumbowyg.o.plugins.upload = $.extend(true, {}, defaultOptions, trumbowyg.o.plugins.upload || {});
79
+ var btnDef = {
80
+ fn: function () {
81
+ trumbowyg.saveRange();
82
+
83
+ var file,
84
+ prefix = trumbowyg.o.prefix;
85
+
86
+ var $modal = trumbowyg.openModalInsert(
87
+ // Title
88
+ trumbowyg.lang.upload,
89
+
90
+ // Fields
91
+ {
92
+ file: {
93
+ type: 'file',
94
+ required: true
95
+ },
96
+ alt: {
97
+ label: 'description',
98
+ value: trumbowyg.getRangeText()
99
+ }
100
+ },
101
+
102
+ // Callback
103
+ function (values) {
104
+ var data = new FormData();
105
+ data.append(trumbowyg.o.plugins.upload.fileFieldName, file);
106
+
107
+ trumbowyg.o.plugins.upload.data.map(function (cur) {
108
+ data.append(cur.name, cur.value);
109
+ });
110
+
111
+ if ($('.' + prefix + 'progress', $modal).length === 0) {
112
+ $('.' + prefix + 'modal-title', $modal)
113
+ .after(
114
+ $('<div/>', {
115
+ 'class': prefix + 'progress'
116
+ }).append(
117
+ $('<div/>', {
118
+ 'class': prefix + 'progress-bar'
119
+ })
120
+ )
121
+ );
122
+ }
123
+
124
+ $.ajax({
125
+ url: trumbowyg.o.plugins.upload.serverPath,
126
+ headers: trumbowyg.o.plugins.upload.headers,
127
+ xhrFields: trumbowyg.o.plugins.upload.xhrFields,
128
+ type: 'POST',
129
+ data: data,
130
+ cache: false,
131
+ dataType: 'json',
132
+ processData: false,
133
+ contentType: false,
134
+
135
+ progressUpload: function (e) {
136
+ $('.' + prefix + 'progress-bar').stop().animate({
137
+ width: Math.round(e.loaded * 100 / e.total) + '%'
138
+ }, 200);
139
+ },
140
+
141
+ success: trumbowyg.o.plugins.upload.success || function (data) {
142
+ if (!!getDeep(data, trumbowyg.o.plugins.upload.statusPropertyName.split('.'))) {
143
+ var url = getDeep(data, trumbowyg.o.plugins.upload.urlPropertyName.split('.'));
144
+ trumbowyg.execCmd('insertImage', url);
145
+ $('img[src="' + url + '"]:not([alt])', trumbowyg.$box).attr('alt', values.alt);
146
+ setTimeout(function () {
147
+ trumbowyg.closeModal();
148
+ }, 250);
149
+ } else {
150
+ trumbowyg.addErrorOnModalField(
151
+ $('input[type=file]', $modal),
152
+ trumbowyg.lang[data.message]
153
+ );
154
+ }
155
+ },
156
+ error: trumbowyg.o.plugins.upload.error || function () {
157
+ trumbowyg.addErrorOnModalField(
158
+ $('input[type=file]', $modal),
159
+ trumbowyg.lang.uploadError
160
+ );
161
+ }
162
+ });
163
+ }
164
+ );
165
+
166
+ $('input[type=file]').on('change', function (e) {
167
+ try {
168
+ // If multiple files allowed, we just get the first.
169
+ file = e.target.files[0];
170
+ } catch (err) {
171
+ // In IE8, multiple files not allowed
172
+ file = e.target.value;
173
+ }
174
+ });
175
+ }
176
+ };
177
+
178
+ trumbowyg.addBtnDef('upload', btnDef);
179
+ }
180
+ }
181
+ }
182
+ });
183
+
184
+
185
+ function addXhrProgressEvent() {
186
+ if (!$.trumbowyg && !$.trumbowyg.addedXhrProgressEvent) { // Avoid adding progress event multiple times
187
+ var originalXhr = $.ajaxSettings.xhr;
188
+ $.ajaxSetup({
189
+ xhr: function () {
190
+ var req = originalXhr(),
191
+ that = this;
192
+ if (req && typeof req.upload === 'object' && that.progressUpload !== undefined) {
193
+ req.upload.addEventListener('progress', function (e) {
194
+ that.progressUpload(e);
195
+ }, false);
196
+ }
197
+
198
+ return req;
199
+ }
200
+ });
201
+ $.trumbowyg.addedXhrProgressEvent = true;
202
+ }
203
+ }
204
+ })(jQuery);
@@ -1,99 +1,111 @@
1
- jQuery.trumbowyg = {
1
+ jQuery.trumbowyg = {
2
2
  langs: {
3
3
  en: {
4
- viewHTML: "View HTML",
4
+ viewHTML: 'View HTML',
5
5
 
6
- formatting: "Formatting",
7
- p: "Paragraph",
8
- blockquote: "Quote",
9
- code: "Code",
10
- header: "Header",
6
+ undo: 'Undo',
7
+ redo: 'Redo',
11
8
 
12
- bold: "Bold",
13
- italic: "Italic",
14
- strikethrough: "Stroke",
15
- underline: "Underline",
9
+ formatting: 'Formatting',
10
+ p: 'Paragraph',
11
+ blockquote: 'Quote',
12
+ code: 'Code',
13
+ header: 'Header',
16
14
 
17
- strong: "Strong",
18
- em: "Emphasis",
19
- del: "Deleted",
15
+ bold: 'Bold',
16
+ italic: 'Italic',
17
+ strikethrough: 'Stroke',
18
+ underline: 'Underline',
20
19
 
21
- unorderedList: "Unordered list",
22
- orderedList: "Ordered list",
20
+ strong: 'Strong',
21
+ em: 'Emphasis',
22
+ del: 'Deleted',
23
23
 
24
- insertImage: "Insert Image",
25
- insertVideo: "Insert Video",
26
- link: "Link",
27
- createLink: "Insert link",
28
- unlink: "Remove link",
24
+ superscript: 'Superscript',
25
+ subscript: 'Subscript',
29
26
 
30
- justifyLeft: "Align Left",
31
- justifyCenter: "Align Center",
32
- justifyRight: "Align Right",
33
- justifyFull: "Align Justify",
27
+ unorderedList: 'Unordered list',
28
+ orderedList: 'Ordered list',
34
29
 
35
- horizontalRule: "Insert horizontal rule",
30
+ insertImage: 'Insert Image',
31
+ link: 'Link',
32
+ createLink: 'Insert link',
33
+ unlink: 'Remove link',
36
34
 
37
- fullscreen: "fullscreen",
35
+ justifyLeft: 'Align Left',
36
+ justifyCenter: 'Align Center',
37
+ justifyRight: 'Align Right',
38
+ justifyFull: 'Align Justify',
38
39
 
39
- close: "Close",
40
+ horizontalRule: 'Insert horizontal rule',
41
+ removeformat: 'Remove format',
40
42
 
41
- submit: "Confirm",
42
- reset: "Cancel",
43
+ fullscreen: 'Fullscreen',
43
44
 
44
- invalidUrl: "Invalid URL",
45
- required: "Required",
46
- description: "Description",
47
- title: "Title",
48
- text: "Text"
45
+ close: 'Close',
46
+
47
+ submit: 'Confirm',
48
+ reset: 'Cancel',
49
+
50
+ required: 'Required',
51
+ description: 'Description',
52
+ title: 'Title',
53
+ text: 'Text',
54
+ target: 'Target'
49
55
  }
50
56
  },
51
57
 
52
- // User default options
53
- opts: {},
58
+ // Plugins
59
+ plugins: {},
54
60
 
55
- btnsGrps: {
56
- design: ['bold', 'italic', 'underline', 'strikethrough'],
57
- semantic: ['strong', 'em', 'del'],
58
- justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
59
- lists: ['unorderedList', 'orderedList']
60
- }
61
+ // SVG Path globally
62
+ svgPath: null
61
63
  };
62
64
 
63
65
 
64
-
65
- (function(window, document, $){
66
+ (function (navigator, window, document, $) {
66
67
  'use strict';
67
68
 
68
- // @param : o are options
69
- // @param : p are params
70
- $.fn.trumbowyg = function(o, p){
71
- if(o === Object(o) || !o){
72
- return this.each(function(){
73
- if(!$(this).data('trumbowyg'))
74
- $(this).data('trumbowyg', new Trumbowyg(this, o));
69
+ $.fn.trumbowyg = function (options, params) {
70
+ var trumbowygDataName = 'trumbowyg';
71
+ if (options === Object(options) || !options) {
72
+ return this.each(function () {
73
+ if (!$(this).data(trumbowygDataName)) {
74
+ $(this).data(trumbowygDataName, new Trumbowyg(this, options));
75
+ }
75
76
  });
76
- } else if(this.length === 1){
77
+ }
78
+ if (this.length === 1) {
77
79
  try {
78
- var t = $(this).data('trumbowyg');
79
- switch(o){
80
+ var t = $(this).data(trumbowygDataName);
81
+ switch (options) {
82
+ // Exec command
83
+ case 'execCmd':
84
+ return t.execCmd(params.cmd, params.param, params.forceCss);
85
+
80
86
  // Modal box
81
87
  case 'openModal':
82
- return t.openModal(p.title, p.content);
88
+ return t.openModal(params.title, params.content);
83
89
  case 'closeModal':
84
90
  return t.closeModal();
85
91
  case 'openModalInsert':
86
- return t.openModalInsert(p.title, p.fields, p.callback);
87
-
88
- // Selection
89
- case 'saveSelection':
90
- return t.saveSelection();
91
- case 'getSelection':
92
- return t.selection;
93
- case 'getSelectedText':
94
- return t.selection+'';
95
- case 'restoreSelection':
96
- return t.restoreSelection();
92
+ return t.openModalInsert(params.title, params.fields, params.callback);
93
+
94
+ // Range
95
+ case 'saveRange':
96
+ return t.saveRange();
97
+ case 'getRange':
98
+ return t.range;
99
+ case 'getRangeText':
100
+ return t.getRangeText();
101
+ case 'restoreRange':
102
+ return t.restoreRange();
103
+
104
+ // Enable/disable
105
+ case 'enable':
106
+ return t.toggleDisable(false);
107
+ case 'disable':
108
+ return t.toggleDisable(true);
97
109
 
98
110
  // Destroy
99
111
  case 'destroy':
@@ -103,181 +115,326 @@
103
115
  case 'empty':
104
116
  return t.empty();
105
117
 
106
- // Public options
107
- case 'lang':
108
- return t.lang;
109
- case 'duration':
110
- return t.o.duration;
111
-
112
118
  // HTML
113
119
  case 'html':
114
- return t.html(p);
120
+ return t.html(params);
115
121
  }
116
- } catch(e){}
122
+ } catch (c) {
123
+ }
117
124
  }
118
125
 
119
126
  return false;
120
127
  };
121
128
 
129
+ // @param: editorElem is the DOM element
130
+ var Trumbowyg = function (editorElem, options) {
131
+ var t = this,
132
+ trumbowygIconsId = 'trumbowyg-icons';
122
133
 
134
+ // Get the document of the element. It use to makes the plugin
135
+ // compatible on iframes.
136
+ t.doc = editorElem.ownerDocument || document;
123
137
 
124
- var Trumbowyg = function(editorElem, opts){
125
- var t = this;
126
138
  // jQuery object of the editor
127
- t.$e = $(editorElem);
128
- t.$creator = $(editorElem);
139
+ t.$ta = $(editorElem); // $ta : Textarea
140
+ t.$c = $(editorElem); // $c : creator
129
141
 
130
- // Extend with options
131
- opts = $.extend(true, {}, opts, $.trumbowyg.opts);
142
+ options = options || {};
132
143
 
133
144
  // Localization management
134
- if(typeof opts.lang === 'undefined' || typeof $.trumbowyg.langs[opts.lang] === 'undefined')
145
+ if (options.lang != null || $.trumbowyg.langs[options.lang] != null) {
146
+ t.lang = $.extend(true, {}, $.trumbowyg.langs.en, $.trumbowyg.langs[options.lang]);
147
+ } else {
135
148
  t.lang = $.trumbowyg.langs.en;
136
- else
137
- t.lang = $.extend(true, {}, $.trumbowyg.langs.en, $.trumbowyg.langs[opts.lang]);
149
+ }
150
+
151
+ // SVG path
152
+ var svgPathOption = $.trumbowyg.svgPath != null ? $.trumbowyg.svgPath : options.svgPath;
153
+ t.hasSvg = svgPathOption !== false;
154
+ t.svgPath = !!t.doc.querySelector('base') ? window.location : '';
155
+ if ($('#' + trumbowygIconsId, t.doc).length === 0 && svgPathOption !== false) {
156
+ if (svgPathOption == null) {
157
+ try {
158
+ throw new Error();
159
+ } catch (e) {
160
+ var stackLines = e.stack.split('\n');
161
+
162
+ for (var i in stackLines) {
163
+ if (!stackLines[i].match(/http[s]?:\/\//)) {
164
+ continue;
165
+ }
166
+ svgPathOption = stackLines[Number(i)].match(/((http[s]?:\/\/.+\/)([^\/]+\.js)):/)[1].split('/');
167
+ svgPathOption.pop();
168
+ svgPathOption = svgPathOption.join('/') + '/ui/icons.svg';
169
+ break;
170
+ }
171
+ }
172
+ }
173
+
174
+ var div = t.doc.createElement('div');
175
+ div.id = trumbowygIconsId;
176
+ t.doc.body.insertBefore(div, t.doc.body.childNodes[0]);
177
+ $.get(svgPathOption, function (data) {
178
+ div.innerHTML = new XMLSerializer().serializeToString(data.documentElement);
179
+ });
180
+ }
181
+
182
+
183
+ /**
184
+ * When the button is associated to a empty object
185
+ * fn and title attributs are defined from the button key value
186
+ *
187
+ * For example
188
+ * foo: {}
189
+ * is equivalent to :
190
+ * foo: {
191
+ * fn: 'foo',
192
+ * title: this.lang.foo
193
+ * }
194
+ */
195
+ var h = t.lang.header, // Header translation
196
+ isBlinkFunction = function () {
197
+ return (window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window;
198
+ };
199
+ t.btnsDef = {
200
+ viewHTML: {
201
+ fn: 'toggle'
202
+ },
203
+
204
+ undo: {
205
+ isSupported: isBlinkFunction,
206
+ key: 'Z'
207
+ },
208
+ redo: {
209
+ isSupported: isBlinkFunction,
210
+ key: 'Y'
211
+ },
212
+
213
+ p: {
214
+ fn: 'formatBlock'
215
+ },
216
+ blockquote: {
217
+ fn: 'formatBlock'
218
+ },
219
+ h1: {
220
+ fn: 'formatBlock',
221
+ title: h + ' 1'
222
+ },
223
+ h2: {
224
+ fn: 'formatBlock',
225
+ title: h + ' 2'
226
+ },
227
+ h3: {
228
+ fn: 'formatBlock',
229
+ title: h + ' 3'
230
+ },
231
+ h4: {
232
+ fn: 'formatBlock',
233
+ title: h + ' 4'
234
+ },
235
+ subscript: {
236
+ tag: 'sub'
237
+ },
238
+ superscript: {
239
+ tag: 'sup'
240
+ },
241
+
242
+ bold: {
243
+ key: 'B'
244
+ },
245
+ italic: {
246
+ key: 'I'
247
+ },
248
+ underline: {
249
+ tag: 'u'
250
+ },
251
+ strikethrough: {
252
+ tag: 'strike'
253
+ },
254
+
255
+ strong: {
256
+ fn: 'bold',
257
+ key: 'B'
258
+ },
259
+ em: {
260
+ fn: 'italic',
261
+ key: 'I'
262
+ },
263
+ del: {
264
+ fn: 'strikethrough'
265
+ },
266
+
267
+ createLink: {
268
+ key: 'K',
269
+ tag: 'a'
270
+ },
271
+ unlink: {},
272
+
273
+ insertImage: {},
274
+
275
+ justifyLeft: {
276
+ tag: 'left',
277
+ forceCss: true
278
+ },
279
+ justifyCenter: {
280
+ tag: 'center',
281
+ forceCss: true
282
+ },
283
+ justifyRight: {
284
+ tag: 'right',
285
+ forceCss: true
286
+ },
287
+ justifyFull: {
288
+ tag: 'justify',
289
+ forceCss: true
290
+ },
291
+
292
+ unorderedList: {
293
+ fn: 'insertUnorderedList',
294
+ tag: 'ul'
295
+ },
296
+ orderedList: {
297
+ fn: 'insertOrderedList',
298
+ tag: 'ol'
299
+ },
300
+
301
+ horizontalRule: {
302
+ fn: 'insertHorizontalRule'
303
+ },
304
+
305
+ removeformat: {},
306
+
307
+ fullscreen: {
308
+ class: 'trumbowyg-not-disable'
309
+ },
310
+ close: {
311
+ fn: 'destroy',
312
+ class: 'trumbowyg-not-disable'
313
+ },
314
+
315
+ // Dropdowns
316
+ formatting: {
317
+ dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4'],
318
+ ico: 'p'
319
+ },
320
+ link: {
321
+ dropdown: ['createLink', 'unlink']
322
+ }
323
+ };
138
324
 
139
325
  // Defaults Options
140
- t.o = $.extend(true, {
326
+ t.o = $.extend(true, {}, {
141
327
  lang: 'en',
142
- dir: 'ltr',
143
- duration: 200, // Duration of modal box animations
144
328
 
145
- mobile: false,
146
- tablet: true,
147
- closable: false,
148
- fullscreenable: true,
149
329
  fixedBtnPane: false,
150
330
  fixedFullWidth: false,
151
- semantic: false,
152
- resetCss: false,
153
331
  autogrow: false,
154
332
 
155
333
  prefix: 'trumbowyg-',
156
334
 
157
- convertLink: true,
158
-
159
- btns: ['viewHTML',
160
- '|', 'formatting',
161
- '|', $.trumbowyg.btnsGrps.design,
162
- '|', 'link',
163
- '|', 'insertImage',
164
- '|', $.trumbowyg.btnsGrps.justify,
165
- '|', $.trumbowyg.btnsGrps.lists,
166
- '|', 'horizontalRule'],
167
- btnsAdd: [],
168
-
169
- /**
170
- * When the button is associated to a empty object
171
- * func and title attributs are defined from the button key value
172
- *
173
- * For example
174
- * foo: {}
175
- * is equivalent to :
176
- * foo: {
177
- * func: 'foo',
178
- * title: this.lang.foo
179
- * }
180
- */
181
- btnsDef: {
182
- viewHTML: {
183
- func: 'toggle'
184
- },
185
-
186
- p: {
187
- func: 'formatBlock'
188
- },
189
- blockquote: {
190
- func: 'formatBlock'
191
- },
192
- h1: {
193
- func: 'formatBlock',
194
- title: t.lang.header + ' 1'
195
- },
196
- h2: {
197
- func: 'formatBlock',
198
- title: t.lang.header + ' 2'
199
- },
200
- h3: {
201
- func: 'formatBlock',
202
- title: t.lang.header + ' 3'
203
- },
204
- h4: {
205
- func: 'formatBlock',
206
- title: t.lang.header + ' 4'
207
- },
335
+ semantic: true,
336
+ resetCss: false,
337
+ removeformatPasted: false,
338
+ tagsToRemove: [],
339
+
340
+ btnsGrps: {
341
+ design: ['bold', 'italic', 'underline', 'strikethrough'],
342
+ semantic: ['strong', 'em', 'del'],
343
+ justify: ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
344
+ lists: ['unorderedList', 'orderedList']
345
+ },
346
+ btns: [
347
+ ['viewHTML'],
348
+ ['undo', 'redo'],
349
+ ['formatting'],
350
+ 'btnGrp-semantic',
351
+ ['superscript', 'subscript'],
352
+ ['link'],
353
+ ['insertImage'],
354
+ 'btnGrp-justify',
355
+ 'btnGrp-lists',
356
+ ['horizontalRule'],
357
+ ['removeformat'],
358
+ ['fullscreen']
359
+ ],
360
+ // For custom button definitions
361
+ btnsDef: {},
362
+
363
+ inlineElementsSelector: 'a,abbr,acronym,b,caption,cite,code,col,dfn,dir,dt,dd,em,font,hr,i,kbd,li,q,span,strikeout,strong,sub,sup,u',
364
+
365
+ pasteHandlers: [],
366
+
367
+ imgDblClickHandler: function () {
368
+ var $img = $(this),
369
+ src = $img.attr('src'),
370
+ base64 = '(Base64)';
371
+
372
+ if (src.indexOf('data:image') === 0) {
373
+ src = base64;
374
+ }
208
375
 
209
- bold: {},
210
- italic: {},
211
- underline: {},
212
- strikethrough: {},
376
+ t.openModalInsert(t.lang.insertImage, {
377
+ url: {
378
+ label: 'URL',
379
+ value: src,
380
+ required: true
381
+ },
382
+ alt: {
383
+ label: t.lang.description,
384
+ value: $img.attr('alt')
385
+ }
386
+ }, function (v) {
387
+ if (v.src !== base64) {
388
+ $img.attr({
389
+ src: v.src
390
+ });
391
+ }
392
+ $img.attr({
393
+ alt: v.alt
394
+ });
395
+ return true;
396
+ });
397
+ return false;
398
+ },
213
399
 
214
- strong: {
215
- func: 'bold'
216
- },
217
- em: {
218
- func: 'italic'
219
- },
220
- del: {
221
- func: 'strikethrough'
222
- },
400
+ plugins: {}
401
+ }, options);
223
402
 
224
- createLink: {},
225
- unlink: {},
403
+ t.disabled = t.o.disabled || (editorElem.nodeName === 'TEXTAREA' && editorElem.disabled);
226
404
 
227
- insertImage: {},
405
+ if (options.btns) {
406
+ t.o.btns = options.btns;
407
+ } else if (!t.o.semantic) {
408
+ t.o.btns[4] = 'btnGrp-design';
409
+ }
228
410
 
229
- justifyLeft: {},
230
- justifyCenter: {},
231
- justifyRight: {},
232
- justifyFull: {},
411
+ $.each(t.o.btnsDef, function (btnName, btnDef) {
412
+ t.addBtnDef(btnName, btnDef);
413
+ });
233
414
 
234
- unorderedList: {
235
- func: 'insertUnorderedList'
236
- },
237
- orderedList: {
238
- func: 'insertOrderedList'
239
- },
415
+ // Keyboard shortcuts are load in this array
416
+ t.keys = [];
240
417
 
241
- horizontalRule: {
242
- func: 'insertHorizontalRule'
243
- },
418
+ // Tag to button dynamically hydrated
419
+ t.tagToButton = {};
420
+ t.tagHandlers = [];
244
421
 
245
- // Dropdowns
246
- formatting: {
247
- dropdown: ['p', 'blockquote', 'h1', 'h2', 'h3', 'h4']
248
- },
249
- link: {
250
- dropdown: ['createLink', 'unlink']
251
- }
252
- }
253
- }, opts);
254
-
255
- if(t.o.semantic && !opts.btns)
256
- t.o.btns = [
257
- 'viewHTML',
258
- '|', 'formatting',
259
- '|', $.trumbowyg.btnsGrps.semantic,
260
- '|', 'link',
261
- '|', 'insertImage',
262
- '|', $.trumbowyg.btnsGrps.justify,
263
- '|', $.trumbowyg.btnsGrps.lists,
264
- '|', 'horizontalRule'
265
- ];
266
- else if(opts && opts.btns)
267
- t.o.btns = opts.btns;
422
+ // Admit multiple paste handlers
423
+ t.pasteHandlers = [].concat(t.o.pasteHandlers);
268
424
 
269
425
  t.init();
270
426
  };
271
427
 
272
428
  Trumbowyg.prototype = {
273
- init: function(){
429
+ init: function () {
274
430
  var t = this;
275
- t.height = t.$e.css('height');
431
+ t.height = t.$ta.height();
276
432
 
277
- if(t.isEnabled()){
278
- t.buildEditor(true);
279
- return;
280
- }
433
+ t.initPlugins();
434
+
435
+ // Disable image resize in Firefox
436
+ t.doc.execCommand('enableObjectResizing', false, false);
437
+ t.doc.execCommand('defaultParagraphSeparator', false, 'p');
281
438
 
282
439
  t.buildEditor();
283
440
  t.buildBtnPane();
@@ -285,561 +442,715 @@
285
442
  t.fixedBtnPaneEvents();
286
443
 
287
444
  t.buildOverlay();
288
- },
289
-
290
- buildEditor: function(disable){
291
- var t = this;
292
- var pfx = t.o.prefix;
293
-
294
445
 
295
- if(disable === true){
296
- if(!t.$e.is('textarea')){
297
- var textarea = t.buildTextarea().val(t.$e.val());
298
- t.$e.hide().after(textarea);
446
+ setTimeout(function () {
447
+ if (t.disabled) {
448
+ t.toggleDisable(true);
299
449
  }
300
- return;
301
- }
450
+ t.$c.trigger('tbwinit');
451
+ });
452
+ },
302
453
 
454
+ addBtnDef: function (btnName, btnDef) {
455
+ this.btnsDef[btnName] = btnDef;
456
+ },
457
+
458
+ buildEditor: function () {
459
+ var t = this,
460
+ prefix = t.o.prefix,
461
+ html = '';
303
462
 
304
463
  t.$box = $('<div/>', {
305
- class: pfx + 'box ' + pfx + t.o.lang + ' trumbowyg'
464
+ class: prefix + 'box ' + prefix + 'editor-visible ' + prefix + t.o.lang + ' trumbowyg'
306
465
  });
307
466
 
308
- t.isTextarea = true;
309
- if(t.$e.is('textarea'))
310
- t.$editor = $('<div/>');
311
- else {
312
- t.$editor = t.$e;
313
- t.$e = t.buildTextarea().val(t.$e.val());
314
- t.isTextarea = false;
315
- }
467
+ // $ta = Textarea
468
+ // $ed = Editor
469
+ t.isTextarea = t.$ta.is('textarea');
470
+ if (t.isTextarea) {
471
+ html = t.$ta.val();
472
+ t.$ed = $('<div/>');
473
+ t.$box
474
+ .insertAfter(t.$ta)
475
+ .append(t.$ed, t.$ta);
476
+ } else {
477
+ t.$ed = t.$ta;
478
+ html = t.$ed.html();
316
479
 
317
- t.$e.hide()
318
- .addClass(pfx + 'textarea');
480
+ t.$ta = $('<textarea/>', {
481
+ name: t.$ta.attr('id'),
482
+ height: t.height
483
+ }).val(html);
319
484
 
320
- var html = '';
321
- if(t.isTextarea){
322
- html = t.$e.val();
323
- t.$box.insertAfter(t.$e)
324
- .append(t.$editor)
325
- .append(t.$e);
326
- } else {
327
- html = t.$editor.html();
328
- t.$box.insertAfter(t.$editor)
329
- .append(t.$e)
330
- .append(t.$editor);
485
+ t.$box
486
+ .insertAfter(t.$ed)
487
+ .append(t.$ta, t.$ed);
331
488
  t.syncCode();
332
489
  }
333
490
 
334
- t.$editor.addClass(pfx + 'editor')
335
- .attr('contenteditable', true)
336
- .attr('dir', t.o.dir)
337
- .html(html);
491
+ t.$ta
492
+ .addClass(prefix + 'textarea')
493
+ .attr('tabindex', -1)
494
+ ;
338
495
 
339
- if(t.o.resetCss)
340
- t.$editor.addClass(pfx + 'reset-css');
496
+ t.$ed
497
+ .addClass(prefix + 'editor')
498
+ .attr({
499
+ contenteditable: true,
500
+ dir: t.lang._dir || 'ltr'
501
+ })
502
+ .html(html)
503
+ ;
341
504
 
342
- if(!t.o.autogrow){
343
- $.each([t.$editor, t.$e], function(i, $el){
344
- $el.css({
345
- height: t.height,
346
- overflow: 'auto'
347
- });
348
- });
505
+ if (t.o.tabindex) {
506
+ t.$ed.attr('tabindex', t.o.tabindex);
349
507
  }
350
508
 
351
- if(t.o.semantic){
352
- t.$editor.html(
353
- t.$editor.html()
354
- .replace('<br>', '</p><p>')
355
- .replace('&nbsp;', '')
356
- );
357
- t.semanticCode();
509
+ if (t.$c.is('[placeholder]')) {
510
+ t.$ed.attr('placeholder', t.$c.attr('placeholder'));
358
511
  }
359
512
 
513
+ if (t.o.resetCss) {
514
+ t.$ed.addClass(prefix + 'reset-css');
515
+ }
360
516
 
517
+ if (!t.o.autogrow) {
518
+ t.$ta.add(t.$ed).css({
519
+ height: t.height
520
+ });
521
+ }
361
522
 
362
- t.$editor
363
- .on('dblclick', 'img', function(){
364
- var $img = $(this);
365
- t.openModalInsert(t.lang.insertImage, {
366
- url: {
367
- label: 'URL',
368
- value: $img.attr('src'),
369
- required: true
370
- },
371
- alt: {
372
- label: 'description',
373
- value: $img.attr('alt')
523
+ t.semanticCode();
524
+
525
+
526
+ t._ctrl = false;
527
+ t.$ed
528
+ .on('dblclick', 'img', t.o.imgDblClickHandler)
529
+ .on('keydown', function (e) {
530
+ t._composition = (e.which === 229);
531
+
532
+ if (e.ctrlKey) {
533
+ t._ctrl = true;
534
+ var k = t.keys[String.fromCharCode(e.which).toUpperCase()];
535
+
536
+ try {
537
+ t.execCmd(k.fn, k.param);
538
+ return false;
539
+ } catch (c) {
540
+ }
541
+ }
542
+ })
543
+ .on('keyup', function (e) {
544
+ if (e.which >= 37 && e.which <= 40) {
545
+ return;
546
+ }
547
+
548
+ if (e.ctrlKey && (e.which === 89 || e.which === 90)) {
549
+ t.$c.trigger('tbwchange');
550
+ } else if (!t._ctrl && e.which !== 17 && !t._composition) {
551
+ t.semanticCode(false, e.which === 13);
552
+ t.$c.trigger('tbwchange');
553
+ }
554
+
555
+ setTimeout(function () {
556
+ t._ctrl = false;
557
+ }, 200);
558
+ })
559
+ .on('mouseup keydown keyup', function () {
560
+ t.updateButtonPaneStatus();
561
+ })
562
+ .on('focus blur', function (e) {
563
+ t.$c.trigger('tbw' + e.type);
564
+ if (e.type === 'blur') {
565
+ $('.' + prefix + 'active-button', t.$btnPane).removeClass(prefix + 'active-button ' + prefix + 'active');
374
566
  }
375
- }, function(v){
376
- $img.attr('src', v.url);
377
- $img.attr('alt', v.alt);
567
+ })
568
+ .on('cut', function () {
569
+ t.$c.trigger('tbwchange');
570
+ })
571
+ .on('paste', function (e) {
572
+ if (t.o.removeformatPasted) {
573
+ e.preventDefault();
574
+
575
+ try {
576
+ // IE
577
+ var text = window.clipboardData.getData('Text');
578
+
579
+ try {
580
+ // <= IE10
581
+ t.doc.selection.createRange().pasteHTML(text);
582
+ } catch (c) {
583
+ // IE 11
584
+ t.doc.getSelection().getRangeAt(0).insertNode(t.doc.createTextNode(text));
585
+ }
586
+ } catch (d) {
587
+ // Not IE
588
+ t.execCmd('insertText', (e.originalEvent || e).clipboardData.getData('text/plain'));
589
+ }
590
+ }
591
+
592
+ // Call pasteHandlers
593
+ $.each(t.pasteHandlers, function (i, pasteHandler) {
594
+ pasteHandler(e);
595
+ });
596
+
597
+ setTimeout(function () {
598
+ if (t.o.semantic) {
599
+ t.semanticCode(false, true);
600
+ } else {
601
+ t.syncCode();
602
+ }
603
+ t.$c.trigger('tbwpaste', e);
604
+ }, 0);
378
605
  });
379
- return false;
380
- })
381
- .on('keyup', function(e){
382
- t.semanticCode(false, e.which === 13);
383
- })
384
- .on('blur', function(){
385
- t.syncCode();
606
+ t.$ta.on('keyup paste', function () {
607
+ t.$c.trigger('tbwchange');
386
608
  });
387
- },
388
-
389
609
 
390
- // Build the Textarea which contain HTML generated code
391
- buildTextarea: function(){
392
- return $('<textarea/>', {
393
- name: this.$e.attr('id'),
394
- height: this.height
610
+ $(t.doc).on('keydown', function (e) {
611
+ if (e.which === 27) {
612
+ t.closeModal();
613
+ return false;
614
+ }
395
615
  });
396
616
  },
397
617
 
398
618
 
399
- // Build button pane, use o.btns and o.btnsAdd options
400
- buildBtnPane: function(){
619
+ // Build button pane, use o.btns option
620
+ buildBtnPane: function () {
401
621
  var t = this,
402
- pfx = t.o.prefix;
622
+ prefix = t.o.prefix;
403
623
 
404
- if(t.o.btns === false)
405
- return;
406
-
407
- t.$btnPane = $('<ul/>', {
408
- class: pfx + 'button-pane'
624
+ var $btnPane = t.$btnPane = $('<div/>', {
625
+ class: prefix + 'button-pane'
409
626
  });
410
627
 
411
- $.each(t.o.btns.concat(t.o.btnsAdd), function(i, btn){
628
+ $.each(t.o.btns, function (i, btnGrps) {
412
629
  // Managment of group of buttons
413
630
  try {
414
- var b = btn.split('btnGrp-');
415
- if(b[1] !== undefined)
416
- btn = $.trumbowyg.btnsGrps[b[1]];
417
- } catch(e){}
631
+ var b = btnGrps.split('btnGrp-');
632
+ if (b[1] != null) {
633
+ btnGrps = t.o.btnsGrps[b[1]];
634
+ }
635
+ } catch (c) {
636
+ }
418
637
 
419
- if(!$.isArray(btn))
420
- btn = [btn];
638
+ if (!$.isArray(btnGrps)) {
639
+ btnGrps = [btnGrps];
640
+ }
421
641
 
422
- $.each(btn, function(i, b){
642
+ var $btnGroup = $('<div/>', {
643
+ class: prefix + 'button-group ' + ((btnGrps.indexOf('fullscreen') >= 0) ? prefix + 'right' : '')
644
+ });
645
+ $.each(btnGrps, function (i, btn) {
423
646
  try { // Prevent buildBtn error
424
- var $li = $('<li/>');
647
+ var $item;
425
648
 
426
- if(b === '|') // It's a separator
427
- $li.addClass(pfx + 'separator');
428
- else if(t.isSupportedBtn(b)) // It's a supported button
429
- $li.append(t.buildBtn(b));
649
+ if (t.isSupportedBtn(btn)) { // It's a supported button
650
+ $item = t.buildBtn(btn);
651
+ }
430
652
 
431
- t.$btnPane.append($li);
432
- } catch(e){}
653
+ $btnGroup.append($item);
654
+ } catch (c) {
655
+ }
433
656
  });
657
+ $btnPane.append($btnGroup);
434
658
  });
435
659
 
436
- // Build right li for fullscreen and close buttons
437
- var $liRight = $('<li/>', {
438
- class: pfx + 'not-disable ' + pfx + 'buttons-right'
439
- });
440
-
441
- // Add the fullscreen button
442
- if(t.o.fullscreenable)
443
- $liRight
444
- .append(t.buildRightBtn('fullscreen')
445
- .on('click', function(){
446
- var cssClass = pfx + 'fullscreen';
447
- t.$box.toggleClass(cssClass);
448
-
449
- if(t.$box.hasClass(cssClass)){
450
- $('body').css('overflow', 'hidden');
451
- $.each([t.$editor, t.$e], function(){
452
- $(this).css({
453
- height: 'calc(100% - 35px)',
454
- overflow: 'auto'
455
- });
456
- });
457
- t.$btnPane.css('width', '100%');
458
- } else {
459
- $('body').css('overflow', 'auto');
460
- t.$box.removeAttr('style');
461
- if(!t.o.autogrow)
462
- $([t.$editor, t.$e]).each(function(i, $el){
463
- $el.css('height', t.height);
464
- });
465
- }
466
- $(window).trigger('scroll');
467
- }));
468
-
469
- // Build and add close button
470
- if(t.o.closable)
471
- $liRight
472
- .append(
473
- t.buildRightBtn('close')
474
- .on('click', function(){
475
- if(t.$box.hasClass(pfx + 'fullscreen'))
476
- $('body').css('overflow', 'auto');
477
- t.destroy();
478
- })
479
- );
480
-
481
-
482
- // Add right li only if isn't empty
483
- if($liRight.not(':empty'))
484
- t.$btnPane.append($liRight);
485
-
486
- t.$box.prepend(t.$btnPane);
660
+ t.$box.prepend($btnPane);
487
661
  },
488
662
 
489
663
 
490
664
  // Build a button and his action
491
- buildBtn: function(n){ // n is name of the button
665
+ buildBtn: function (btnName) { // btnName is name of the button
492
666
  var t = this,
493
- pfx = t.o.prefix,
494
- btn = t.o.btnsDef[n],
495
- d = btn.dropdown,
496
- textDef = t.lang[n] || n,
667
+ prefix = t.o.prefix,
668
+ btn = t.btnsDef[btnName],
669
+ isDropdown = btn.dropdown,
670
+ textDef = t.lang[btnName] || btnName,
497
671
 
498
672
  $btn = $('<button/>', {
499
673
  type: 'button',
500
- class: pfx + n +'-button' + (btn.ico ? ' '+ pfx + btn.ico +'-button' : ''),
501
- text: btn.text || btn.title || textDef,
502
- title: btn.title || btn.text || textDef,
503
- mousedown: function(e){
504
- if(!d || t.$box.find('.'+n+'-'+pfx + 'dropdown').is(':hidden'))
505
- $('body').trigger('mousedown');
506
-
507
- if(t.$btnPane.hasClass(pfx + 'disable') && !$(this).hasClass(pfx + 'active') && !$(this).parent().hasClass(pfx + 'not-disable'))
674
+ class: prefix + btnName + '-button ' + (btn.class || ''),
675
+ html: t.hasSvg ? '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' : '',
676
+ title: (btn.title || btn.text || textDef) + ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : ''),
677
+ tabindex: -1,
678
+ mousedown: function () {
679
+ if (!isDropdown || $('.' + btnName + '-' + prefix + 'dropdown', t.$box).is(':hidden')) {
680
+ $('body', t.doc).trigger('mousedown');
681
+ }
682
+
683
+ if (t.$btnPane.hasClass(prefix + 'disable') && !$(this).hasClass(prefix + 'active') && !$(this).hasClass(prefix + 'not-disable')) {
508
684
  return false;
685
+ }
509
686
 
510
- t.execCmd((d ? 'dropdown' : false) || btn.func || n,
511
- btn.param || n);
687
+ t.execCmd((isDropdown ? 'dropdown' : false) || btn.fn || btnName, btn.param || btnName, btn.forceCss || false);
512
688
 
513
- e.stopPropagation();
514
- e.preventDefault();
689
+ return false;
515
690
  }
516
691
  });
517
692
 
518
-
519
-
520
- if(d){
521
- $btn.addClass(pfx + 'open-dropdown');
522
- var c = pfx + 'dropdown',
523
- dd = $('<div/>', { // the dropdown
524
- class: n + '-' + c + ' ' + c + ' ' + pfx + 'fixed-top'
693
+ if (isDropdown) {
694
+ $btn.addClass(prefix + 'open-dropdown');
695
+ var dropdownPrefix = prefix + 'dropdown',
696
+ $dropdown = $('<div/>', { // the dropdown
697
+ class: dropdownPrefix + '-' + btnName + ' ' + dropdownPrefix + ' ' + prefix + 'fixed-top',
698
+ 'data-dropdown': btnName
525
699
  });
526
- for(var i = 0, l = d.length; i < l; i++)
527
- if(t.o.btnsDef[d[i]] && t.isSupportedBtn(d[i]))
528
- dd.append(t.buildSubBtn(d[i]));
529
- t.$box.append(dd.hide());
700
+ $.each(isDropdown, function (i, def) {
701
+ if (t.btnsDef[def] && t.isSupportedBtn(def)) {
702
+ $dropdown.append(t.buildSubBtn(def));
703
+ }
704
+ });
705
+ t.$box.append($dropdown.hide());
706
+ } else if (btn.key) {
707
+ t.keys[btn.key] = {
708
+ fn: btn.fn || btnName,
709
+ param: btn.param || btnName
710
+ };
711
+ }
712
+
713
+ if (!isDropdown) {
714
+ t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
530
715
  }
531
716
 
532
717
  return $btn;
533
718
  },
534
719
  // Build a button for dropdown menu
535
720
  // @param n : name of the subbutton
536
- buildSubBtn: function(n){
721
+ buildSubBtn: function (btnName) {
537
722
  var t = this,
538
- btnDef = t.o.btnsDef[n];
723
+ prefix = t.o.prefix,
724
+ btn = t.btnsDef[btnName];
725
+
726
+ if (btn.key) {
727
+ t.keys[btn.key] = {
728
+ fn: btn.fn || btnName,
729
+ param: btn.param || btnName
730
+ };
731
+ }
732
+
733
+ t.tagToButton[(btn.tag || btnName).toLowerCase()] = btnName;
734
+
539
735
  return $('<button/>', {
540
736
  type: 'button',
541
- text: btnDef.text || btnDef.title || t.lang[n] || n,
542
- mousedown: function(e){
543
- $('body').trigger('mousedown');
737
+ class: prefix + btnName + '-dropdown-button' + (btn.ico ? ' ' + prefix + btn.ico + '-button' : ''),
738
+ html: t.hasSvg ? '<svg><use xlink:href="' + t.svgPath + '#' + prefix + (btn.ico || btnName).replace(/([A-Z]+)/g, '-$1').toLowerCase() + '"/></svg>' + (btn.text || btn.title || t.lang[btnName] || btnName) : '',
739
+ title: ((btn.key) ? ' (Ctrl + ' + btn.key + ')' : null),
740
+ style: btn.style || null,
741
+ mousedown: function () {
742
+ $('body', t.doc).trigger('mousedown');
544
743
 
545
- t.execCmd(btnDef.func || n,
546
- btnDef.param || n);
744
+ t.execCmd(btn.fn || btnName, btn.param || btnName, btn.forceCss || false);
547
745
 
548
- e.stopPropagation();
549
- e.preventDefault();
550
746
  return false;
551
747
  }
552
748
  });
553
749
  },
554
- // Build a button for right li
555
- // @param n : name of the right button
556
- buildRightBtn: function(n){
557
- return $('<button/>', {
558
- type: 'button',
559
- class: this.o.prefix + n + '-button',
560
- title: this.lang[n],
561
- text: this.lang[n]
562
- });
563
- },
564
750
  // Check if button is supported
565
- isSupportedBtn: function(btn){
566
- return typeof this.o.btnsDef[btn].isSupported !== 'function' || this.o.btnsDef[btn].isSupported();
751
+ isSupportedBtn: function (b) {
752
+ try {
753
+ return this.btnsDef[b].isSupported();
754
+ } catch (c) {
755
+ }
756
+ return true;
567
757
  },
568
758
 
569
759
  // Build overlay for modal box
570
- buildOverlay: function(){
760
+ buildOverlay: function () {
571
761
  var t = this;
572
762
  t.$overlay = $('<div/>', {
573
763
  class: t.o.prefix + 'overlay'
574
764
  }).css({
575
765
  top: t.$btnPane.outerHeight(),
576
- height: (parseInt(t.$editor.outerHeight()) + 1) + 'px'
766
+ height: (t.$ed.outerHeight() + 1) + 'px'
577
767
  }).appendTo(t.$box);
578
768
  return t.$overlay;
579
769
  },
580
- showOverlay: function(){
770
+ showOverlay: function () {
581
771
  var t = this;
582
772
  $(window).trigger('scroll');
583
- t.$overlay.fadeIn(t.o.duration);
773
+ t.$overlay.fadeIn(200);
584
774
  t.$box.addClass(t.o.prefix + 'box-blur');
585
775
  },
586
- hideOverlay: function(){
776
+ hideOverlay: function () {
587
777
  var t = this;
588
- t.$overlay.fadeOut(t.o.duration/4);
778
+ t.$overlay.fadeOut(50);
589
779
  t.$box.removeClass(t.o.prefix + 'box-blur');
590
780
  },
591
781
 
592
782
  // Management of fixed button pane
593
- fixedBtnPaneEvents: function(){
783
+ fixedBtnPaneEvents: function () {
594
784
  var t = this,
595
- ffw = t.o.fixedFullWidth;
596
- if(!t.o.fixedBtnPane)
785
+ fixedFullWidth = t.o.fixedFullWidth,
786
+ $box = t.$box;
787
+
788
+ if (!t.o.fixedBtnPane) {
597
789
  return;
790
+ }
598
791
 
599
792
  t.isFixed = false;
600
793
 
601
794
  $(window)
602
- .on('scroll resize', function(){
603
- if(!t.$box)
604
- return;
605
-
606
- t.syncCode();
607
-
608
- var s = $(window).scrollTop(), // s is top scroll
609
- o = t.$box.offset().top + 1, // o is offset
610
- toFixed = (s - o > 0) && ((s - o - parseInt(t.height)) < 0),
611
- bp = t.$btnPane,
612
- mt = bp.css('height'),
613
- oh = bp.outerHeight();
795
+ .on('scroll resize', function () {
796
+ if (!$box) {
797
+ return;
798
+ }
614
799
 
615
- if(toFixed){
616
- if(!t.isFixed){
617
- t.isFixed = true;
800
+ t.syncCode();
801
+
802
+ var scrollTop = $(window).scrollTop(),
803
+ offset = $box.offset().top + 1,
804
+ bp = t.$btnPane,
805
+ oh = bp.outerHeight() - 2;
806
+
807
+ if ((scrollTop - offset > 0) && ((scrollTop - offset - t.height) < 0)) {
808
+ if (!t.isFixed) {
809
+ t.isFixed = true;
810
+ bp.css({
811
+ position: 'fixed',
812
+ top: 0,
813
+ left: fixedFullWidth ? '0' : 'auto',
814
+ zIndex: 7
815
+ });
816
+ $([t.$ta, t.$ed]).css({marginTop: bp.height()});
817
+ }
618
818
  bp.css({
619
- position: 'fixed',
620
- top: 0,
621
- left: ffw ? '0' : 'auto',
622
- zIndex: 7
819
+ width: fixedFullWidth ? '100%' : (($box.width() - 1) + 'px')
623
820
  });
624
- t.$editor.css({ marginTop: mt });
625
- t.$e.css({ marginTop: mt });
626
- }
627
- bp.css({
628
- width: ffw ? '100%' : ((parseInt(t.$box.css('width'))-1) + 'px')
629
- });
630
821
 
631
- $('.' + t.o.prefix + 'fixed-top', t.$box).css({
632
- position: ffw ? 'fixed' : 'absolute',
633
- top: ffw ? oh : parseInt(oh) + (s - o) + 'px',
634
- zIndex: 15
635
- });
636
- } else if(t.isFixed) {
637
- t.isFixed = false;
638
- bp.removeAttr('style');
639
- t.$editor.css({ marginTop: 0 });
640
- t.$e.css({ marginTop: 0 });
641
- $('.' + t.o.prefix + 'fixed-top', t.$box).css({
642
- position: 'absolute',
643
- top: oh
644
- });
645
- }
646
- });
822
+ $('.' + t.o.prefix + 'fixed-top', $box).css({
823
+ position: fixedFullWidth ? 'fixed' : 'absolute',
824
+ top: fixedFullWidth ? oh : oh + (scrollTop - offset) + 'px',
825
+ zIndex: 15
826
+ });
827
+ } else if (t.isFixed) {
828
+ t.isFixed = false;
829
+ bp.removeAttr('style');
830
+ $([t.$ta, t.$ed]).css({marginTop: 0});
831
+ $('.' + t.o.prefix + 'fixed-top', $box).css({
832
+ position: 'absolute',
833
+ top: oh
834
+ });
835
+ }
836
+ });
647
837
  },
648
838
 
839
+ // Disable editor
840
+ toggleDisable: function (disable) {
841
+ var t = this,
842
+ prefix = t.o.prefix;
649
843
 
844
+ t.disabled = disable;
845
+
846
+ if (disable) {
847
+ t.$ta.attr('disabled', true);
848
+ } else {
849
+ t.$ta.removeAttr('disabled');
850
+ }
851
+ t.$box.toggleClass(prefix + 'disabled', disable);
852
+ t.$ed.attr('contenteditable', !disable);
853
+ },
650
854
 
651
855
  // Destroy the editor
652
- destroy: function(){
856
+ destroy: function () {
653
857
  var t = this,
654
- pfx = t.o.prefix,
655
- h = t.height,
656
- html = t.html();
858
+ prefix = t.o.prefix,
859
+ height = t.height;
657
860
 
658
- if(t.isTextarea)
861
+ if (t.isTextarea) {
659
862
  t.$box.after(
660
- t.$e.css({height: h})
661
- .val(html)
662
- .removeClass(pfx + 'textarea')
863
+ t.$ta
864
+ .css({height: height})
865
+ .val(t.html())
866
+ .removeClass(prefix + 'textarea')
663
867
  .show()
664
868
  );
665
- else
869
+ } else {
666
870
  t.$box.after(
667
- t.$editor
668
- .css({height: h})
669
- .removeClass(pfx + 'editor')
670
- .attr('contenteditable', false)
671
- .html(html)
871
+ t.$ed
872
+ .css({height: height})
873
+ .removeClass(prefix + 'editor')
874
+ .removeAttr('contenteditable')
875
+ .html(t.html())
672
876
  .show()
673
877
  );
878
+ }
879
+
880
+ t.$ed.off('dblclick', 'img');
881
+
882
+ t.destroyPlugins();
674
883
 
675
884
  t.$box.remove();
676
- t.$creator.removeData('trumbowyg');
885
+ t.$c.removeData('trumbowyg');
886
+ $('body').removeClass(prefix + 'body-fullscreen');
677
887
  },
678
888
 
679
889
 
680
-
681
890
  // Empty the editor
682
- empty: function(){
683
- this.$e.val('');
891
+ empty: function () {
892
+ this.$ta.val('');
684
893
  this.syncCode(true);
685
894
  },
686
895
 
687
896
 
688
-
689
897
  // Function call when click on viewHTML button
690
- toggle: function(){
898
+ toggle: function () {
691
899
  var t = this,
692
- pfx = t.o.prefix;
900
+ prefix = t.o.prefix;
693
901
  t.semanticCode(false, true);
694
- t.$editor.toggle();
695
- t.$e.toggle();
696
- t.$btnPane.toggleClass(pfx + 'disable');
697
- t.$btnPane.find('.'+pfx + 'viewHTML-button').toggleClass(pfx + 'active');
902
+ setTimeout(function () {
903
+ t.doc.activeElement.blur();
904
+ t.$box.toggleClass(prefix + 'editor-hidden ' + prefix + 'editor-visible');
905
+ t.$btnPane.toggleClass(prefix + 'disable');
906
+ $('.' + prefix + 'viewHTML-button', t.$btnPane).toggleClass(prefix + 'active');
907
+ if (t.$box.hasClass(prefix + 'editor-visible')) {
908
+ t.$ta.attr('tabindex', -1);
909
+ } else {
910
+ t.$ta.removeAttr('tabindex');
911
+ }
912
+ }, 0);
698
913
  },
699
914
 
700
915
  // Open dropdown when click on a button which open that
701
- dropdown: function(name){
916
+ dropdown: function (name) {
702
917
  var t = this,
703
- pfx = t.o.prefix,
704
- $dropdown = t.$box.find('.'+name+'-'+pfx + 'dropdown'),
705
- $btn = t.$btnPane.find('.'+pfx+name+'-button');
918
+ d = t.doc,
919
+ prefix = t.o.prefix,
920
+ $dropdown = $('[data-dropdown=' + name + ']', t.$box),
921
+ $btn = $('.' + prefix + name + '-button', t.$btnPane),
922
+ show = $dropdown.is(':hidden');
923
+
924
+ $('body', d).trigger('mousedown');
706
925
 
707
- if($dropdown.is(':hidden')){
926
+ if (show) {
708
927
  var o = $btn.offset().left;
709
- $btn.addClass(pfx + 'active');
928
+ $btn.addClass(prefix + 'active');
710
929
 
711
930
  $dropdown.css({
712
931
  position: 'absolute',
713
- top: t.$btnPane.outerHeight(),
714
- left: (t.o.fixedFullWidth && t.isFixed) ? o+'px' : (o - t.$btnPane.offset().left)+'px'
932
+ top: $btn.offset().top - t.$btnPane.offset().top + $btn.outerHeight(),
933
+ left: (t.o.fixedFullWidth && t.isFixed) ? o + 'px' : (o - t.$btnPane.offset().left) + 'px'
715
934
  }).show();
716
935
 
717
936
  $(window).trigger('scroll');
718
937
 
719
- $('body').on('mousedown', function(){
720
- $('.' + pfx + 'dropdown').hide();
721
- $('.' + pfx + 'active').removeClass(pfx + 'active');
722
- $('body').off('mousedown');
938
+ $('body', d).on('mousedown', function () {
939
+ $('.' + prefix + 'dropdown', d).hide();
940
+ $('.' + prefix + 'active', d).removeClass(prefix + 'active');
941
+ $('body', d).off('mousedown');
723
942
  });
724
- } else
725
- $('body').trigger('mousedown');
943
+ }
726
944
  },
727
945
 
728
946
 
729
-
730
-
731
947
  // HTML Code management
732
- html: function(html){
948
+ html: function (html) {
733
949
  var t = this;
734
- if(html){
735
- t.$e.val(html);
950
+ if (html != null) {
951
+ t.$ta.val(html);
736
952
  t.syncCode(true);
737
953
  return t;
738
- } else
739
- return t.$e.val();
954
+ }
955
+ return t.$ta.val();
740
956
  },
741
- syncCode: function(force){
957
+ syncTextarea: function () {
742
958
  var t = this;
743
- if(!force && t.$editor.is(':visible'))
744
- t.$e.val(t.$editor.html());
745
- else
746
- t.$editor.html(t.$e.val());
747
-
748
- if(t.o.autogrow){
749
- t.height = t.$editor.css('height');
750
- t.$e.css({ height: t.height });
959
+ t.$ta.val(t.$ed.text().trim().length > 0 || t.$ed.find('hr,img,embed,input').length > 0 ? t.$ed.html() : '');
960
+ },
961
+ syncCode: function (force) {
962
+ var t = this;
963
+ if (!force && t.$ed.is(':visible')) {
964
+ t.syncTextarea();
965
+ } else {
966
+ t.$ed.html(t.$ta.val());
967
+ }
968
+
969
+ if (t.o.autogrow) {
970
+ t.height = t.$ed.height();
971
+ if (t.height !== t.$ta.css('height')) {
972
+ t.$ta.css({height: t.height});
973
+ t.$c.trigger('tbwresize');
974
+ }
751
975
  }
752
976
  },
753
977
 
754
978
  // Analyse and update to semantic code
755
979
  // @param force : force to sync code from textarea
756
980
  // @param full : wrap text nodes in <p>
757
- semanticCode: function(force, full){
981
+ semanticCode: function (force, full) {
758
982
  var t = this;
983
+ t.saveRange();
759
984
  t.syncCode(force);
760
985
 
761
- if(t.o.semantic){
986
+ $(t.o.tagsToRemove.join(','), t.$ed).remove();
987
+
988
+ if (t.o.semantic) {
762
989
  t.semanticTag('b', 'strong');
763
990
  t.semanticTag('i', 'em');
764
991
  t.semanticTag('strike', 'del');
765
992
 
766
- if(full){
767
- // Wrap text nodes in p
768
- t.$editor.contents()
769
- .filter(function(){
770
- // Only non-empty text nodes
771
- return this.nodeType === 3 && $.trim(this.nodeValue).length > 0;
772
- }).wrap('<p></p>').end()
993
+ if (full) {
994
+ var inlineElementsSelector = t.o.inlineElementsSelector,
995
+ blockElementsSelector = ':not(' + inlineElementsSelector + ')';
996
+
997
+ // Wrap text nodes in span for easier processing
998
+ t.$ed.contents().filter(function () {
999
+ return this.nodeType === 3 && this.nodeValue.trim().length > 0;
1000
+ }).wrap('<span data-tbw/>');
1001
+
1002
+ // Wrap groups of inline elements in paragraphs (recursive)
1003
+ var wrapInlinesInParagraphsFrom = function ($from) {
1004
+ if ($from.length !== 0) {
1005
+ var $finalParagraph = $from.nextUntil(blockElementsSelector).andSelf().wrapAll('<p/>').parent(),
1006
+ $nextElement = $finalParagraph.nextAll(inlineElementsSelector).first();
1007
+ $finalParagraph.next('br').remove();
1008
+ wrapInlinesInParagraphsFrom($nextElement);
1009
+ }
1010
+ };
1011
+ wrapInlinesInParagraphsFrom(t.$ed.children(inlineElementsSelector).first());
773
1012
 
774
- // Remove all br
775
- .filter('br').remove();
1013
+ t.semanticTag('div', 'p', true);
776
1014
 
777
- t.saveSelection();
778
- t.semanticTag('div', 'p');
779
- t.restoreSelection();
1015
+ // Unwrap paragraphs content, containing nothing usefull
1016
+ t.$ed.find('p').filter(function () {
1017
+ // Don't remove currently being edited element
1018
+ if (t.range && this === t.range.startContainer) {
1019
+ return false;
1020
+ }
1021
+ return $(this).text().trim().length === 0 && $(this).children().not('br,span').length === 0;
1022
+ }).contents().unwrap();
1023
+
1024
+ // Get rid of temporial span's
1025
+ $('[data-tbw]', t.$ed).contents().unwrap();
1026
+
1027
+ // Remove empty <p>
1028
+ t.$ed.find('p:empty').remove();
780
1029
  }
781
1030
 
782
- t.$e.val(t.$editor.html());
1031
+ t.restoreRange();
1032
+
1033
+ t.syncTextarea();
783
1034
  }
784
1035
  },
785
- semanticTag: function(oldTag, newTag){
786
- $(oldTag, this.$editor).each(function(){
787
- $(this).replaceWith(function(){
788
- return '<'+newTag+'>' + $(this).html() + '</'+newTag+'>';
789
- });
1036
+
1037
+ semanticTag: function (oldTag, newTag, copyAttributes) {
1038
+ $(oldTag, this.$ed).each(function () {
1039
+ var $oldTag = $(this);
1040
+ $oldTag.wrap('<' + newTag + '/>');
1041
+ if (copyAttributes) {
1042
+ $.each($oldTag.prop('attributes'), function () {
1043
+ $oldTag.parent().attr(this.name, this.value);
1044
+ });
1045
+ }
1046
+ $oldTag.contents().unwrap();
790
1047
  });
791
1048
  },
792
1049
 
793
-
794
1050
  // Function call when user click on "Insert Link"
795
- createLink: function(){
796
- var t = this;
797
- t.saveSelection();
1051
+ createLink: function () {
1052
+ var t = this,
1053
+ documentSelection = t.doc.getSelection(),
1054
+ node = documentSelection.focusNode,
1055
+ url,
1056
+ title,
1057
+ target;
1058
+
1059
+ while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
1060
+ node = node.parentNode;
1061
+ }
1062
+
1063
+ if (node && node.nodeName === 'A') {
1064
+ var $a = $(node);
1065
+ url = $a.attr('href');
1066
+ title = $a.attr('title');
1067
+ target = $a.attr('target');
1068
+ var range = t.doc.createRange();
1069
+ range.selectNode(node);
1070
+ documentSelection.addRange(range);
1071
+ }
1072
+
1073
+ t.saveRange();
1074
+
798
1075
  t.openModalInsert(t.lang.createLink, {
799
1076
  url: {
800
1077
  label: 'URL',
801
- value: 'http://',
802
- required: true
1078
+ required: true,
1079
+ value: url
803
1080
  },
804
1081
  title: {
805
1082
  label: t.lang.title,
806
- value: t.selection
1083
+ value: title
807
1084
  },
808
1085
  text: {
809
1086
  label: t.lang.text,
810
- value: t.selection
1087
+ value: t.getRangeText()
1088
+ },
1089
+ target: {
1090
+ label: t.lang.target,
1091
+ value: target
811
1092
  }
812
- }, function(v){ // v is value
813
- t.execCmd('createLink', v.url);
814
- var l = $('a[href="'+v.url+'"]:not([title])', t.$box);
815
- if(v.text.length > 0)
816
- l.text(v.text);
817
-
818
- if(v.title.length > 0)
819
- l.attr('title', v.title);
820
-
1093
+ }, function (v) { // v is value
1094
+ var link = $(['<a href="', v.url, '">', v.text, '</a>'].join(''));
1095
+ if (v.title.length > 0) {
1096
+ link.attr('title', v.title);
1097
+ }
1098
+ if (v.target.length > 0) {
1099
+ link.attr('target', v.target);
1100
+ }
1101
+ t.range.deleteContents();
1102
+ t.range.insertNode(link[0]);
821
1103
  return true;
822
1104
  });
823
1105
  },
824
- insertImage: function(){
1106
+ unlink: function () {
1107
+ var t = this,
1108
+ documentSelection = t.doc.getSelection(),
1109
+ node = documentSelection.focusNode;
1110
+
1111
+ if (documentSelection.isCollapsed) {
1112
+ while (['A', 'DIV'].indexOf(node.nodeName) < 0) {
1113
+ node = node.parentNode;
1114
+ }
1115
+
1116
+ if (node && node.nodeName === 'A') {
1117
+ var range = t.doc.createRange();
1118
+ range.selectNode(node);
1119
+ documentSelection.addRange(range);
1120
+ }
1121
+ }
1122
+ t.execCmd('unlink', undefined, undefined, true);
1123
+ },
1124
+ insertImage: function () {
825
1125
  var t = this;
826
- t.saveSelection();
1126
+ t.saveRange();
827
1127
  t.openModalInsert(t.lang.insertImage, {
828
1128
  url: {
829
1129
  label: 'URL',
830
- value: 'http://',
831
1130
  required: true
832
1131
  },
833
1132
  alt: {
834
1133
  label: t.lang.description,
835
- value: t.selection
1134
+ value: t.getRangeText()
836
1135
  }
837
- }, function(v){ // v are values
1136
+ }, function (v) { // v are values
838
1137
  t.execCmd('insertImage', v.url);
839
- $('img[src="'+v.url+'"]:not([alt])', t.$box).attr('alt', v.alt);
1138
+ $('img[src="' + v.url + '"]:not([alt])', t.$box).attr('alt', v.alt);
840
1139
  return true;
841
1140
  });
842
1141
  },
1142
+ fullscreen: function () {
1143
+ var t = this,
1144
+ prefix = t.o.prefix,
1145
+ fullscreenCssClass = prefix + 'fullscreen',
1146
+ isFullscreen;
1147
+
1148
+ t.$box.toggleClass(fullscreenCssClass);
1149
+ isFullscreen = t.$box.hasClass(fullscreenCssClass);
1150
+ $('body').toggleClass(prefix + 'body-fullscreen', isFullscreen);
1151
+ $(window).trigger('scroll');
1152
+ t.$c.trigger('tbw' + (isFullscreen ? 'open' : 'close') + 'fullscreen');
1153
+ },
843
1154
 
844
1155
 
845
1156
  /*
@@ -847,56 +1158,69 @@
847
1158
  * else try to call anonymous function
848
1159
  * and finaly native execCommand
849
1160
  */
850
- execCmd: function(cmd, param){
1161
+ execCmd: function (cmd, param, forceCss, skipTrumbowyg) {
851
1162
  var t = this;
852
- if(cmd != 'dropdown')
853
- t.$editor.focus();
1163
+ skipTrumbowyg = !!skipTrumbowyg || '';
1164
+
1165
+ if (cmd !== 'dropdown') {
1166
+ t.$ed.focus();
1167
+ }
1168
+
1169
+ t.doc.execCommand('styleWithCSS', false, forceCss || false);
854
1170
 
855
1171
  try {
856
- t[cmd](param);
857
- } catch(e){
1172
+ t[cmd + skipTrumbowyg](param);
1173
+ } catch (c) {
858
1174
  try {
859
- cmd(param, t);
860
- } catch(e2){
861
- t.$editor.focus();
862
- if(cmd == 'insertHorizontalRule')
863
- param = null;
864
- else if(cmd == 'formatBlock' && (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0))
1175
+ cmd(param);
1176
+ } catch (e2) {
1177
+ if (cmd === 'insertHorizontalRule') {
1178
+ param = undefined;
1179
+ } else if (cmd === 'formatBlock' && (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') !== -1)) {
865
1180
  param = '<' + param + '>';
1181
+ }
866
1182
 
867
- document.execCommand(cmd, false, param);
1183
+ t.doc.execCommand(cmd, false, param);
1184
+
1185
+ t.syncCode();
1186
+ t.semanticCode(false, true);
1187
+ }
1188
+
1189
+ if (cmd !== 'dropdown') {
1190
+ t.updateButtonPaneStatus();
1191
+ t.$c.trigger('tbwchange');
868
1192
  }
869
1193
  }
870
- t.syncCode();
871
1194
  },
872
1195
 
873
1196
 
874
1197
  // Open a modal box
875
- openModal: function(title, content){
1198
+ openModal: function (title, content) {
876
1199
  var t = this,
877
- pfx = t.o.prefix;
1200
+ prefix = t.o.prefix;
878
1201
 
879
1202
  // No open a modal box when exist other modal box
880
- if($('.' + pfx + 'modal-box', t.$box).size() > 0)
1203
+ if ($('.' + prefix + 'modal-box', t.$box).length > 0) {
881
1204
  return false;
1205
+ }
882
1206
 
883
- t.saveSelection();
1207
+ t.saveRange();
884
1208
  t.showOverlay();
885
1209
 
886
1210
  // Disable all btnPane btns
887
- t.$btnPane.addClass(pfx + 'disable');
1211
+ t.$btnPane.addClass(prefix + 'disable');
888
1212
 
889
1213
  // Build out of ModalBox, it's the mask for animations
890
1214
  var $modal = $('<div/>', {
891
- class: pfx + 'modal ' + pfx + 'fixed-top'
1215
+ class: prefix + 'modal ' + prefix + 'fixed-top'
892
1216
  }).css({
893
- top: (parseInt(t.$btnPane.css('height')) + 1) + 'px'
1217
+ top: t.$btnPane.height()
894
1218
  }).appendTo(t.$box);
895
1219
 
896
- // Click on overflay close modal by cancelling them
897
- t.$overlay.one('click', function(e){
898
- e.preventDefault();
899
- $modal.trigger(pfx + 'cancel');
1220
+ // Click on overlay close modal by cancelling them
1221
+ t.$overlay.one('click', function () {
1222
+ $modal.trigger('tbwcancel');
1223
+ return false;
900
1224
  });
901
1225
 
902
1226
  // Build the form
@@ -904,41 +1228,43 @@
904
1228
  action: '',
905
1229
  html: content
906
1230
  })
907
- .on('submit', function(e){
908
- e.preventDefault();
909
- $modal.trigger(pfx + 'confirm');
910
- })
911
- .on('reset', function(e){
912
- e.preventDefault();
913
- $modal.trigger(pfx + 'cancel');
914
- });
1231
+ .on('submit', function () {
1232
+ $modal.trigger('tbwconfirm');
1233
+ return false;
1234
+ })
1235
+ .on('reset', function () {
1236
+ $modal.trigger('tbwcancel');
1237
+ return false;
1238
+ });
915
1239
 
916
1240
 
917
1241
  // Build ModalBox and animate to show them
918
1242
  var $box = $('<div/>', {
919
- class: pfx + 'modal-box',
1243
+ class: prefix + 'modal-box',
920
1244
  html: $form
921
1245
  })
922
- .css({
923
- top: '-' + parseInt(t.$btnPane.outerHeight()) + 'px',
924
- opacity: 0
925
- })
926
- .appendTo($modal)
927
- .animate({
928
- top: 0,
929
- opacity: 1
930
- }, t.o.duration / 2);
1246
+ .css({
1247
+ top: '-' + t.$btnPane.outerHeight() + 'px',
1248
+ opacity: 0
1249
+ })
1250
+ .appendTo($modal)
1251
+ .animate({
1252
+ top: 0,
1253
+ opacity: 1
1254
+ }, 100);
931
1255
 
932
1256
 
933
1257
  // Append title
934
1258
  $('<span/>', {
935
1259
  text: title,
936
- class: pfx + 'modal-title'
1260
+ class: prefix + 'modal-title'
937
1261
  }).prependTo($box);
938
1262
 
1263
+ $modal.height($box.outerHeight() + 10);
1264
+
939
1265
 
940
1266
  // Focus in modal box
941
- $box.find('input:first').focus();
1267
+ $('input:first', $box).focus();
942
1268
 
943
1269
 
944
1270
  // Append Confirm and Cancel buttons
@@ -951,146 +1277,246 @@
951
1277
  return $modal;
952
1278
  },
953
1279
  // @param n is name of modal
954
- buildModalBtn: function(n, modal){
1280
+ buildModalBtn: function (n, $modal) {
955
1281
  var t = this,
956
- pfx = t.o.prefix;
1282
+ prefix = t.o.prefix;
957
1283
 
958
1284
  return $('<button/>', {
959
- class: pfx + 'modal-button ' + pfx + 'modal-' + n,
1285
+ class: prefix + 'modal-button ' + prefix + 'modal-' + n,
960
1286
  type: n,
961
1287
  text: t.lang[n] || n
962
- }).appendTo(modal.find('form'));
1288
+ }).appendTo($('form', $modal));
963
1289
  },
964
1290
  // close current modal box
965
- closeModal: function(){
1291
+ closeModal: function () {
966
1292
  var t = this,
967
- pfx = t.o.prefix;
1293
+ prefix = t.o.prefix;
968
1294
 
969
- t.$btnPane.removeClass(pfx + 'disable');
1295
+ t.$btnPane.removeClass(prefix + 'disable');
970
1296
  t.$overlay.off();
971
1297
 
972
- var $modalBox = $('.' + pfx + 'modal-box', t.$box);
1298
+ // Find the modal box
1299
+ var $mb = $('.' + prefix + 'modal-box', t.$box);
973
1300
 
974
- $modalBox.animate({
975
- top: '-' + $modalBox.css('height')
976
- }, t.o.duration/2, function(){
977
- $(this).parent().remove();
1301
+ $mb.animate({
1302
+ top: '-' + $mb.height()
1303
+ }, 100, function () {
1304
+ $mb.parent().remove();
978
1305
  t.hideOverlay();
979
1306
  });
1307
+
1308
+ t.restoreRange();
980
1309
  },
981
1310
  // Preformated build and management modal
982
- openModalInsert: function(title, fields, cmd){
1311
+ openModalInsert: function (title, fields, cmd) {
983
1312
  var t = this,
984
- pfx = t.o.prefix,
1313
+ prefix = t.o.prefix,
985
1314
  lg = t.lang,
986
- html = '';
1315
+ html = '',
1316
+ CONFIRM_EVENT = 'tbwconfirm';
987
1317
 
988
- for(var f in fields){
989
- var fd = fields[f], // field definition
990
- label = (fd.label === undefined) ? (lg[f] ? lg[f] : f) : (lg[fd.label] ? lg[fd.label] : fd.label);
1318
+ $.each(fields, function (fieldName, field) {
1319
+ var l = field.label,
1320
+ n = field.name || fieldName;
991
1321
 
992
- if(fd.name === undefined)
993
- fd.name = f;
994
-
995
- if(!fd.pattern && f === 'url'){
996
- fd.pattern = /^(http|https):\/\/([\w~#!:.?+=&%@!\-\/]+)$/;
997
- fd.patternError = lg.invalidUrl;
998
- }
999
-
1000
- html += '<label><input type="'+(fd.type || 'text')+'" name="'+fd.name+'" value="'+(fd.value || '')+'"><span class="'+pfx+'input-infos"><span>'+label+'</span></span></label>';
1001
- }
1322
+ html += '<label><input type="' + (field.type || 'text') + '" name="' + n + '" value="' + (field.value || '').replace(/"/g, '&quot;') + '"><span class="' + prefix + 'input-infos"><span>' +
1323
+ ((!l) ? (lg[fieldName] ? lg[fieldName] : fieldName) : (lg[l] ? lg[l] : l)) +
1324
+ '</span></span></label>';
1325
+ });
1002
1326
 
1003
1327
  return t.openModal(title, html)
1004
- .on(pfx + 'confirm', function(){
1005
- var $form = $(this).find('form'),
1006
- valid = true,
1007
- v = {}; // values
1008
-
1009
- for(var f in fields){
1010
- var $field = $('input[name="'+f+'"]', $form);
1011
-
1012
- v[f] = $.trim($field.val());
1013
-
1014
- // Validate value
1015
- if(fields[f].required && v[f] === ''){
1016
- valid = false;
1017
- t.addErrorOnModalField($field, t.lang.required);
1018
- } else if(fields[f].pattern && !fields[f].pattern.test(v[f])){
1019
- valid = false;
1020
- t.addErrorOnModalField($field, fields[f].patternError);
1021
- }
1022
- }
1328
+ .on(CONFIRM_EVENT, function () {
1329
+ var $form = $('form', $(this)),
1330
+ valid = true,
1331
+ values = {};
1332
+
1333
+ $.each(fields, function (fieldName, field) {
1334
+ var $field = $('input[name="' + fieldName + '"]', $form);
1335
+
1336
+ values[fieldName] = $.trim($field.val());
1337
+
1338
+ // Validate value
1339
+ if (field.required && values[fieldName] === '') {
1340
+ valid = false;
1341
+ t.addErrorOnModalField($field, t.lang.required);
1342
+ } else if (field.pattern && !field.pattern.test(values[fieldName])) {
1343
+ valid = false;
1344
+ t.addErrorOnModalField($field, field.patternError);
1345
+ }
1346
+ });
1023
1347
 
1024
- if(valid){
1025
- t.restoreSelection();
1348
+ if (valid) {
1349
+ t.restoreRange();
1026
1350
 
1027
- if(cmd(v, fields)){
1028
- t.syncCode();
1029
- t.closeModal();
1030
- $(this).off(pfx + 'confirm');
1351
+ if (cmd(values, fields)) {
1352
+ t.syncCode();
1353
+ t.$c.trigger('tbwchange');
1354
+ t.closeModal();
1355
+ $(this).off(CONFIRM_EVENT);
1356
+ }
1031
1357
  }
1032
- }
1033
- })
1034
- .one(pfx + 'cancel', function(){
1035
- $(this).off(pfx + 'confirm');
1036
- t.closeModal();
1037
- t.restoreSelection();
1038
- });
1358
+ })
1359
+ .one('tbwcancel', function () {
1360
+ $(this).off(CONFIRM_EVENT);
1361
+ t.closeModal();
1362
+ });
1039
1363
  },
1040
- addErrorOnModalField: function($field, err){
1041
- var pfx = this.o.prefix,
1364
+ addErrorOnModalField: function ($field, err) {
1365
+ var prefix = this.o.prefix,
1042
1366
  $label = $field.parent();
1043
1367
 
1044
- $field.on('change keyup', function(){
1045
- $label.removeClass(pfx + 'input-error');
1046
- });
1368
+ $field
1369
+ .on('change keyup', function () {
1370
+ $label.removeClass(prefix + 'input-error');
1371
+ });
1372
+
1047
1373
  $label
1048
- .addClass(pfx + 'input-error')
1049
- .find('input+span').append(
1050
- $('<span/>', {
1051
- class: pfx +'msg-error',
1052
- text: err
1053
- })
1054
- );
1374
+ .addClass(prefix + 'input-error')
1375
+ .find('input+span')
1376
+ .append(
1377
+ $('<span/>', {
1378
+ class: prefix + 'msg-error',
1379
+ text: err
1380
+ })
1381
+ );
1055
1382
  },
1056
1383
 
1057
1384
 
1385
+ // Range management
1386
+ saveRange: function () {
1387
+ var t = this,
1388
+ documentSelection = t.doc.getSelection();
1389
+
1390
+ t.range = null;
1391
+
1392
+ if (documentSelection.rangeCount) {
1393
+ var savedRange = t.range = documentSelection.getRangeAt(0),
1394
+ range = t.doc.createRange(),
1395
+ rangeStart;
1396
+ range.selectNodeContents(t.$ed[0]);
1397
+ range.setEnd(savedRange.startContainer, savedRange.startOffset);
1398
+ rangeStart = (range + '').length;
1399
+ t.metaRange = {
1400
+ start: rangeStart,
1401
+ end: rangeStart + (savedRange + '').length
1402
+ };
1403
+ }
1404
+ },
1405
+ restoreRange: function () {
1406
+ var t = this,
1407
+ metaRange = t.metaRange,
1408
+ savedRange = t.range,
1409
+ documentSelection = t.doc.getSelection(),
1410
+ range;
1411
+
1412
+ if (!savedRange) {
1413
+ return;
1414
+ }
1415
+
1416
+ if (metaRange && metaRange.start !== metaRange.end) { // Algorithm from http://jsfiddle.net/WeWy7/3/
1417
+ var charIndex = 0,
1418
+ nodeStack = [t.$ed[0]],
1419
+ node,
1420
+ foundStart = false,
1421
+ stop = false;
1422
+
1423
+ range = t.doc.createRange();
1424
+
1425
+ while (!stop && (node = nodeStack.pop())) {
1426
+ if (node.nodeType === 3) {
1427
+ var nextCharIndex = charIndex + node.length;
1428
+ if (!foundStart && metaRange.start >= charIndex && metaRange.start <= nextCharIndex) {
1429
+ range.setStart(node, metaRange.start - charIndex);
1430
+ foundStart = true;
1431
+ }
1432
+ if (foundStart && metaRange.end >= charIndex && metaRange.end <= nextCharIndex) {
1433
+ range.setEnd(node, metaRange.end - charIndex);
1434
+ stop = true;
1435
+ }
1436
+ charIndex = nextCharIndex;
1437
+ } else {
1438
+ var cn = node.childNodes,
1439
+ i = cn.length;
1440
+
1441
+ while (i > 0) {
1442
+ i -= 1;
1443
+ nodeStack.push(cn[i]);
1444
+ }
1445
+ }
1446
+ }
1447
+ }
1058
1448
 
1449
+ documentSelection.removeAllRanges();
1450
+ documentSelection.addRange(range || savedRange);
1451
+ },
1452
+ getRangeText: function () {
1453
+ return this.range + '';
1454
+ },
1059
1455
 
1060
- // Selection management
1061
- saveSelection: function(){
1456
+ updateButtonPaneStatus: function () {
1062
1457
  var t = this,
1063
- d = document;
1064
-
1065
- t.selection = null;
1066
- if(window.getSelection){
1067
- var s = window.getSelection();
1068
- if(s.getRangeAt && s.rangeCount)
1069
- t.selection = s.getRangeAt(0);
1070
- } else if(d.selection && d.selection.createRange)
1071
- t.selection = d.selection.createRange();
1458
+ prefix = t.o.prefix,
1459
+ tags = t.getTagsRecursive(t.doc.getSelection().focusNode.parentNode),
1460
+ activeClasses = prefix + 'active-button ' + prefix + 'active';
1461
+
1462
+ $('.' + prefix + 'active-button', t.$btnPane).removeClass(activeClasses);
1463
+ $.each(tags, function (i, tag) {
1464
+ var btnName = t.tagToButton[tag.toLowerCase()],
1465
+ $btn = $('.' + prefix + btnName + '-button', t.$btnPane);
1466
+
1467
+ if ($btn.length > 0) {
1468
+ $btn.addClass(activeClasses);
1469
+ } else {
1470
+ try {
1471
+ $btn = $('.' + prefix + 'dropdown .' + prefix + btnName + '-dropdown-button', t.$box);
1472
+ var dropdownBtnName = $btn.parent().data('dropdown');
1473
+ $('.' + prefix + dropdownBtnName + '-button', t.$box).addClass(activeClasses);
1474
+ } catch (e) {
1475
+ }
1476
+ }
1477
+ });
1072
1478
  },
1073
- restoreSelection: function(){
1074
- var range = this.selection;
1075
- if(range){
1076
- if(window.getSelection){
1077
- var s = window.getSelection();
1078
- s.removeAllRanges();
1079
- s.addRange(range);
1080
- } else if(document.selection && range.select)
1081
- range.select();
1479
+ getTagsRecursive: function (element, tags) {
1480
+ var t = this;
1481
+ tags = tags || [];
1482
+
1483
+ var tag = element.tagName;
1484
+ if (tag === 'DIV') {
1485
+ return tags;
1486
+ }
1487
+ if (tag === 'P' && element.style.textAlign !== '') {
1488
+ tags.push(element.style.textAlign);
1082
1489
  }
1083
- },
1084
1490
 
1491
+ $.each(t.tagHandlers, function (i, tagHandler) {
1492
+ tags = tags.concat(tagHandler(element, t));
1493
+ });
1085
1494
 
1495
+ tags.push(tag);
1086
1496
 
1087
- // Return true if must enable Trumbowyg on this mobile device
1088
- isEnabled: function(){
1089
- var exprTablet = new RegExp("(iPad|webOS)"),
1090
- exprMobile = new RegExp("(iPhone|iPod|Android|BlackBerry|Windows Phone|ZuneWP7)"),
1091
- ua = navigator.userAgent;
1497
+ return t.getTagsRecursive(element.parentNode, tags);
1498
+ },
1092
1499
 
1093
- return (this.o.tablet === true && exprTablet.test(ua)) || (this.o.mobile === true && exprMobile.test(ua));
1500
+ // Plugins
1501
+ initPlugins: function () {
1502
+ var t = this;
1503
+ t.loadedPlugins = [];
1504
+ $.each($.trumbowyg.plugins, function (name, plugin) {
1505
+ if (!plugin.shouldInit || plugin.shouldInit(t)) {
1506
+ plugin.init(t);
1507
+ if (plugin.tagHandler) {
1508
+ t.tagHandlers.push(plugin.tagHandler);
1509
+ }
1510
+ t.loadedPlugins.push(plugin);
1511
+ }
1512
+ });
1513
+ },
1514
+ destroyPlugins: function () {
1515
+ $.each(this.loadedPlugins, function (i, plugin) {
1516
+ if (plugin.destroy) {
1517
+ plugin.destroy();
1518
+ }
1519
+ });
1094
1520
  }
1095
1521
  };
1096
- })(window, document, jQuery);
1522
+ })(navigator, window, document, jQuery);