simditor-rails 1.0.1 → 1.0.5

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