simditor-rails 1.0.1 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eec047eb279afe4a2ce42ef44da6fa8a2af4e540
4
- data.tar.gz: 38515fd63b6f80ce7da112da72b2cb6111a59e7a
3
+ metadata.gz: 896fba20d92494e0ef2e9bbcfcad1b92b7f17660
4
+ data.tar.gz: cc3dc37a5e0058a7ad08601421dba33b1aed0c48
5
5
  SHA512:
6
- metadata.gz: 9258bcda4fa23e45c6638a1687fc2c86e5a3aeee73e18d1a545baf06a18e1f98704c1bab19fb1fae020db22642ee35a978384de8cea218a6a9073aae364974a4
7
- data.tar.gz: 7dfe5455ebbfa5e475ac64926ca187d3418818abe35d67db7b90a037095b5d346360bd12320f1289d836a52a2bdaa520b50f81f4b9ceb4c662242087ac2eb202
6
+ metadata.gz: 7a68523ad12395635a6f07e3e6d6b90f250c075709dfd0c1c83fcc341453d42cb4e4e2431d8d8f2f3625a602dd48e05d9a891c1eb86d4f2d076c2d859c50cae6
7
+ data.tar.gz: ce685882ba6f4002103487975644fb9db37eb1bb68178e7cc8a2098b7d6cbc157c4dc6c52b07ebdc4b8ba86b72b175b509a2bd3b6012470164ef555a3381f249
@@ -1,8 +1,5 @@
1
1
  require "simditor-rails/version"
2
+ require 'simditor-rails/engine'
2
3
 
3
4
  module Simditor
4
- module Rails
5
- class Engine < ::Rails::Engine
6
- end
7
- end
8
- end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Simditor
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ initializer :assets do |app|
5
+ app.config.assets.precompile += %w( loading-upload.gif )
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  module Simditor
2
2
  module Rails
3
- VERSION = "1.0.1"
3
+ VERSION = "1.0.5"
4
4
  end
5
5
  end
@@ -0,0 +1,151 @@
1
+ (function() {
2
+ var Module, Plugin, Widget,
3
+ __slice = [].slice,
4
+ __hasProp = {}.hasOwnProperty,
5
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
6
+
7
+ Module = (function() {
8
+ function Module() {}
9
+
10
+ Module.extend = function(obj) {
11
+ var key, val, _ref;
12
+ if (!((obj != null) && typeof obj === 'object')) {
13
+ return;
14
+ }
15
+ for (key in obj) {
16
+ val = obj[key];
17
+ if (key !== 'included' && key !== 'extended') {
18
+ this[key] = val;
19
+ }
20
+ }
21
+ return (_ref = obj.extended) != null ? _ref.call(this) : void 0;
22
+ };
23
+
24
+ Module.include = function(obj) {
25
+ var key, val, _ref;
26
+ if (!((obj != null) && typeof obj === 'object')) {
27
+ return;
28
+ }
29
+ for (key in obj) {
30
+ val = obj[key];
31
+ if (key !== 'included' && key !== 'extended') {
32
+ this.prototype[key] = val;
33
+ }
34
+ }
35
+ return (_ref = obj.included) != null ? _ref.call(this) : void 0;
36
+ };
37
+
38
+ Module.prototype.on = function() {
39
+ var args, _ref;
40
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
41
+ return (_ref = $(this)).on.apply(_ref, args);
42
+ };
43
+
44
+ Module.prototype.one = function() {
45
+ var args, _ref;
46
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
47
+ return (_ref = $(this)).one.apply(_ref, args);
48
+ };
49
+
50
+ Module.prototype.off = function() {
51
+ var args, _ref;
52
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
53
+ return (_ref = $(this)).off.apply(_ref, args);
54
+ };
55
+
56
+ Module.prototype.trigger = function() {
57
+ var args, _ref;
58
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
59
+ return (_ref = $(this)).trigger.apply(_ref, args);
60
+ };
61
+
62
+ Module.prototype.triggerHandler = function() {
63
+ var args, _ref;
64
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
65
+ return (_ref = $(this)).triggerHandler.apply(_ref, args);
66
+ };
67
+
68
+ return Module;
69
+
70
+ })();
71
+
72
+ Widget = (function(_super) {
73
+ __extends(Widget, _super);
74
+
75
+ Widget.connect = function(cls) {
76
+ if (typeof cls !== 'function') {
77
+ return;
78
+ }
79
+ if (!cls.className) {
80
+ throw new Error('Widget.connect: lack of class property "className"');
81
+ return;
82
+ }
83
+ if (!this._connectedClasses) {
84
+ this._connectedClasses = [];
85
+ }
86
+ this._connectedClasses.push(cls);
87
+ if (cls.className) {
88
+ return this[cls.className] = cls;
89
+ }
90
+ };
91
+
92
+ Widget.prototype._init = function() {};
93
+
94
+ Widget.prototype.opts = {};
95
+
96
+ function Widget(opts) {
97
+ var cls, instance, instances, name, _base, _i, _len;
98
+ this.opts = $.extend({}, this.opts, opts);
99
+ (_base = this.constructor)._connectedClasses || (_base._connectedClasses = []);
100
+ instances = (function() {
101
+ var _i, _len, _ref, _results;
102
+ _ref = this.constructor._connectedClasses;
103
+ _results = [];
104
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
105
+ cls = _ref[_i];
106
+ name = cls.className.charAt(0).toLowerCase() + cls.className.slice(1);
107
+ _results.push(this[name] = new cls(this));
108
+ }
109
+ return _results;
110
+ }).call(this);
111
+ this._init();
112
+ for (_i = 0, _len = instances.length; _i < _len; _i++) {
113
+ instance = instances[_i];
114
+ if (typeof instance._init === "function") {
115
+ instance._init();
116
+ }
117
+ }
118
+ this.trigger('pluginconnected');
119
+ }
120
+
121
+ Widget.prototype.destroy = function() {};
122
+
123
+ return Widget;
124
+
125
+ })(Module);
126
+
127
+ Plugin = (function(_super) {
128
+ __extends(Plugin, _super);
129
+
130
+ Plugin.className = 'Plugin';
131
+
132
+ Plugin.prototype.opts = {};
133
+
134
+ function Plugin(widget) {
135
+ this.widget = widget;
136
+ this.opts = $.extend({}, this.opts, this.widget.opts);
137
+ }
138
+
139
+ Plugin.prototype._init = function() {};
140
+
141
+ return Plugin;
142
+
143
+ })(Module);
144
+
145
+ window.Module = Module;
146
+
147
+ window.Widget = Widget;
148
+
149
+ window.Plugin = Plugin;
150
+
151
+ }).call(this);
@@ -33,6 +33,9 @@ class Selection extends Plugin
33
33
  @clear()
34
34
  @sel.addRange(range)
35
35
 
36
+ # firefox won't auto focus while applying new range
37
+ @editor.body.focus() if !@editor.inputManager.focused and (@editor.util.browser.firefox or @editor.util.browser.msie)
38
+
36
39
  rangeAtEndOf: (node, range = @getRange()) ->
37
40
  return unless range? and range.collapsed
38
41
 
@@ -128,14 +131,36 @@ class Selection extends Plugin
128
131
  range.setEnd(node, 0)
129
132
  else
130
133
  nodeLength = @editor.util.getNodeLength node
131
- nodeLength -= 1 if node.nodeType != 3 and nodeLength > 0 and $(node).contents().last().is('br')
134
+ if node.nodeType != 3 and nodeLength > 0
135
+ $lastNode = $(node).contents().last()
136
+ if $lastNode.is('br')
137
+ nodeLength -= 1
138
+ else if $lastNode[0].nodeType != 3 and @editor.util.isEmptyNode($lastNode)
139
+ $lastNode.append @editor.util.phBr
140
+ node = $lastNode[0]
141
+ nodeLength = 0
142
+
132
143
  range.setEnd(node, nodeLength)
133
144
 
134
145
  range.collapse(false)
135
146
  @selectRange range
136
147
 
137
148
  deleteRangeContents: (range = @getRange()) ->
138
- range.deleteContents()
149
+ startRange = range.cloneRange()
150
+ endRange = range.cloneRange()
151
+ startRange.collapse(true)
152
+ endRange.collapse(false)
153
+
154
+ # the default behavior of cmd+a is buggy
155
+ if !range.collapsed and @rangeAtStartOf(@editor.body, startRange) and @rangeAtEndOf(@editor.body, endRange)
156
+ @editor.body.empty()
157
+ range.setStart @editor.body[0], 0
158
+ range.collapse true
159
+ @selectRange range
160
+ else
161
+ range.deleteContents()
162
+
163
+ range
139
164
 
140
165
  breakBlockEl: (el, range = @getRange()) ->
141
166
  $el = $(el)
@@ -144,10 +169,9 @@ class Selection extends Plugin
144
169
  return $el if range.collapsed
145
170
  $el.before range.extractContents()
146
171
 
147
- save: () ->
172
+ save: (range = @getRange()) ->
148
173
  return if @_selectionSaved
149
174
 
150
- range = @getRange()
151
175
  startCaret = $('<span/>').addClass('simditor-caret-start')
152
176
  endCaret = $('<span/>').addClass('simditor-caret-end')
153
177
 
@@ -171,7 +195,7 @@ class Selection extends Plugin
171
195
  endOffset = endContainer.contents().index(endCaret)
172
196
 
173
197
  if startContainer[0] == endContainer[0]
174
- endOffset -= 1;
198
+ endOffset -= 1
175
199
 
176
200
  range = document.createRange()
177
201
  range.setStart(startContainer.get(0), startOffset)
@@ -180,9 +204,6 @@ class Selection extends Plugin
180
204
  startCaret.remove()
181
205
  endCaret.remove()
182
206
  @selectRange range
183
-
184
- # firefox won't auto focus while applying new range
185
- @editor.body.focus() if @editor.util.browser.firefox
186
207
  else
187
208
  startCaret.remove()
188
209
  endCaret.remove()
@@ -201,22 +222,22 @@ class Formatter extends Plugin
201
222
  super args...
202
223
  @editor = @widget
203
224
 
225
+ @_allowedTags = ['br', 'a', 'img', 'b', 'strong', 'i', 'u', 'font', 'p', 'ul', 'ol', 'li', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'hr']
226
+ @_allowedAttributes =
227
+ img: ['src', 'alt', 'width', 'height', 'data-image-src', 'data-image-size', 'data-image-name', 'data-non-image']
228
+ a: ['href', 'target']
229
+ font: ['color']
230
+ pre: ['data-lang', 'class']
231
+ p: ['data-indent']
232
+ h1: ['data-indent']
233
+ h2: ['data-indent']
234
+ h3: ['data-indent']
235
+ h4: ['data-indent']
236
+
204
237
  _init: ->
205
238
  @editor.body.on 'click', 'a', (e) =>
206
239
  false
207
240
 
208
- _allowedTags: ['br', 'a', 'img', 'b', 'strong', 'i', 'u', 'p', 'ul', 'ol', 'li', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4']
209
-
210
- _allowedAttributes:
211
- img: ['src', 'alt', 'width', 'height', 'data-origin-src', 'data-origin-size', 'data-origin-name']
212
- a: ['href', 'target']
213
- pre: ['data-lang']
214
- p: ['data-indent']
215
- h1: ['data-indent']
216
- h2: ['data-indent']
217
- h3: ['data-indent']
218
- h4: ['data-indent']
219
-
220
241
  decorate: ($el = @editor.body) ->
221
242
  @editor.trigger 'decorate', [$el]
222
243
 
@@ -230,7 +251,7 @@ class Formatter extends Plugin
230
251
  findLinkNode = ($parentNode) ->
231
252
  $parentNode.contents().each (i, node) ->
232
253
  $node = $(node)
233
- if $node.is('a') or $node.closest('a', $el).length
254
+ if $node.is('a') or $node.closest('a, pre', $el).length
234
255
  return
235
256
 
236
257
  if $node.contents().length
@@ -240,18 +261,18 @@ class Formatter extends Plugin
240
261
 
241
262
  findLinkNode $el
242
263
 
243
- re = /(https?:\/\/|www\.)[\w\-\.\?&=\/#%:]+/ig
264
+ re = /(https?:\/\/|www\.)[\w\-\.\?&=\/#%:,\!\+]+/ig
244
265
  for $node in linkNodes
245
- text = $node.text();
246
- replaceEls = [];
247
- match = null;
248
- lastIndex = 0;
266
+ text = $node.text()
267
+ replaceEls = []
268
+ match = null
269
+ lastIndex = 0
249
270
 
250
271
  while (match = re.exec(text)) != null
251
272
  replaceEls.push document.createTextNode(text.substring(lastIndex, match.index))
252
273
  lastIndex = re.lastIndex
253
274
  uri = if /^(http(s)?:\/\/|\/)/.test(match[0]) then match[0] else 'http://' + match[0]
254
- replaceEls.push $('<a href="' + uri + '" rel="nofollow">' + match[0] + '</a>')[0]
275
+ replaceEls.push $('<a href="' + uri + '" rel="nofollow"></a>').text(match[0])[0]
255
276
 
256
277
  replaceEls.push document.createTextNode(text.substring(lastIndex))
257
278
  $node.replaceWith $(replaceEls)
@@ -271,7 +292,7 @@ class Formatter extends Plugin
271
292
  if $node.is('br')
272
293
  blockNode = null if blockNode?
273
294
  $node.remove()
274
- else if @editor.util.isBlockNode(node) or $node.is('img')
295
+ else if @editor.util.isBlockNode(node)
275
296
  if $node.is('li')
276
297
  if blockNode and blockNode.is('ul, ol')
277
298
  blockNode.append node
@@ -291,8 +312,11 @@ class Formatter extends Plugin
291
312
 
292
313
  if $node[0].nodeType == 3
293
314
  text = $node.text().replace(/(\r\n|\n|\r)/gm, '')
294
- textNode = document.createTextNode text
295
- $node.replaceWith textNode
315
+ if text
316
+ textNode = document.createTextNode text
317
+ $node.replaceWith textNode
318
+ else
319
+ $node.remove()
296
320
  return
297
321
 
298
322
  contents = $node.contents()
@@ -300,8 +324,14 @@ class Formatter extends Plugin
300
324
 
301
325
  if $node.is(@_allowedTags.join(',')) or isDecoration
302
326
  # img inside a is not allowed
303
- if $node.is('a') and $node.find('img').length > 0
304
- contents.first().unwrap()
327
+ if $node.is('a') and ($childImg = $node.find('img')).length > 0
328
+ $node.replaceWith $childImg
329
+ $node = $childImg
330
+ contents = null
331
+
332
+ # exclude uploading img
333
+ if $node.is('img') and $node.hasClass('uploading')
334
+ $node.remove()
305
335
 
306
336
  # Clean attributes except `src` `alt` on `img` tag and `href` `target` on `a` tag
307
337
  unless isDecoration
@@ -335,16 +365,17 @@ class Formatter extends Plugin
335
365
 
336
366
  clearHtml: (html, lineBreak = true) ->
337
367
  container = $('<div/>').append(html)
368
+ contents = container.contents()
338
369
  result = ''
339
370
 
340
- container.contents().each (i, node) =>
371
+ contents.each (i, node) =>
341
372
  if node.nodeType == 3
342
373
  result += node.nodeValue
343
374
  else if node.nodeType == 1
344
375
  $node = $(node)
345
- contents = $node.contents()
346
- result += @clearHtml contents if contents.length > 0
347
- if lineBreak and $node.is 'br, p, div, li, tr, pre, address, artticle, aside, dl, figcaption, footer, h1, h2, h3, h4, header'
376
+ children = $node.contents()
377
+ result += @clearHtml children if children.length > 0
378
+ if lineBreak and i < contents.length - 1 and $node.is 'br, p, div, li, tr, pre, address, artticle, aside, dl, figcaption, footer, h1, h2, h3, h4, header'
348
379
  result += '\n'
349
380
 
350
381
  result
@@ -356,9 +387,9 @@ class Formatter extends Plugin
356
387
 
357
388
  $contents.each (i, el) =>
358
389
  $el = $(el)
359
- $el.remove() if $el.is(':not(img, br):empty')
390
+ $el.remove() if $el.is(':not(img, br, col, td, hr, [class^="simditor-"]):empty')
360
391
  $el.remove() if uselessP($el) #and uselessP($el.prev())
361
- $el.find(':not(img, br):empty').remove()
392
+ $el.find(':not(img, br, col, td, hr, [class^="simditor-"]):empty').remove()
362
393
 
363
394
 
364
395
 
@@ -376,11 +407,17 @@ class InputManager extends Plugin
376
407
  @editor = @widget
377
408
  @opts.pasteImage = 'inline' if @opts.pasteImage and typeof @opts.pasteImage != 'string'
378
409
 
410
+ # handlers which will be called when specific key is pressed in specific node
411
+ @_keystrokeHandlers = {}
412
+
413
+ @_shortcuts = {}
414
+
379
415
  _modifierKeys: [16, 17, 18, 91, 93, 224]
380
416
 
381
417
  _arrowKeys: [37..40]
382
418
 
383
419
  _init: ->
420
+
384
421
  @_pasteArea = $('<div/>')
385
422
  .css({
386
423
  width: '1px',
@@ -397,13 +434,45 @@ class InputManager extends Plugin
397
434
  .addClass('simditor-paste-area')
398
435
  .appendTo(@editor.el)
399
436
 
437
+ @_cleanPasteArea = $('<textarea/>')
438
+ .css({
439
+ width: '1px',
440
+ height: '1px',
441
+ overflow: 'hidden',
442
+ position: 'fixed',
443
+ right: '0',
444
+ bottom: '101px'
445
+ })
446
+ .attr({
447
+ tabIndex: '-1'
448
+ })
449
+ .addClass('simditor-clean-paste-area')
450
+ .appendTo(@editor.el)
451
+
400
452
  @editor.on 'valuechanged', =>
401
- # make sure each code block, img and table has a p following it
402
- @editor.body.find('hr, pre, .simditor-image, .simditor-table').each (i, el) =>
453
+ # make sure each code block and table has siblings
454
+ @editor.body.find('hr, pre, .simditor-table').each (i, el) =>
403
455
  $el = $(el)
404
- if ($el.parent().is('blockquote') or $el.parent()[0] == @editor.body[0]) and $el.next().length == 0
405
- $('<p/>').append(@editor.util.phBr)
406
- .insertAfter($el)
456
+ if ($el.parent().is('blockquote') or $el.parent()[0] == @editor.body[0])
457
+ formatted = false
458
+
459
+ if $el.next().length == 0
460
+ $('<p/>').append(@editor.util.phBr)
461
+ .insertAfter($el)
462
+ formatted = true
463
+
464
+ if $el.prev().length == 0
465
+ $('<p/>').append(@editor.util.phBr)
466
+ .insertBefore($el)
467
+ formatted = true
468
+
469
+ if formatted
470
+ setTimeout =>
471
+ @editor.trigger 'valuechanged'
472
+ , 10
473
+
474
+ @editor.body.find('pre:empty').append(@editor.util.phBr)
475
+
407
476
 
408
477
  @editor.body.on('keydown', $.proxy(@_onKeyDown, @))
409
478
  .on('keypress', $.proxy(@_onKeyPress, @))
@@ -412,28 +481,40 @@ class InputManager extends Plugin
412
481
  .on('focus', $.proxy(@_onFocus, @))
413
482
  .on('blur', $.proxy(@_onBlur, @))
414
483
  .on('paste', $.proxy(@_onPaste, @))
484
+ .on('drop', $.proxy(@_onDrop, @))
415
485
 
416
486
  # fix firefox cmd+left/right bug
417
487
  if @editor.util.browser.firefox
418
488
  @addShortcut 'cmd+37', (e) =>
419
489
  e.preventDefault()
420
490
  @editor.selection.sel.modify('move', 'backward', 'lineboundary')
491
+ false
421
492
  @addShortcut 'cmd+39', (e) =>
422
493
  e.preventDefault()
423
494
  @editor.selection.sel.modify('move', 'forward', 'lineboundary')
495
+ false
496
+
497
+ # meta + enter: submit form
498
+ submitKey = if @editor.util.os.mac then 'cmd+13' else 'ctrl+13'
499
+ @addShortcut submitKey, (e) =>
500
+ @editor.el.closest('form')
501
+ .find('button:submit')
502
+ .click()
503
+ false
424
504
 
425
505
  if @editor.textarea.attr 'autofocus'
426
506
  setTimeout =>
427
507
  @editor.focus()
428
508
  , 0
429
509
 
510
+
430
511
  _onFocus: (e) ->
431
512
  @editor.el.addClass('focus')
432
513
  .removeClass('error')
433
514
  @focused = true
434
515
  @lastCaretPosition = null
435
516
 
436
- @editor.body.find('.selected').removeClass('selected')
517
+ #@editor.body.find('.selected').removeClass('selected')
437
518
 
438
519
  setTimeout =>
439
520
  @editor.triggerHandler 'focus'
@@ -449,9 +530,10 @@ class InputManager extends Plugin
449
530
  @editor.triggerHandler 'blur'
450
531
 
451
532
  _onMouseUp: (e) ->
452
- return if $(e.target).is('img, .simditor-image')
453
- @editor.trigger 'selectionchanged'
454
- @editor.undoManager.update()
533
+ setTimeout =>
534
+ @editor.trigger 'selectionchanged'
535
+ @editor.undoManager.update()
536
+ , 0
455
537
 
456
538
  _onKeyDown: (e) ->
457
539
  if @editor.triggerHandler(e) == false
@@ -460,8 +542,7 @@ class InputManager extends Plugin
460
542
  # handle predefined shortcuts
461
543
  shortcutKey = @editor.util.getShortcutKey e
462
544
  if @_shortcuts[shortcutKey]
463
- @_shortcuts[shortcutKey].call(this, e)
464
- return false
545
+ return @_shortcuts[shortcutKey].call(this, e)
465
546
 
466
547
  # Check the condictional handlers
467
548
  if e.which of @_keystrokeHandlers
@@ -475,7 +556,12 @@ class InputManager extends Plugin
475
556
  return unless node.nodeType == 1
476
557
  handler = @_keystrokeHandlers[e.which]?[node.tagName.toLowerCase()]
477
558
  result = handler?(e, $(node))
478
- !result
559
+
560
+ # different result means:
561
+ # 1. true, has do everythings, stop browser default action and traverseUp
562
+ # 2. false, stop traverseUp
563
+ # 3. undefined, continue traverseUp
564
+ false if result == true or result == false
479
565
  if result
480
566
  @editor.trigger 'valuechanged'
481
567
  @editor.trigger 'selectionchanged'
@@ -490,7 +576,18 @@ class InputManager extends Plugin
490
576
  # paste shortcut
491
577
  return if metaKey and e.which == 86
492
578
 
493
- if @_typing
579
+ if @editor.util.browser.webkit and e.which == 8 and @editor.selection.rangeAtStartOf $blockEl
580
+ # fix the span bug in webkit browsers
581
+ setTimeout =>
582
+ $newBlockEl = @editor.util.closestBlockEl()
583
+ @editor.selection.save()
584
+ @editor.formatter.cleanNode $newBlockEl, true
585
+ @editor.selection.restore()
586
+ @editor.trigger 'valuechanged'
587
+ @editor.trigger 'selectionchanged'
588
+ , 10
589
+ @typing = true
590
+ else if @_typing
494
591
  clearTimeout @_typing if @_typing != true
495
592
  @_typing = setTimeout =>
496
593
  @editor.trigger 'valuechanged'
@@ -510,22 +607,6 @@ class InputManager extends Plugin
510
607
  if @editor.triggerHandler(e) == false
511
608
  return false
512
609
 
513
- # input hooks are limited in a single line
514
- @_hookStack.length = 0 if e.which == 13
515
-
516
- # check the input hooks
517
- if e.which == 32
518
- cmd = @_hookStack.join ''
519
- @_hookStack.length = 0
520
-
521
- for hook in @_inputHooks
522
- if (hook.cmd instanceof RegExp and hook.cmd.test(cmd)) or hook.cmd == cmd
523
- hook.callback(e, hook, cmd)
524
- break
525
- else if @_hookKeyMap[e.which]
526
- @_hookStack.push @_hookKeyMap[e.which]
527
- @_hookStack.shift() if @_hookStack.length > 10
528
-
529
610
  _onKeyUp: (e) ->
530
611
  if @editor.triggerHandler(e) == false
531
612
  return false
@@ -535,7 +616,7 @@ class InputManager extends Plugin
535
616
  @editor.undoManager.update()
536
617
  return
537
618
 
538
- if e.which == 8 and (@editor.body.is(':empty') or (@editor.body.children().length == 1 and @editor.body.children().is('br')))
619
+ if e.which == 8 and @editor.util.isEmptyNode(@editor.body)
539
620
  @editor.body.empty()
540
621
  p = $('<p/>').append(@editor.util.phBr)
541
622
  .appendTo(@editor.body)
@@ -546,35 +627,54 @@ class InputManager extends Plugin
546
627
  if @editor.triggerHandler(e) == false
547
628
  return false
548
629
 
630
+ range = @editor.selection.deleteRangeContents()
631
+ range.collapse(true) unless range.collapsed
632
+ $blockEl = @editor.util.closestBlockEl()
633
+ cleanPaste = $blockEl.is 'pre, table'
634
+
549
635
  if e.originalEvent.clipboardData && e.originalEvent.clipboardData.items && e.originalEvent.clipboardData.items.length > 0
550
636
  pasteItem = e.originalEvent.clipboardData.items[0]
551
637
 
552
638
  # paste file in chrome
553
- if /^image\//.test pasteItem.type
639
+ if /^image\//.test(pasteItem.type) and !cleanPaste
554
640
  imageFile = pasteItem.getAsFile()
555
641
  return unless imageFile? and @opts.pasteImage
556
642
 
557
643
  unless imageFile.name
558
- imageFile.name = "来自剪贴板的图片.png";
644
+ imageFile.name = "Clipboard Image.png"
559
645
 
560
646
  uploadOpt = {}
561
647
  uploadOpt[@opts.pasteImage] = true
562
648
  @editor.uploader?.upload(imageFile, uploadOpt)
563
649
  return false
564
650
 
651
+ @editor.selection.save range
565
652
 
566
- $blockEl = @editor.util.closestBlockEl()
567
- codePaste = $blockEl.is 'pre'
568
- @editor.selection.deleteRangeContents()
569
- @editor.selection.save()
653
+ if cleanPaste
654
+ @_cleanPasteArea.focus()
655
+
656
+ # firefox cannot set focus on textarea before pasting
657
+ if @editor.util.browser.firefox
658
+ e.preventDefault()
659
+ @_cleanPasteArea.val e.originalEvent.clipboardData.getData('text/plain')
570
660
 
571
- @_pasteArea.focus()
661
+ # IE10 cannot set focus on textarea or editable div before pasting
662
+ else if @editor.util.browser.msie and @editor.util.browser.version == 10
663
+ e.preventDefault()
664
+ @_cleanPasteArea.val window.clipboardData.getData('Text')
665
+ else
666
+ @_pasteArea.focus()
667
+
668
+ # IE10 cannot set focus on textarea or editable div before pasting
669
+ if @editor.util.browser.msie and @editor.util.browser.version == 10
670
+ e.preventDefault()
671
+ @_pasteArea.html window.clipboardData.getData('Text')
572
672
 
573
673
  setTimeout =>
574
- if @_pasteArea.is(':empty')
674
+ if @_pasteArea.is(':empty') and !@_cleanPasteArea.val()
575
675
  pasteContent = null
576
- else if codePaste
577
- pasteContent = @editor.formatter.clearHtml @_pasteArea.html()
676
+ else if cleanPaste
677
+ pasteContent = @_cleanPasteArea.val()
578
678
  else
579
679
  pasteContent = $('<div/>').append(@_pasteArea.contents())
580
680
  @editor.formatter.format pasteContent
@@ -583,6 +683,7 @@ class InputManager extends Plugin
583
683
  pasteContent = pasteContent.contents()
584
684
 
585
685
  @_pasteArea.empty()
686
+ @_cleanPasteArea.val('')
586
687
  range = @editor.selection.restore()
587
688
 
588
689
  if @editor.triggerHandler('pasting', [pasteContent]) == false
@@ -590,34 +691,45 @@ class InputManager extends Plugin
590
691
 
591
692
  if !pasteContent
592
693
  return
593
- else if codePaste
594
- node = document.createTextNode(pasteContent)
595
- @editor.selection.insertNode node, range
694
+ else if cleanPaste
695
+ if $blockEl.is('table')
696
+ lines = pasteContent.split('\n')
697
+ lastLine = lines.pop()
698
+ for line in lines
699
+ @editor.selection.insertNode document.createTextNode(line)
700
+ @editor.selection.insertNode $('<br/>')
701
+ @editor.selection.insertNode document.createTextNode(lastLine)
702
+ else
703
+ pasteContent = $('<div/>').text(pasteContent)
704
+ @editor.selection.insertNode($(node)[0], range) for node in pasteContent.contents()
705
+ else if $blockEl.is @editor.body
706
+ @editor.selection.insertNode(node, range) for node in pasteContent
596
707
  else if pasteContent.length < 1
597
708
  return
598
709
  else if pasteContent.length == 1
599
710
  if pasteContent.is('p')
600
711
  children = pasteContent.contents()
601
- @editor.selection.insertNode node, range for node in children
602
-
603
- # paste image in firefox
604
- else if pasteContent.is('.simditor-image')
605
- $img = pasteContent.find('img')
606
-
607
- # firefox
608
- if dataURLtoBlob && $img.is('img[src^="data:image/png;base64"]')
609
- return unless @opts.pasteImage
610
- blob = dataURLtoBlob $img.attr( "src" )
611
- blob.name = "来自剪贴板的图片.png"
612
-
613
- uploadOpt = {}
614
- uploadOpt[@opts.pasteImage] = true
615
- @editor.uploader?.upload(blob, uploadOpt)
616
- return
617
-
618
- # cannot paste image in safari
619
- else if imgEl.is('img[src^="webkit-fake-url://"]')
620
- return
712
+
713
+ if children.length == 1 and children.is('img')
714
+ $img = children
715
+
716
+ # paste image in firefox and IE 11
717
+ if /^data:image/.test($img.attr('src'))
718
+ return unless @opts.pasteImage
719
+ blob = @editor.util.dataURLtoBlob $img.attr( "src" )
720
+ blob.name = "Clipboard Image.png"
721
+
722
+ uploadOpt = {}
723
+ uploadOpt[@opts.pasteImage] = true
724
+ @editor.uploader?.upload(blob, uploadOpt)
725
+ return
726
+
727
+ # cannot paste image in safari
728
+ else if $img.is('img[src^="webkit-fake-url://"]')
729
+ return
730
+ else
731
+ @editor.selection.insertNode(node, range) for node in children
732
+
621
733
  else if $blockEl.is('p') and @editor.util.isEmptyNode $blockEl
622
734
  $blockEl.replaceWith pasteContent
623
735
  @editor.selection.setRangeAtEndOf(pasteContent, range)
@@ -645,44 +757,25 @@ class InputManager extends Plugin
645
757
  @editor.trigger 'selectionchanged'
646
758
  , 10
647
759
 
760
+ _onDrop: (e) ->
761
+ if @editor.triggerHandler(e) == false
762
+ return false
763
+
764
+ setTimeout =>
765
+ @editor.trigger 'valuechanged'
766
+ @editor.trigger 'selectionchanged'
767
+ , 0
648
768
 
649
- # handlers which will be called when specific key is pressed in specific node
650
- _keystrokeHandlers: {}
651
769
 
652
770
  addKeystrokeHandler: (key, node, handler) ->
653
771
  @_keystrokeHandlers[key] = {} unless @_keystrokeHandlers[key]
654
772
  @_keystrokeHandlers[key][node] = handler
655
773
 
656
774
 
657
- # a hook will be triggered when specific string typed
658
- _inputHooks: []
659
-
660
- _hookKeyMap: {}
661
-
662
- _hookStack: []
663
-
664
- addInputHook: (hookOpt) ->
665
- $.extend(@_hookKeyMap, hookOpt.key)
666
- @_inputHooks.push hookOpt
667
-
668
-
669
- _shortcuts:
670
- # meta + enter: submit form
671
- 'cmd+13': (e) ->
672
- @editor.el.closest('form')
673
- .find('button:submit')
674
- .click()
675
-
676
775
  addShortcut: (keys, handler) ->
677
776
  @_shortcuts[keys] = $.proxy(handler, this)
678
777
 
679
778
 
680
-
681
-
682
-
683
-
684
-
685
-
686
779
  # Standardize keystroke actions across browsers
687
780
 
688
781
  class Keystroke extends Plugin
@@ -711,11 +804,28 @@ class Keystroke extends Plugin
711
804
  true
712
805
 
713
806
 
714
- # Remove hr and img node
807
+ # press enter at end of title block in webkit and IE
808
+ if @editor.util.browser.webkit or @editor.util.browser.msie
809
+ titleEnterHandler = (e, $node) =>
810
+ return unless @editor.selection.rangeAtEndOf $node
811
+ $p = $('<p/>').append(@editor.util.phBr)
812
+ .insertAfter($node)
813
+ @editor.selection.setRangeAtStartOf $p
814
+ true
815
+
816
+ @editor.inputManager.addKeystrokeHandler '13', 'h1', titleEnterHandler
817
+ @editor.inputManager.addKeystrokeHandler '13', 'h2', titleEnterHandler
818
+ @editor.inputManager.addKeystrokeHandler '13', 'h3', titleEnterHandler
819
+ @editor.inputManager.addKeystrokeHandler '13', 'h4', titleEnterHandler
820
+ @editor.inputManager.addKeystrokeHandler '13', 'h5', titleEnterHandler
821
+ @editor.inputManager.addKeystrokeHandler '13', 'h6', titleEnterHandler
822
+
823
+
824
+ # Remove hr
715
825
  @editor.inputManager.addKeystrokeHandler '8', '*', (e) =>
716
826
  $rootBlock = @editor.util.furthestBlockEl()
717
827
  $prevBlockEl = $rootBlock.prev()
718
- if $prevBlockEl.is('hr, .simditor-image') and @editor.selection.rangeAtStartOf $rootBlock
828
+ if $prevBlockEl.is('hr') and @editor.selection.rangeAtStartOf $rootBlock
719
829
  # TODO: need to test on IE
720
830
  @editor.selection.save()
721
831
  $prevBlockEl.remove()
@@ -814,21 +924,28 @@ class Keystroke extends Plugin
814
924
  @editor.inputManager.addKeystrokeHandler '8', 'li', (e, $node) =>
815
925
  $childList = $node.children('ul, ol')
816
926
  $prevNode = $node.prev('li')
817
- return unless $childList.length > 0 and $prevNode.length > 0
927
+ return false unless $childList.length > 0 and $prevNode.length > 0
818
928
 
819
929
  text = ''
820
930
  $textNode = null
821
931
  $node.contents().each (i, n) =>
822
- if n.nodeType == 3 and n.nodeValue
932
+ return false if n.nodeType is 1 and /UL|OL/.test(n.nodeName)
933
+ return if n.nodeType is 1 and /BR/.test(n.nodeName)
934
+
935
+ if n.nodeType is 3 and n.nodeValue
823
936
  text += n.nodeValue
824
- $textNode = $(n)
937
+ else if n.nodeType is 1
938
+ text += $(n).text()
939
+
940
+ $textNode= $(n)
941
+
825
942
  if $textNode and text.length == 1 and @editor.util.browser.firefox and !$textNode.next('br').length
826
943
  $br = $(@editor.util.phBr).insertAfter $textNode
827
944
  $textNode.remove()
828
945
  @editor.selection.setRangeBefore $br
829
946
  return true
830
947
  else if text.length > 0
831
- return
948
+ return false
832
949
 
833
950
  range = document.createRange()
834
951
  $prevChildList = $prevNode.children('ul, ol')
@@ -868,8 +985,6 @@ class UndoManager extends Plugin
868
985
 
869
986
  @className: 'UndoManager'
870
987
 
871
- _stack: []
872
-
873
988
  _index: -1
874
989
 
875
990
  _capacity: 50
@@ -879,22 +994,29 @@ class UndoManager extends Plugin
879
994
  constructor: (args...) ->
880
995
  super args...
881
996
  @editor = @widget
997
+ @_stack = []
882
998
 
883
999
  _init: ->
884
1000
  if @editor.util.os.mac
885
1001
  undoShortcut = 'cmd+90'
886
- redoShortcut = 'shift+cmd+89'
887
- else
1002
+ redoShortcut = 'shift+cmd+90'
1003
+ else if @editor.util.os.win
888
1004
  undoShortcut = 'ctrl+90'
889
1005
  redoShortcut = 'ctrl+89'
1006
+ else
1007
+ undoShortcut = 'ctrl+90'
1008
+ redoShortcut = 'shift+ctrl+90'
1009
+
890
1010
 
891
1011
  @editor.inputManager.addShortcut undoShortcut, (e) =>
892
1012
  e.preventDefault()
893
1013
  @undo()
1014
+ false
894
1015
 
895
1016
  @editor.inputManager.addShortcut redoShortcut, (e) =>
896
1017
  e.preventDefault()
897
1018
  @redo()
1019
+ false
898
1020
 
899
1021
  @editor.on 'valuechanged', (e, src) =>
900
1022
  return if src == 'undo'
@@ -908,6 +1030,8 @@ class UndoManager extends Plugin
908
1030
  , 200
909
1031
 
910
1032
  _pushUndoState: ->
1033
+ return if @editor.triggerHandler('pushundostate') == false
1034
+
911
1035
  currentState = @currentState()
912
1036
  html = @editor.body.html()
913
1037
  return if currentState and currentState.html == html
@@ -939,6 +1063,7 @@ class UndoManager extends Plugin
939
1063
  state = @_stack[@_index]
940
1064
  @editor.body.html state.html
941
1065
  @caretPosition state.caret
1066
+ @editor.body.find('.selected').removeClass('selected')
942
1067
  @editor.sync()
943
1068
 
944
1069
  @editor.trigger 'valuechanged', ['undo']
@@ -954,6 +1079,7 @@ class UndoManager extends Plugin
954
1079
  state = @_stack[@_index]
955
1080
  @editor.body.html state.html
956
1081
  @caretPosition state.caret
1082
+ @editor.body.find('.selected').removeClass('selected')
957
1083
  @editor.sync()
958
1084
 
959
1085
  @editor.trigger 'valuechanged', ['undo']
@@ -1093,16 +1219,20 @@ class Util extends Plugin
1093
1219
  phBr: '<br/>'
1094
1220
 
1095
1221
  os: (->
1222
+ os = {}
1096
1223
  if /Mac/.test navigator.appVersion
1097
- mac: true
1224
+ os.mac = true
1098
1225
  else if /Linux/.test navigator.appVersion
1099
- linux: true
1226
+ os.linux = true
1100
1227
  else if /Win/.test navigator.appVersion
1101
- win: true
1228
+ os.win = true
1102
1229
  else if /X11/.test navigator.appVersion
1103
- unix: true
1104
- else
1105
- {}
1230
+ os.unix = true
1231
+
1232
+ if /Mobi/.test navigator.appVersion
1233
+ os.mobile = true
1234
+
1235
+ os
1106
1236
  )()
1107
1237
 
1108
1238
  browser: (->
@@ -1114,19 +1244,19 @@ class Util extends Plugin
1114
1244
 
1115
1245
  if ie
1116
1246
  msie: true
1117
- version: ua.match(/(msie |rv:)(\d+(\.\d+)?)/i)[2]
1247
+ version: ua.match(/(msie |rv:)(\d+(\.\d+)?)/i)[2] * 1
1118
1248
  else if chrome
1119
1249
  webkit: true
1120
1250
  chrome: true
1121
- version: ua.match(/(?:chrome|crios)\/(\d+(\.\d+)?)/i)[1]
1251
+ version: ua.match(/(?:chrome|crios)\/(\d+(\.\d+)?)/i)[1] * 1
1122
1252
  else if safari
1123
1253
  webkit: true
1124
1254
  safari: true
1125
- version: ua.match(/version\/(\d+(\.\d+)?)/i)[1]
1255
+ version: ua.match(/version\/(\d+(\.\d+)?)/i)[1] * 1
1126
1256
  else if firefox
1127
1257
  mozilla: true
1128
1258
  firefox: true
1129
- version: ua.match(/firefox\/(\d+(\.\d+)?)/i)[1]
1259
+ version: ua.match(/firefox\/(\d+(\.\d+)?)/i)[1] * 1
1130
1260
  else
1131
1261
  {}
1132
1262
  )()
@@ -1137,7 +1267,7 @@ class Util extends Plugin
1137
1267
 
1138
1268
  isEmptyNode: (node) ->
1139
1269
  $node = $(node)
1140
- !$node.text() and !$node.find(':not(br, span)').length
1270
+ $node.is(':empty') or (!$node.text() and !$node.find(':not(br, span, div)').length)
1141
1271
 
1142
1272
  isBlockNode: (node) ->
1143
1273
  node = $(node)[0]
@@ -1315,6 +1445,49 @@ class Util extends Plugin
1315
1445
  @editor.trigger 'selectionchanged'
1316
1446
  true
1317
1447
 
1448
+ # convert base64 data url to blob object for pasting images in firefox and IE11
1449
+ dataURLtoBlob: (dataURL) ->
1450
+ hasBlobConstructor = window.Blob && (->
1451
+ try
1452
+ return Boolean(new Blob())
1453
+ catch e
1454
+ return false
1455
+ )()
1456
+
1457
+ hasArrayBufferViewSupport = hasBlobConstructor && window.Uint8Array && (->
1458
+ try
1459
+ return new Blob([new Uint8Array(100)]).size == 100
1460
+ catch e
1461
+ return false
1462
+ )()
1463
+
1464
+ BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder ||
1465
+ window.MozBlobBuilder || window.MSBlobBuilder;
1466
+
1467
+ return false unless (hasBlobConstructor || BlobBuilder) && window.atob && window.ArrayBuffer && window.Uint8Array
1468
+
1469
+ if dataURL.split(',')[0].indexOf('base64') >= 0
1470
+ # Convert base64 to raw binary data held in a string:
1471
+ byteString = atob(dataURL.split(',')[1])
1472
+ else
1473
+ # Convert base64/URLEncoded data component to raw binary data:
1474
+ byteString = decodeURIComponent(dataURL.split(',')[1])
1475
+
1476
+ # Write the bytes of the string to an ArrayBuffer:
1477
+ arrayBuffer = new ArrayBuffer(byteString.length)
1478
+ intArray = new Uint8Array(arrayBuffer)
1479
+ for i in [0..byteString.length]
1480
+ intArray[i] = byteString.charCodeAt(i)
1481
+
1482
+ # Separate out the mime component:
1483
+ mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]
1484
+ # Write the ArrayBuffer (or ArrayBufferView) to a blob:
1485
+ if hasBlobConstructor
1486
+ return new Blob([if hasArrayBufferViewSupport then intArray else arrayBuffer], {type: mimeString})
1487
+ bb = new BlobBuilder()
1488
+ bb.append(arrayBuffer)
1489
+ bb.getBlob(mimeString)
1490
+
1318
1491
 
1319
1492
  class Toolbar extends Plugin
1320
1493
 
@@ -1336,7 +1509,7 @@ class Toolbar extends Plugin
1336
1509
  return unless @opts.toolbar
1337
1510
 
1338
1511
  unless $.isArray @opts.toolbar
1339
- @opts.toolbar = ['bold', 'italic', 'underline', '|', 'ol', 'ul', 'blockquote', 'code', '|', 'link', 'image', '|', 'indent', 'outdent']
1512
+ @opts.toolbar = ['bold', 'italic', 'underline', 'strikethrough', '|', 'ol', 'ul', 'blockquote', 'code', '|', 'link', 'image', '|', 'indent', 'outdent']
1340
1513
 
1341
1514
  @_render()
1342
1515
 
@@ -1351,7 +1524,9 @@ class Toolbar extends Plugin
1351
1524
 
1352
1525
  if @opts.toolbarFloat
1353
1526
  @wrapper.width @wrapper.outerWidth()
1354
- @wrapper.css 'left', @wrapper.offset().left
1527
+ unless @editor.util.os.mobile
1528
+ @wrapper.css 'left', @wrapper.offset().left
1529
+ toolbarHeight = @wrapper.outerHeight()
1355
1530
  $(window).on 'scroll.simditor-' + @editor.id, (e) =>
1356
1531
  topEdge = @editor.wrapper.offset().top
1357
1532
  bottomEdge = topEdge + @editor.wrapper.outerHeight() - 80
@@ -1359,8 +1534,16 @@ class Toolbar extends Plugin
1359
1534
 
1360
1535
  if scrollTop <= topEdge or scrollTop >= bottomEdge
1361
1536
  @editor.wrapper.removeClass('toolbar-floating')
1537
+ .css('padding-top', '')
1538
+ if @editor.util.os.mobile
1539
+ @wrapper.css
1540
+ top: 'auto'
1362
1541
  else
1363
1542
  @editor.wrapper.addClass('toolbar-floating')
1543
+ .css('padding-top', toolbarHeight)
1544
+ if @editor.util.os.mobile
1545
+ @wrapper.css
1546
+ top: scrollTop - topEdge
1364
1547
 
1365
1548
  @editor.on 'selectionchanged focus', =>
1366
1549
  @toolbarStatus()
@@ -1387,6 +1570,8 @@ class Toolbar extends Plugin
1387
1570
 
1388
1571
  @buttons.push new @constructor.buttons[name](@editor)
1389
1572
 
1573
+ @editor.placeholderEl.css 'top', @wrapper.outerHeight()
1574
+
1390
1575
  toolbarStatus: (name) ->
1391
1576
  return unless @editor.inputManager.focused
1392
1577
 
@@ -1429,15 +1614,15 @@ class Simditor extends Widget
1429
1614
 
1430
1615
  opts:
1431
1616
  textarea: null
1432
- placeholder: false
1617
+ placeholder: ''
1433
1618
  defaultImage: 'images/image.png'
1434
- params: null
1619
+ params: {}
1435
1620
  upload: false
1436
1621
  tabIndent: true
1437
1622
 
1438
1623
  _init: ->
1439
- @textarea = $(@opts.textarea);
1440
- @opts.placeholder = @opts.placeholder ? @textarea.attr('placeholder')
1624
+ @textarea = $(@opts.textarea)
1625
+ @opts.placeholder = @opts.placeholder || @textarea.attr('placeholder')
1441
1626
 
1442
1627
  unless @textarea.length
1443
1628
  throw new Error 'simditor: param textarea is required.'
@@ -1450,9 +1635,9 @@ class Simditor extends Widget
1450
1635
  @id = ++ Simditor.count
1451
1636
  @_render()
1452
1637
 
1453
- if @opts.upload and Uploader
1638
+ if @opts.upload and simple?.uploader
1454
1639
  uploadOpts = if typeof @opts.upload == 'object' then @opts.upload else {}
1455
- @uploader = new Uploader(uploadOpts)
1640
+ @uploader = simple.uploader(uploadOpts)
1456
1641
 
1457
1642
  form = @textarea.closest 'form'
1458
1643
  if form.length
@@ -1463,20 +1648,16 @@ class Simditor extends Widget
1463
1648
 
1464
1649
  # set default value after all plugins are connected
1465
1650
  @on 'pluginconnected', =>
1466
- @setValue @textarea.val() || ''
1467
-
1468
1651
  if @opts.placeholder
1469
1652
  @on 'valuechanged', =>
1470
1653
  @_placeholder()
1471
1654
 
1472
- setTimeout =>
1473
- @trigger 'valuechanged'
1474
- , 0
1655
+ @setValue @textarea.val() || ''
1475
1656
 
1476
1657
  # Disable the resizing of `img` and `table`
1477
- #if @browser.mozilla
1478
- #document.execCommand "enableObjectResizing", false, "false"
1479
- #document.execCommand "enableInlineTableEditing", false, "false"
1658
+ if @util.browser.mozilla
1659
+ document.execCommand "enableObjectResizing", false, false
1660
+ document.execCommand "enableInlineTableEditing", false, false
1480
1661
 
1481
1662
  _tpl:"""
1482
1663
  <div class="simditor">
@@ -1506,6 +1687,9 @@ class Simditor extends Widget
1506
1687
  else if @util.os.linux
1507
1688
  @el.addClass 'simditor-linux'
1508
1689
 
1690
+ if @util.os.mobile
1691
+ @el.addClass 'simditor-mobile'
1692
+
1509
1693
  if @opts.params
1510
1694
  for key, val of @opts.params
1511
1695
  $('<input/>', {
@@ -1522,16 +1706,22 @@ class Simditor extends Widget
1522
1706
  @placeholderEl.hide()
1523
1707
 
1524
1708
  setValue: (val) ->
1709
+ @hidePopover()
1525
1710
  @textarea.val val
1526
1711
  @body.html val
1527
1712
 
1528
1713
  @formatter.format()
1529
1714
  @formatter.decorate()
1530
1715
 
1716
+ setTimeout =>
1717
+ @trigger 'valuechanged'
1718
+ , 0
1719
+
1531
1720
  getValue: () ->
1532
1721
  @sync()
1533
1722
 
1534
1723
  sync: ->
1724
+ @hidePopover
1535
1725
  cloneBody = @body.clone()
1536
1726
  @formatter.undecorate cloneBody
1537
1727
  @formatter.format cloneBody
@@ -1539,23 +1729,35 @@ class Simditor extends Widget
1539
1729
  # generate `a` tag automatically
1540
1730
  @formatter.autolink cloneBody
1541
1731
 
1542
- # remove empty `p` tag at the end of content
1543
- lastP = cloneBody.children().last 'p'
1544
- while lastP.is('p') and !lastP.text() and !lastP.find('img').length
1732
+ # remove empty `p` tag at the start/end of content
1733
+ children = cloneBody.children()
1734
+ lastP = children.last 'p'
1735
+ firstP = children.first 'p'
1736
+ while lastP.is('p') and @util.isEmptyNode(lastP)
1545
1737
  emptyP = lastP
1546
1738
  lastP = lastP.prev 'p'
1547
1739
  emptyP.remove()
1740
+ while firstP.is('p') and @util.isEmptyNode(firstP)
1741
+ emptyP = firstP
1742
+ firstP = lastP.next 'p'
1743
+ emptyP.remove()
1744
+
1745
+ # remove images being uploaded
1746
+ cloneBody.find('img.uploading').remove()
1548
1747
 
1549
1748
  val = $.trim(cloneBody.html())
1550
1749
  @textarea.val val
1551
1750
  val
1552
1751
 
1553
1752
  focus: ->
1554
- $blockEl = @body.find('p, li, pre, h1, h2, h3, h4, td').first()
1555
- return unless $blockEl.length > 0
1556
- range = document.createRange()
1557
- @selection.setRangeAtStartOf $blockEl, range
1558
- @body.focus()
1753
+ if @inputManager.lastCaretPosition
1754
+ @undoManager.caretPosition @inputManager.lastCaretPosition
1755
+ else
1756
+ $blockEl = @body.find('p, li, pre, h1, h2, h3, h4, td').first()
1757
+ return unless $blockEl.length > 0
1758
+ range = document.createRange()
1759
+ @selection.setRangeAtStartOf $blockEl, range
1760
+ @body.focus()
1559
1761
 
1560
1762
  blur: ->
1561
1763
  @body.blur()
@@ -1587,11 +1789,6 @@ class Simditor extends Widget
1587
1789
  window.Simditor = Simditor
1588
1790
 
1589
1791
 
1590
- class TestPlugin extends Plugin
1591
-
1592
- class Test extends Widget
1593
- @connect TestPlugin
1594
-
1595
1792
 
1596
1793
  class Button extends Module
1597
1794
 
@@ -1628,13 +1825,25 @@ class Button extends Module
1628
1825
 
1629
1826
  @el.on 'mousedown', (e) =>
1630
1827
  e.preventDefault()
1828
+ return false if @el.hasClass('disabled') or (@needFocus and !@editor.inputManager.focused)
1829
+
1631
1830
  if @menu
1632
1831
  @wrapper.toggleClass('menu-on')
1633
1832
  .siblings('li')
1634
1833
  .removeClass('menu-on')
1635
- return false
1636
1834
 
1637
- return false if @el.hasClass('disabled') or (@needFocus and !@editor.inputManager.focused)
1835
+ if @wrapper.is('.menu-on')
1836
+ exceed = @menuWrapper.offset().left + @menuWrapper.outerWidth() + 5 -
1837
+ @editor.wrapper.offset().left - @editor.wrapper.outerWidth()
1838
+
1839
+ if exceed > 0
1840
+ @menuWrapper.css
1841
+ 'left': 'auto'
1842
+ 'right': 0
1843
+
1844
+ @trigger 'menuexpand'
1845
+
1846
+ return false
1638
1847
 
1639
1848
  param = @el.data('param')
1640
1849
  @command(param)
@@ -1662,6 +1871,7 @@ class Button extends Module
1662
1871
  if @shortcut?
1663
1872
  @editor.inputManager.addShortcut @shortcut, (e) =>
1664
1873
  @el.mousedown()
1874
+ false
1665
1875
 
1666
1876
  for tag in @htmlTag.split ','
1667
1877
  tag = $.trim tag
@@ -1742,8 +1952,8 @@ class Popover extends Module
1742
1952
  .data('popover', @)
1743
1953
  @render()
1744
1954
 
1745
- @editor.on 'blur.linkpopover', =>
1746
- @target.addClass('selected') if @active and @target?
1955
+ #@editor.on 'blur.popover', =>
1956
+ #@target.addClass('selected') if @active and @target?
1747
1957
 
1748
1958
  @el.on 'mouseenter', (e) =>
1749
1959
  @el.addClass 'hover'
@@ -1754,11 +1964,9 @@ class Popover extends Module
1754
1964
 
1755
1965
  show: ($target, position = 'bottom') ->
1756
1966
  return unless $target?
1757
- @target = $target
1967
+ @editor.hidePopover()
1758
1968
 
1759
- @el.siblings('.simditor-popover').each (i, el) =>
1760
- popover = $(el).data('popover')
1761
- popover.hide()
1969
+ @target = $target.addClass('selected')
1762
1970
 
1763
1971
  if @active
1764
1972
  @refresh(position)
@@ -1784,6 +1992,7 @@ class Popover extends Module
1784
1992
  @trigger 'popoverhide'
1785
1993
 
1786
1994
  refresh: (position = 'bottom') ->
1995
+ return unless @active
1787
1996
  wrapperOffset = @editor.wrapper.offset()
1788
1997
  targetOffset = @target.offset()
1789
1998
  targetH = @target.outerHeight()
@@ -1807,11 +2016,14 @@ class Popover extends Module
1807
2016
  @el.remove()
1808
2017
 
1809
2018
 
2019
+ window.SimditorPopover = Popover
2020
+
2021
+
1810
2022
  class TitleButton extends Button
1811
2023
 
1812
2024
  name: 'title'
1813
2025
 
1814
- title: '加粗文字'
2026
+ title: '标题文字'
1815
2027
 
1816
2028
  htmlTag: 'h1, h2, h3, h4'
1817
2029
 
@@ -1907,6 +2119,14 @@ class BoldButton extends Button
1907
2119
 
1908
2120
  shortcut: 'cmd+66'
1909
2121
 
2122
+ render: ->
2123
+ if @editor.util.os.mac
2124
+ @title = @title + ' ( Cmd + b )'
2125
+ else
2126
+ @title = @title + ' ( Ctrl + b )'
2127
+ @shortcut = 'ctrl+66'
2128
+ super()
2129
+
1910
2130
  status: ($node) ->
1911
2131
  @setDisabled $node.is(@disableTag) if $node?
1912
2132
  return true if @disabled
@@ -1938,6 +2158,15 @@ class ItalicButton extends Button
1938
2158
 
1939
2159
  shortcut: 'cmd+73'
1940
2160
 
2161
+ render: ->
2162
+ if @editor.util.os.mac
2163
+ @title = @title + ' ( Cmd + i )'
2164
+ else
2165
+ @title = @title + ' ( Ctrl + i )'
2166
+ @shortcut = 'ctrl+73'
2167
+
2168
+ super()
2169
+
1941
2170
  status: ($node) ->
1942
2171
  @setDisabled $node.is(@disableTag) if $node?
1943
2172
  return @disabled if @disabled
@@ -1970,6 +2199,14 @@ class UnderlineButton extends Button
1970
2199
 
1971
2200
  shortcut: 'cmd+85'
1972
2201
 
2202
+ render: ->
2203
+ if @editor.util.os.mac
2204
+ @title = @title + ' ( Cmd + u )'
2205
+ else
2206
+ @title = @title + ' ( Ctrl + u )'
2207
+ @shortcut = 'ctrl+85'
2208
+ super()
2209
+
1973
2210
  status: ($node) ->
1974
2211
  @setDisabled $node.is(@disableTag) if $node?
1975
2212
  return @disabled if @disabled
@@ -1989,6 +2226,80 @@ Simditor.Toolbar.addButton(UnderlineButton)
1989
2226
 
1990
2227
 
1991
2228
 
2229
+ class ColorButton extends Button
2230
+
2231
+ name: 'color'
2232
+
2233
+ icon: 'font'
2234
+
2235
+ title: '文字颜色'
2236
+
2237
+ disableTag: 'pre'
2238
+
2239
+ menu: true
2240
+
2241
+ render: (args...) ->
2242
+ super args...
2243
+
2244
+ renderMenu: ->
2245
+ $('''
2246
+ <ul class="color-list">
2247
+ <li><a href="javascript:;" class="font-color font-color-1" data-color=""></a></li>
2248
+ <li><a href="javascript:;" class="font-color font-color-2" data-color=""></a></li>
2249
+ <li><a href="javascript:;" class="font-color font-color-3" data-color=""></a></li>
2250
+ <li><a href="javascript:;" class="font-color font-color-4" data-color=""></a></li>
2251
+ <li><a href="javascript:;" class="font-color font-color-5" data-color=""></a></li>
2252
+ <li><a href="javascript:;" class="font-color font-color-6" data-color=""></a></li>
2253
+ <li><a href="javascript:;" class="font-color font-color-7" data-color=""></a></li>
2254
+ <li><a href="javascript:;" class="font-color font-color-8" data-color=""></a></li>
2255
+ <li class="remove-color"><a href="javascript:;" class="link-remove-color">去掉颜色</a></li>
2256
+ </ul>
2257
+ ''').appendTo(@menuWrapper)
2258
+
2259
+ @menuWrapper.on 'mousedown', '.color-list', (e) ->
2260
+ false
2261
+
2262
+ @menuWrapper.on 'click', '.font-color', (e) =>
2263
+ @wrapper.removeClass('menu-on')
2264
+ $link = $(e.currentTarget)
2265
+ rgb = window.getComputedStyle($link[0], null).getPropertyValue('background-color')
2266
+ hex = @_convertRgbToHex rgb
2267
+ return unless hex
2268
+ document.execCommand 'foreColor', false, hex
2269
+ @editor.trigger 'valuechanged'
2270
+ @editor.trigger 'selectionchanged'
2271
+
2272
+ @menuWrapper.on 'click', '.link-remove-color', (e) =>
2273
+ @wrapper.removeClass('menu-on')
2274
+ $p = @editor.body.find 'p'
2275
+ return unless $p.length > 0
2276
+
2277
+ rgb = window.getComputedStyle($p[0], null).getPropertyValue('color')
2278
+ hex = @_convertRgbToHex rgb
2279
+ return unless hex
2280
+
2281
+ document.execCommand 'foreColor', false, hex
2282
+ @editor.trigger 'valuechanged'
2283
+ @editor.trigger 'selectionchanged'
2284
+
2285
+ _convertRgbToHex:(rgb) ->
2286
+ re = /rgb\((\d+),\s?(\d+),\s?(\d+)\)/g
2287
+ match = re.exec rgb
2288
+ return '' unless match
2289
+
2290
+ rgbToHex = (r, g, b) ->
2291
+ componentToHex = (c) ->
2292
+ hex = c.toString(16)
2293
+ if hex.length == 1 then '0' + hex else hex
2294
+ "#" + componentToHex(r) + componentToHex(g) + componentToHex(b)
2295
+
2296
+ rgbToHex match[1] * 1, match[2] * 1, match[3] * 1
2297
+
2298
+
2299
+ Simditor.Toolbar.addButton(ColorButton)
2300
+
2301
+
2302
+
1992
2303
  class ListButton extends Button
1993
2304
 
1994
2305
  type: ''
@@ -2047,20 +2358,6 @@ class ListButton extends Button
2047
2358
 
2048
2359
  $contents = $(range.extractContents())
2049
2360
 
2050
- #if $breakedEl?
2051
- #$contents.wrapInner('<' + $breakedEl[0].tagName + '/>')
2052
- #if @editor.selection.rangeAtStartOf $breakedEl, range
2053
- #range.setEndBefore($breakedEl[0])
2054
- #range.collapse(false)
2055
- #$breakedEl.remove() if $breakedEl.children().length < 1
2056
- #else if @editor.selection.rangeAtEndOf $breakedEl, range
2057
- #range.setEndAfter($breakedEl[0])
2058
- #range.collapse(false)
2059
- #else
2060
- #$breakedEl = @editor.selection.breakBlockEl($breakedEl, range)
2061
- #range.setEndBefore($breakedEl[0])
2062
- #range.collapse(false)
2063
-
2064
2361
  results = []
2065
2362
  $contents.children().each (i, el) =>
2066
2363
  converted = @_convertEl el
@@ -2080,7 +2377,7 @@ class ListButton extends Button
2080
2377
  $el = $(el)
2081
2378
  results = []
2082
2379
  anotherType = if @type == 'ul' then 'ol' else 'ul'
2083
-
2380
+
2084
2381
  if $el.is @type
2085
2382
  $el.children('li').each (i, li) =>
2086
2383
  $li = $(li)
@@ -2110,6 +2407,14 @@ class OrderListButton extends ListButton
2110
2407
  title: '有序列表'
2111
2408
  icon: 'list-ol'
2112
2409
  htmlTag: 'ol'
2410
+ shortcut: 'cmd+191'
2411
+ render: ->
2412
+ if @editor.util.os.mac
2413
+ @title = @title + ' ( Cmd + / )'
2414
+ else
2415
+ @title = @title + ' ( ctrl + / )'
2416
+ @shortcut = 'ctrl+191'
2417
+ super()
2113
2418
 
2114
2419
  class UnorderListButton extends ListButton
2115
2420
  type: 'ul'
@@ -2117,6 +2422,14 @@ class UnorderListButton extends ListButton
2117
2422
  title: '无序列表'
2118
2423
  icon: 'list-ul'
2119
2424
  htmlTag: 'ul'
2425
+ shortcut: 'cmd+190'
2426
+ render: ->
2427
+ if @editor.util.os.mac
2428
+ @title = @title + ' ( Cmd + . )'
2429
+ else
2430
+ @title = @title + ' ( Ctrl + . )'
2431
+ @shortcut = 'ctrl+190'
2432
+ super()
2120
2433
 
2121
2434
  Simditor.Toolbar.addButton(OrderListButton)
2122
2435
  Simditor.Toolbar.addButton(UnorderListButton)
@@ -2195,6 +2508,18 @@ class CodeButton extends Button
2195
2508
 
2196
2509
  disableTag: 'li, table'
2197
2510
 
2511
+
2512
+ constructor: (@editor) ->
2513
+ super @editor
2514
+
2515
+ @editor.on 'decorate', (e, $el) =>
2516
+ $el.find('pre').each (i, pre) =>
2517
+ @decorate $(pre)
2518
+
2519
+ @editor.on 'undecorate', (e, $el) =>
2520
+ $el.find('pre').each (i, pre) =>
2521
+ @undecorate $(pre)
2522
+
2198
2523
  render: (args...) ->
2199
2524
  super args...
2200
2525
  @popover = new CodePopover(@editor)
@@ -2209,6 +2534,16 @@ class CodeButton extends Button
2209
2534
 
2210
2535
  result
2211
2536
 
2537
+ decorate: ($pre) ->
2538
+ lang = $pre.attr('data-lang')
2539
+ $pre.removeClass()
2540
+ $pre.addClass('lang-' + lang) if lang and lang != -1
2541
+
2542
+ undecorate: ($pre) ->
2543
+ lang = $pre.attr('data-lang')
2544
+ $pre.removeClass()
2545
+ $pre.addClass('lang-' + lang) if lang and lang != -1
2546
+
2212
2547
  command: ->
2213
2548
  range = @editor.selection.getRange()
2214
2549
  startNode = range.startContainer
@@ -2248,7 +2583,7 @@ class CodeButton extends Button
2248
2583
  codeStr = '\n'
2249
2584
  else
2250
2585
  codeStr = @editor.formatter.clearHtml($el)
2251
- block = $('<' + @htmlTag + '/>').append(codeStr)
2586
+ block = $('<' + @htmlTag + '/>').text(codeStr)
2252
2587
  results.push(block)
2253
2588
 
2254
2589
  results
@@ -2286,14 +2621,16 @@ class CodePopover extends Popover
2286
2621
  @selectEl = @el.find '.select-lang'
2287
2622
 
2288
2623
  @selectEl.on 'change', (e) =>
2289
- lang = @selectEl.val()
2290
- oldLang = @target.attr('data-lang')
2291
- @target.removeClass('lang-' + oldLang)
2624
+ @lang = @selectEl.val()
2625
+ selected = @target.hasClass('selected')
2626
+ @target.removeClass()
2292
2627
  .removeAttr('data-lang')
2293
2628
 
2294
2629
  if @lang isnt -1
2295
- @target.addClass('lang-' + lang)
2296
- @target.attr('data-lang', lang)
2630
+ @target.addClass('lang-' + @lang)
2631
+ @target.attr('data-lang', @lang)
2632
+
2633
+ @target.addClass('selected') if selected
2297
2634
 
2298
2635
  show: (args...) ->
2299
2636
  super args...
@@ -2375,16 +2712,15 @@ class LinkButton extends Button
2375
2712
 
2376
2713
  range.selectNodeContents $link[0]
2377
2714
 
2378
- @editor.selection.selectRange range
2379
-
2380
- @popover.one 'popovershow', =>
2381
- if linkText
2382
- @popover.urlEl.focus()
2383
- @popover.urlEl[0].select()
2384
- else
2385
- @popover.textEl.focus()
2386
- @popover.textEl[0].select()
2715
+ @popover.one 'popovershow', =>
2716
+ if linkText
2717
+ @popover.urlEl.focus()
2718
+ @popover.urlEl[0].select()
2719
+ else
2720
+ @popover.textEl.focus()
2721
+ @popover.textEl[0].select()
2387
2722
 
2723
+ @editor.selection.selectRange range
2388
2724
  @editor.trigger 'valuechanged'
2389
2725
  @editor.trigger 'selectionchanged'
2390
2726
 
@@ -2426,9 +2762,9 @@ class LinkPopover extends Popover
2426
2762
  setTimeout =>
2427
2763
  range = document.createRange()
2428
2764
  @editor.selection.setRangeAfter @target, range
2429
- @editor.body.focus() if @editor.util.browser.firefox
2430
2765
  @hide()
2431
2766
  @editor.trigger 'valuechanged'
2767
+ @editor.trigger 'selectionchanged'
2432
2768
  , 0
2433
2769
 
2434
2770
  @unlinkEl.on 'click', (e) =>
@@ -2438,7 +2774,8 @@ class LinkPopover extends Popover
2438
2774
 
2439
2775
  range = document.createRange()
2440
2776
  @editor.selection.setRangeAfter txtNode, range
2441
- @editor.body.focus() if @editor.util.browser.firefox and !@editor.inputManager.focused
2777
+ @editor.trigger 'valuechanged'
2778
+ @editor.trigger 'selectionchanged'
2442
2779
 
2443
2780
  show: (args...) ->
2444
2781
  super args...
@@ -2453,14 +2790,6 @@ Simditor.Toolbar.addButton(LinkButton)
2453
2790
 
2454
2791
  class ImageButton extends Button
2455
2792
 
2456
- _wrapperTpl: """
2457
- <div class="simditor-image" contenteditable="false" tabindex="-1">
2458
- <div class="simditor-image-resize-handle right"></div>
2459
- <div class="simditor-image-resize-handle bottom"></div>
2460
- <div class="simditor-image-resize-handle right-bottom"></div>
2461
- </div>
2462
- """
2463
-
2464
2793
  name: 'image'
2465
2794
 
2466
2795
  icon: 'picture-o'
@@ -2473,9 +2802,11 @@ class ImageButton extends Button
2473
2802
 
2474
2803
  defaultImage: ''
2475
2804
 
2476
- maxWidth: 0
2805
+ needFocus: false
2806
+
2807
+ #maxWidth: 0
2477
2808
 
2478
- maxHeight: 0
2809
+ #maxHeight: 0
2479
2810
 
2480
2811
  menu: [{
2481
2812
  name: 'upload-image',
@@ -2490,54 +2821,53 @@ class ImageButton extends Button
2490
2821
  super @editor
2491
2822
 
2492
2823
  @defaultImage = @editor.opts.defaultImage
2493
- @maxWidth = @editor.opts.maxImageWidth || @editor.body.width()
2494
- @maxHeight = @editor.opts.maxImageHeight || $(window).height()
2824
+ #@maxWidth = @editor.opts.maxImageWidth || @editor.body.width()
2825
+ #@maxHeight = @editor.opts.maxImageHeight || $(window).height()
2495
2826
 
2496
- @editor.on 'decorate', (e, $el) =>
2497
- $el.find('img').each (i, img) =>
2498
- @decorate $(img)
2499
-
2500
- @editor.on 'undecorate', (e, $el) =>
2501
- $el.find('img').each (i, img) =>
2502
- @undecorate $(img)
2827
+ @editor.body.on 'click', 'img:not([data-non-image])', (e) =>
2828
+ $img = $(e.currentTarget)
2503
2829
 
2504
- @editor.body.on 'mousedown', '.simditor-image', (e) =>
2505
- $imgWrapper = $(e.currentTarget)
2506
-
2507
- if $imgWrapper.hasClass 'selected'
2508
- @popover.srcEl.blur()
2509
- @popover.hide()
2510
- $imgWrapper.removeClass('selected')
2511
- else
2512
- @editor.body.blur()
2513
- @editor.body.find('.simditor-image').removeClass('selected')
2514
- $imgWrapper.addClass('selected').focus()
2515
- $img = $imgWrapper.find('img')
2516
- $imgWrapper.width $img.width()
2517
- $imgWrapper.height $img.height()
2518
- @popover.show $imgWrapper
2830
+ #@popover.show $img
2831
+ range = document.createRange()
2832
+ range.selectNode $img[0]
2833
+ @editor.selection.selectRange range
2834
+ setTimeout =>
2835
+ @editor.body.focus()
2836
+ @editor.trigger 'selectionchanged'
2837
+ , 0
2519
2838
 
2520
2839
  false
2521
2840
 
2522
- @editor.body.on 'click', '.simditor-image', (e) =>
2523
- false
2841
+ @editor.body.on 'mouseup', 'img:not([data-non-image])', (e) =>
2842
+ return false
2843
+
2524
2844
 
2525
2845
  @editor.on 'selectionchanged.image', =>
2526
- range = @editor.selection.getRange()
2846
+ range = @editor.selection.sel.getRangeAt(0)
2527
2847
  return unless range?
2528
- $container = $(range.commonAncestorContainer)
2529
2848
 
2530
- if range.collapsed and $container.is('.simditor-image')
2531
- $container.mousedown()
2532
- else if @popover.active
2533
- @popover.hide() if @popover.active
2849
+ $contents = $(range.cloneContents()).contents()
2850
+ if $contents.length == 1 and $contents.is('img:not([data-non-image])')
2851
+ $img = $(range.startContainer).contents().eq(range.startOffset)
2852
+ @popover.show $img
2853
+ else
2854
+ @popover.hide()
2855
+
2856
+ @editor.on 'valuechanged.image', =>
2857
+ $masks = @editor.wrapper.find('.simditor-image-loading')
2858
+ return unless $masks.length > 0
2859
+ $masks.each (i, mask) =>
2860
+ $mask = $(mask)
2861
+ $img = $mask.data 'img'
2862
+ unless $img and $img.parent().length > 0
2863
+ $mask.remove()
2864
+ if $img
2865
+ file = $img.data 'file'
2866
+ if file
2867
+ @editor.uploader.cancel file
2868
+ if @editor.body.find('img.uploading').length < 1
2869
+ @editor.uploader.trigger 'uploadready', [file]
2534
2870
 
2535
- @editor.body.on 'keydown', '.simditor-image', (e) =>
2536
- return unless e.which == 8
2537
- @popover.hide()
2538
- $(e.currentTarget).remove()
2539
- @editor.trigger 'valuechanged'
2540
- return false
2541
2871
 
2542
2872
  render: (args...) ->
2543
2873
  super args...
@@ -2547,25 +2877,31 @@ class ImageButton extends Button
2547
2877
  super()
2548
2878
 
2549
2879
  $uploadItem = @menuEl.find('.menu-item-upload-image')
2550
- $input = $('<input type="file" title="上传图片" name="upload_file" accept="image/*">')
2551
- .appendTo($uploadItem)
2880
+ $input = null
2881
+
2882
+ createInput = =>
2883
+ $input.remove() if $input
2884
+ $input = $('<input type="file" title="上传图片" accept="image/*">')
2885
+ .appendTo($uploadItem)
2886
+
2887
+ createInput()
2552
2888
 
2553
- $input.on 'click mousedown', (e) =>
2889
+ $uploadItem.on 'click mousedown', 'input[type=file]', (e) =>
2554
2890
  e.stopPropagation()
2555
2891
 
2556
- $input.on 'change', (e) =>
2892
+ $uploadItem.on 'change', 'input[type=file]', (e) =>
2557
2893
  if @editor.inputManager.focused
2558
2894
  @editor.uploader.upload($input, {
2559
2895
  inline: true
2560
2896
  })
2561
- $input.val ''
2562
- else if @editor.inputManager.lastCaretPosition
2897
+ createInput()
2898
+ else
2563
2899
  @editor.one 'focus', (e) =>
2564
2900
  @editor.uploader.upload($input, {
2565
2901
  inline: true
2566
2902
  })
2567
- $input = $input.clone(true).replaceAll($input)
2568
- @editor.undoManager.caretPosition @editor.inputManager.lastCaretPosition
2903
+ createInput()
2904
+ @editor.focus()
2569
2905
  @wrapper.removeClass('menu-on')
2570
2906
 
2571
2907
  @_initUploader()
@@ -2578,49 +2914,71 @@ class ImageButton extends Button
2578
2914
  @editor.uploader.on 'beforeupload', (e, file) =>
2579
2915
  return unless file.inline
2580
2916
 
2581
- if file.imgWrapper
2582
- $img = file.imgWrapper.find("img")
2917
+ if file.img
2918
+ $img = $(file.img)
2583
2919
  else
2584
- $img = @createImage()
2585
- $img.mousedown()
2586
- file.imgWrapper = $img.parent '.simditor-image'
2920
+ $img = @createImage(file.name)
2921
+ #$img.click()
2922
+ file.img = $img
2923
+
2924
+ $img.addClass 'uploading'
2925
+ $img.data 'file', file
2587
2926
 
2588
2927
  @editor.uploader.readImageFile file.obj, (img) =>
2589
- prepare = () =>
2928
+ return unless $img.hasClass('uploading')
2929
+ src = if img then img.src else @defaultImage
2930
+
2931
+ @loadImage $img, src, () =>
2932
+ @popover.refresh()
2590
2933
  @popover.srcEl.val('正在上传...')
2591
- file.imgWrapper.append '<div class="mask"></div>'
2592
- $bar = $('<div class="simditor-image-progress-bar"><div><span></span></div></div>').appendTo file.imgWrapper
2593
- $bar.text('正在上传').addClass('hint') unless @editor.uploader.html5
2594
-
2595
- if img
2596
- @loadImage $img, img.src, () =>
2597
- @popover.refresh()
2598
- prepare()
2599
- else
2600
- prepare()
2934
+ .prop('disabled', true)
2601
2935
 
2602
2936
  @editor.uploader.on 'uploadprogress', (e, file, loaded, total) =>
2603
2937
  return unless file.inline
2604
2938
 
2605
2939
  percent = loaded / total
2606
-
2607
- if percent > 0.99
2608
- percent = "正在处理";
2609
- file.imgWrapper.find(".simditor-image-progress-bar").text(percent).addClass('hint')
2610
- else
2611
- percent = (percent * 100).toFixed(0) + "%"
2612
- file.imgWrapper.find(".simditor-image-progress-bar span").width(percent)
2940
+ percent = (percent * 100).toFixed(0)
2941
+ percent = 99 if percent > 99
2942
+
2943
+ $mask = file.img.data('mask')
2944
+ if $mask
2945
+ $img = $mask.data('img')
2946
+ if $img and $img.parent().length > 0
2947
+ $mask.find("span").text(percent)
2948
+ else
2949
+ $mask.remove()
2613
2950
 
2614
2951
  @editor.uploader.on 'uploadsuccess', (e, file, result) =>
2615
2952
  return unless file.inline
2616
2953
 
2617
- $img = file.imgWrapper.find("img")
2618
- @loadImage $img, result.file_path, () =>
2619
- file.imgWrapper.find(".mask, .simditor-image-progress-bar").remove()
2620
- @popover.srcEl.val result.file_path
2621
- @editor.trigger 'valuechanged'
2954
+ $img = file.img
2955
+ $img.removeData 'file'
2956
+ $img.removeClass 'uploading'
2957
+
2958
+ $mask = $img.data('mask')
2959
+ $mask.remove() if $mask
2960
+ $img.removeData 'mask'
2961
+
2962
+ if result.success == false
2963
+ msg = result.msg || '上传被拒绝了'
2964
+ if simple? and simple.message?
2965
+ simple.message
2966
+ content: msg
2967
+ else
2968
+ alert msg
2969
+ $img.attr 'src', @defaultImage
2970
+ else
2971
+ $img.attr 'src', result.file_path
2972
+
2973
+ @popover.srcEl.prop('disabled', false)
2974
+
2975
+ @editor.trigger 'valuechanged'
2976
+ if @editor.body.find('img.uploading').length < 1
2977
+ @editor.uploader.trigger 'uploadready', [file, result]
2978
+
2622
2979
 
2623
2980
  @editor.uploader.on 'uploaderror', (e, file, xhr) =>
2981
+ return unless file.inline
2624
2982
  return if xhr.statusText == 'abort'
2625
2983
 
2626
2984
  if xhr.responseText
@@ -2631,109 +2989,113 @@ class ImageButton extends Button
2631
2989
  msg = '上传出错了'
2632
2990
 
2633
2991
  if simple? and simple.message?
2634
- simple.message(msg)
2992
+ simple.message
2993
+ content: msg
2635
2994
  else
2636
- alert(msg)
2995
+ alert msg
2637
2996
 
2638
- $img = file.imgWrapper.find("img")
2639
- @loadImage $img, @defaultImage, =>
2640
- @popover.refresh()
2641
- @popover.srcEl.val $img.attr('src')
2642
- file.imgWrapper.find(".mask, .simditor-image-progress-bar").remove()
2643
- @editor.trigger 'valuechanged'
2997
+ $img = file.img
2998
+ $img.removeData 'file'
2999
+ $img.removeClass 'uploading'
2644
3000
 
2645
- status: ($node) ->
2646
- @setDisabled $node.is(@disableTag) if $node?
2647
- return true if @disabled
3001
+ $mask = $img.data('mask')
3002
+ $mask.remove() if $mask
3003
+ $img.removeData 'mask'
2648
3004
 
2649
- decorate: ($img) ->
2650
- $wrapper = $img.parent('.simditor-image')
2651
- return if $wrapper.length > 0
3005
+ $img.attr 'src', @defaultImage
3006
+ @popover.srcEl.prop('disabled', false)
2652
3007
 
2653
- $wrapper = $(@_wrapperTpl)
2654
- .insertBefore($img)
2655
- .prepend($img)
3008
+ @editor.trigger 'valuechanged'
3009
+ if @editor.body.find('img.uploading').length < 1
3010
+ @editor.uploader.trigger 'uploadready', [file, result]
2656
3011
 
2657
- undecorate: ($img) ->
2658
- $wrapper = $img.parent('.simditor-image')
2659
- return if $wrapper.length < 1
2660
3012
 
2661
- $img.insertAfter $wrapper unless $img.is('img[src^="data:image/png;base64"]')
2662
- $wrapper.remove()
3013
+ status: ($node) ->
3014
+ @setDisabled $node.is(@disableTag) if $node?
3015
+ return true if @disabled
2663
3016
 
2664
3017
  loadImage: ($img, src, callback) ->
2665
- $wrapper = $img.parent('.simditor-image')
3018
+ $mask = $img.data('mask')
3019
+ if !$mask
3020
+ $mask = $('<div class="simditor-image-loading"><span></span></div>')
3021
+ .appendTo(@editor.wrapper)
3022
+ $mask.addClass('uploading') if $img.hasClass('uploading') and @editor.uploader.html5
3023
+ $img.data('mask', $mask)
3024
+ $mask.data('img', $img)
3025
+
3026
+ imgPosition = $img.position()
3027
+ toolbarH = @editor.toolbar.wrapper.outerHeight()
3028
+ $mask.css({
3029
+ top: imgPosition.top + toolbarH,
3030
+ left: imgPosition.left,
3031
+ width: $img.width(),
3032
+ height: $img.height()
3033
+ })
3034
+
2666
3035
  img = new Image()
2667
3036
 
2668
3037
  img.onload = =>
2669
3038
  width = img.width
2670
3039
  height = img.height
2671
- if width > @maxWidth
2672
- height = @maxWidth * height / width
2673
- width = @maxWidth
2674
- if height > @maxHeight
2675
- width = @maxHeight * width / height
2676
- height = @maxHeight
3040
+ #if width > @maxWidth
3041
+ #height = @maxWidth * height / width
3042
+ #width = @maxWidth
3043
+ #if height > @maxHeight
3044
+ #width = @maxHeight * width / height
3045
+ #height = @maxHeight
2677
3046
 
2678
3047
  $img.attr({
2679
3048
  src: src,
2680
- width: width,
2681
- 'data-origin-src': src,
2682
- 'data-origin-name': '图片',
2683
- 'data-origin-size': img.width + ',' + img.height
3049
+ #width: width,
3050
+ #height: height,
3051
+ 'data-image-size': img.width + ',' + img.height
2684
3052
  })
2685
3053
 
2686
- $wrapper.width(width)
2687
- .height(height)
3054
+ if $img.hasClass 'uploading' # img being uploaded
3055
+ $mask.css({
3056
+ width: $img.width(),
3057
+ height: $img.height()
3058
+ })
3059
+ else
3060
+ $mask.remove()
3061
+ $img.removeData('mask')
2688
3062
 
2689
- callback(true)
3063
+ callback(img)
2690
3064
 
2691
3065
  img.onerror = =>
2692
3066
  callback(false)
3067
+ $mask.remove()
3068
+ $img.removeData('mask')
2693
3069
 
2694
3070
  img.src = src
2695
3071
 
2696
- createImage: () ->
3072
+ createImage: (name = 'Image') ->
3073
+ @editor.focus() unless @editor.inputManager.focused
2697
3074
  range = @editor.selection.getRange()
2698
- startNode = range.startContainer
2699
- endNode = range.endContainer
2700
- $startBlock = @editor.util.closestBlockEl(startNode)
2701
- $endBlock = @editor.util.closestBlockEl(endNode)
2702
-
2703
3075
  range.deleteContents()
2704
3076
 
2705
- if $startBlock[0] == $endBlock[0]
2706
- if $startBlock.is 'li'
2707
- $startBlock = @editor.util.furthestNode($startBlock, 'ul, ol')
2708
- $endBlock = $startBlock
2709
- range.setEndAfter($startBlock[0])
2710
- range.collapse(false)
2711
- else if $startBlock.is 'p'
2712
- if @editor.util.isEmptyNode $startBlock
2713
- range.selectNode $startBlock[0]
2714
- range.deleteContents()
2715
- else if @editor.selection.rangeAtEndOf $startBlock, range
2716
- range.setEndAfter($startBlock[0])
2717
- range.collapse(false)
2718
- else if @editor.selection.rangeAtStartOf $startBlock, range
2719
- range.setEndBefore($startBlock[0])
2720
- range.collapse(false)
2721
- else
2722
- $breakedEl = @editor.selection.breakBlockEl($startBlock, range)
2723
- range.setEndBefore($breakedEl[0])
2724
- range.collapse(false)
3077
+ $block = @editor.util.closestBlockEl()
3078
+ if $block.is('p') and !@editor.util.isEmptyNode $block
3079
+ $block = $('<p/>').append(@editor.util.phBr).insertAfter($block)
3080
+ @editor.selection.setRangeAtStartOf $block, range
2725
3081
 
2726
- $img = $('<img/>')
3082
+ $img = $('<img/>').attr('alt', name)
2727
3083
  range.insertNode $img[0]
2728
- @decorate $img
3084
+
3085
+ $nextBlock = $block.next 'p'
3086
+ unless $nextBlock.length > 0
3087
+ $nextBlock = $('<p/>').append(@editor.util.phBr).insertAfter($block)
3088
+ @editor.selection.setRangeAtStartOf $nextBlock
3089
+
2729
3090
  $img
2730
3091
 
2731
- command: () ->
3092
+ command: (src) ->
2732
3093
  $img = @createImage()
2733
3094
 
2734
- @loadImage $img, @defaultImage, =>
3095
+ @loadImage $img, src or @defaultImage, =>
2735
3096
  @editor.trigger 'valuechanged'
2736
- $img.mousedown()
3097
+ $img[0].offsetHeight
3098
+ $img.click()
2737
3099
 
2738
3100
  @popover.one 'popovershow', =>
2739
3101
  @popover.srcEl.focus()
@@ -2749,7 +3111,6 @@ class ImagePopover extends Popover
2749
3111
  <input class="image-src" type="text"/>
2750
3112
  <a class="btn-upload" href="javascript:;" title="上传图片" tabindex="-1">
2751
3113
  <span class="fa fa-upload"></span>
2752
- <input type="file" title="上传图片" name="upload_file" accept="image/*">
2753
3114
  </a>
2754
3115
  </div>
2755
3116
  </div>
@@ -2767,59 +3128,63 @@ class ImagePopover extends Popover
2767
3128
  .append(@_tpl)
2768
3129
  @srcEl = @el.find '.image-src'
2769
3130
 
2770
- @srcEl.on 'keyup', (e) =>
2771
- return if e.which == 13
2772
- clearTimeout @timer if @timer
2773
-
2774
- @timer = setTimeout =>
2775
- src = @srcEl.val()
2776
- $img = @target.find('img')
2777
- @button.loadImage $img, src, (success) =>
2778
- return unless success
2779
- @refresh()
2780
- @editor.trigger 'valuechanged'
2781
-
2782
- @timer = null
2783
- , 200
2784
-
2785
3131
  @srcEl.on 'keydown', (e) =>
2786
3132
  if e.which == 13 or e.which == 27 or e.which == 9
2787
3133
  e.preventDefault()
2788
- @srcEl.blur()
2789
- @target.removeClass('selected')
2790
- @hide()
3134
+
3135
+ if e.which == 13 and !@target.hasClass('uploading')
3136
+ src = @srcEl.val()
3137
+ @button.loadImage @target, src, (success) =>
3138
+ return unless success
3139
+ @button.editor.body.focus()
3140
+ @button.editor.selection.setRangeAfter @target
3141
+ @hide()
3142
+ @editor.trigger 'valuechanged'
3143
+ else
3144
+ @button.editor.body.focus()
3145
+ @button.editor.selection.setRangeAfter @target
3146
+ @hide()
3147
+
3148
+ @editor.on 'valuechanged', (e) =>
3149
+ @refresh() if @active
2791
3150
 
2792
3151
  @_initUploader()
2793
3152
 
2794
3153
  _initUploader: ->
3154
+ $uploadBtn = @el.find('.btn-upload')
2795
3155
  unless @editor.uploader?
2796
- @el.find('.btn-upload').remove()
3156
+ $uploadBtn.remove()
2797
3157
  return
2798
3158
 
2799
- @input = @el.find 'input:file'
3159
+ createInput = =>
3160
+ @input.remove() if @input
3161
+ @input = $('<input type="file" title="上传图片" accept="image/*">')
3162
+ .appendTo($uploadBtn)
2800
3163
 
2801
- @input.on 'click mousedown', (e) =>
3164
+ createInput()
3165
+
3166
+ @el.on 'click mousedown', 'input[type=file]', (e) =>
2802
3167
  e.stopPropagation()
2803
3168
 
2804
- @input.on 'change', (e) =>
3169
+ @el.on 'change', 'input[type=file]', (e) =>
2805
3170
  @editor.uploader.upload(@input, {
2806
3171
  inline: true,
2807
- imgWrapper: @target
3172
+ img: @target
2808
3173
  })
2809
-
2810
- @input = @input.clone(true).replaceAll(@input)
3174
+ createInput()
2811
3175
 
2812
3176
  show: (args...) ->
2813
3177
  super args...
2814
- $img = @target.find('img')
2815
- @srcEl.val $img.attr('src')
3178
+ $img = @target
3179
+ if $img.hasClass 'uploading'
3180
+ @srcEl.val '正在上传'
3181
+ else
3182
+ @srcEl.val $img.attr('src')
2816
3183
 
2817
3184
 
2818
3185
  Simditor.Toolbar.addButton(ImageButton)
2819
3186
 
2820
3187
 
2821
-
2822
-
2823
3188
  class IndentButton extends Button
2824
3189
 
2825
3190
  name: 'indent'
@@ -2985,15 +3350,16 @@ class TableButton extends Button
2985
3350
  $table.find('tr:first td').each (i, td) =>
2986
3351
  $col = $('<col/>').appendTo $colgroup
2987
3352
 
2988
- @refreshTableWidth $table
3353
+ @refreshTableWidth $table
3354
+
2989
3355
 
2990
- $resizeHandle = $('<div class="resize-handle" contenteditable="false"></div>')
3356
+ $resizeHandle = $('<div class="simditor-resize-handle" contenteditable="false"></div>')
2991
3357
  .appendTo($wrapper)
2992
3358
 
2993
3359
  $wrapper.on 'mousemove', 'td', (e) =>
2994
3360
  return if $wrapper.hasClass('resizing')
2995
3361
  $td = $(e.currentTarget)
2996
- x = e.pageX - $(e.currentTarget).offset().left;
3362
+ x = e.pageX - $(e.currentTarget).offset().left
2997
3363
  $td = $td.prev() if x < 5 and $td.prev().length > 0
2998
3364
 
2999
3365
  if $td.next('td').length < 1
@@ -3020,7 +3386,7 @@ class TableButton extends Button
3020
3386
  $wrapper.on 'mouseleave', (e) =>
3021
3387
  $resizeHandle.hide()
3022
3388
 
3023
- $wrapper.on 'mousedown', '.resize-handle', (e) =>
3389
+ $wrapper.on 'mousedown', '.simditor-resize-handle', (e) =>
3024
3390
  $handle = $(e.currentTarget)
3025
3391
  $leftTd = $handle.data 'td'
3026
3392
  $leftCol = $handle.data 'col'
@@ -3058,7 +3424,9 @@ class TableButton extends Button
3058
3424
  false
3059
3425
 
3060
3426
  decorate: ($table) ->
3061
- return if $table.parent('.simditor-table').length > 0
3427
+ if $table.parent('.simditor-table').length > 0
3428
+ @undecorate $table
3429
+
3062
3430
  $table.wrap '<div class="simditor-table"></div>'
3063
3431
  @initResize $table
3064
3432
  $table.parent()
@@ -3250,3 +3618,32 @@ class TableButton extends Button
3250
3618
 
3251
3619
  Simditor.Toolbar.addButton TableButton
3252
3620
 
3621
+
3622
+
3623
+ class StrikethroughButton extends Button
3624
+
3625
+ name: 'strikethrough'
3626
+
3627
+ icon: 'strikethrough'
3628
+
3629
+ title: '删除线文字'
3630
+
3631
+ htmlTag: 'strike'
3632
+
3633
+ disableTag: 'pre'
3634
+
3635
+ status: ($node) ->
3636
+ @setDisabled $node.is(@disableTag) if $node?
3637
+ return true if @disabled
3638
+
3639
+ active = document.queryCommandState('strikethrough') is true
3640
+ @setActive active
3641
+ active
3642
+
3643
+ command: ->
3644
+ document.execCommand 'strikethrough'
3645
+ @editor.trigger 'valuechanged'
3646
+ @editor.trigger 'selectionchanged'
3647
+
3648
+
3649
+ Simditor.Toolbar.addButton(StrikethroughButton)