trumbowyg_rails 1.1.3 → 2.1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/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);