simditor-rails 1.0.1

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.
@@ -0,0 +1,3252 @@
1
+
2
+ class Selection extends Plugin
3
+
4
+ @className: 'Selection'
5
+
6
+ constructor: (args...) ->
7
+ super args...
8
+ @sel = document.getSelection()
9
+ @editor = @widget
10
+
11
+ _init: ->
12
+
13
+ #@editor.on 'selectionchanged focus', (e) =>
14
+ #range = @editor.selection.getRange()
15
+ #return unless range?
16
+ #$container = $(range.commonAncestorContainer)
17
+
18
+ #if range.collapsed and $container.is('.simditor-body') and @editor.util.isBlockNode($container.children())
19
+ #@editor.blur()
20
+
21
+ clear: ->
22
+ try
23
+ @sel.removeAllRanges()
24
+ catch e
25
+
26
+ getRange: ->
27
+ if !@editor.inputManager.focused or !@sel.rangeCount
28
+ return null
29
+
30
+ return @sel.getRangeAt 0
31
+
32
+ selectRange: (range) ->
33
+ @clear()
34
+ @sel.addRange(range)
35
+
36
+ rangeAtEndOf: (node, range = @getRange()) ->
37
+ return unless range? and range.collapsed
38
+
39
+ node = $(node)[0]
40
+ endNode = range.endContainer
41
+ endNodeLength = @editor.util.getNodeLength endNode
42
+ #node.normalize()
43
+
44
+ if !(range.endOffset == endNodeLength - 1 and $(endNode).contents().last().is('br')) and range.endOffset != endNodeLength
45
+ return false
46
+
47
+ if node == endNode
48
+ return true
49
+ else if !$.contains(node, endNode)
50
+ return false
51
+
52
+ result = true
53
+ $(endNode).parentsUntil(node).addBack().each (i, n) =>
54
+ nodes = $(n).parent().contents().filter ->
55
+ !(this != n && this.nodeType == 3 && !this.nodeValue)
56
+ $lastChild = nodes.last()
57
+ unless $lastChild.get(0) == n or ($lastChild.is('br') and $lastChild.prev().get(0) == n)
58
+ result = false
59
+ return false
60
+
61
+ result
62
+
63
+ rangeAtStartOf: (node, range = @getRange()) ->
64
+ return unless range? and range.collapsed
65
+
66
+ node = $(node)[0]
67
+ startNode = range.startContainer
68
+
69
+ if range.startOffset != 0
70
+ return false
71
+
72
+ if node == startNode
73
+ return true
74
+ else if !$.contains(node, startNode)
75
+ return false
76
+
77
+ result = true
78
+ $(startNode).parentsUntil(node).addBack().each (i, n) =>
79
+ nodes = $(n).parent().contents().filter ->
80
+ !(this != n && this.nodeType == 3 && !this.nodeValue)
81
+ result = false unless nodes.first().get(0) == n
82
+
83
+ result
84
+
85
+ insertNode: (node, range = @getRange()) ->
86
+ return unless range?
87
+
88
+ node = $(node)[0]
89
+ range.insertNode node
90
+ @setRangeAfter node, range
91
+
92
+ setRangeAfter: (node, range = @getRange()) ->
93
+ return unless range?
94
+
95
+ node = $(node)[0]
96
+ range.setEndAfter node
97
+ range.collapse(false)
98
+ @selectRange range
99
+
100
+ setRangeBefore: (node, range = @getRange()) ->
101
+ return unless range?
102
+
103
+ node = $(node)[0]
104
+ range.setEndBefore node
105
+ range.collapse(false)
106
+ @selectRange range
107
+
108
+ setRangeAtStartOf: (node, range = @getRange()) ->
109
+ node = $(node).get(0)
110
+ range.setEnd(node, 0)
111
+ range.collapse(false)
112
+ @selectRange range
113
+
114
+ setRangeAtEndOf: (node, range = @getRange()) ->
115
+ $node = $(node)
116
+ node = $node.get(0)
117
+
118
+ if $node.is('pre')
119
+ contents = $node.contents()
120
+ if contents.length > 0
121
+ lastChild = contents.last()
122
+ lastText = lastChild.text()
123
+ if lastText.charAt(lastText.length - 1) is '\n'
124
+ range.setEnd(lastChild[0], @editor.util.getNodeLength(lastChild[0]) - 1)
125
+ else
126
+ range.setEnd(lastChild[0], @editor.util.getNodeLength(lastChild[0]))
127
+ else
128
+ range.setEnd(node, 0)
129
+ else
130
+ nodeLength = @editor.util.getNodeLength node
131
+ nodeLength -= 1 if node.nodeType != 3 and nodeLength > 0 and $(node).contents().last().is('br')
132
+ range.setEnd(node, nodeLength)
133
+
134
+ range.collapse(false)
135
+ @selectRange range
136
+
137
+ deleteRangeContents: (range = @getRange()) ->
138
+ range.deleteContents()
139
+
140
+ breakBlockEl: (el, range = @getRange()) ->
141
+ $el = $(el)
142
+ return $el unless range.collapsed
143
+ range.setStartBefore $el.get(0)
144
+ return $el if range.collapsed
145
+ $el.before range.extractContents()
146
+
147
+ save: () ->
148
+ return if @_selectionSaved
149
+
150
+ range = @getRange()
151
+ startCaret = $('<span/>').addClass('simditor-caret-start')
152
+ endCaret = $('<span/>').addClass('simditor-caret-end')
153
+
154
+ range.insertNode(startCaret[0])
155
+ range.collapse(false)
156
+ range.insertNode(endCaret[0])
157
+
158
+ @clear()
159
+ @_selectionSaved = true
160
+
161
+ restore: () ->
162
+ return false unless @_selectionSaved
163
+
164
+ startCaret = @editor.body.find('.simditor-caret-start')
165
+ endCaret = @editor.body.find('.simditor-caret-end')
166
+
167
+ if startCaret.length and endCaret.length
168
+ startContainer = startCaret.parent()
169
+ startOffset = startContainer.contents().index(startCaret)
170
+ endContainer = endCaret.parent()
171
+ endOffset = endContainer.contents().index(endCaret)
172
+
173
+ if startContainer[0] == endContainer[0]
174
+ endOffset -= 1;
175
+
176
+ range = document.createRange()
177
+ range.setStart(startContainer.get(0), startOffset)
178
+ range.setEnd(endContainer.get(0), endOffset)
179
+
180
+ startCaret.remove()
181
+ endCaret.remove()
182
+ @selectRange range
183
+
184
+ # firefox won't auto focus while applying new range
185
+ @editor.body.focus() if @editor.util.browser.firefox
186
+ else
187
+ startCaret.remove()
188
+ endCaret.remove()
189
+
190
+ @_selectionSaved = false
191
+ range
192
+
193
+
194
+
195
+
196
+ class Formatter extends Plugin
197
+
198
+ @className: 'Formatter'
199
+
200
+ constructor: (args...) ->
201
+ super args...
202
+ @editor = @widget
203
+
204
+ _init: ->
205
+ @editor.body.on 'click', 'a', (e) =>
206
+ false
207
+
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
+ decorate: ($el = @editor.body) ->
221
+ @editor.trigger 'decorate', [$el]
222
+
223
+ undecorate: ($el = @editor.body.clone()) ->
224
+ @editor.trigger 'undecorate', [$el]
225
+ $.trim $el.html()
226
+
227
+ autolink: ($el = @editor.body) ->
228
+ linkNodes = []
229
+
230
+ findLinkNode = ($parentNode) ->
231
+ $parentNode.contents().each (i, node) ->
232
+ $node = $(node)
233
+ if $node.is('a') or $node.closest('a', $el).length
234
+ return
235
+
236
+ if $node.contents().length
237
+ findLinkNode $node
238
+ else if (text = $node.text()) and /https?:\/\/|www\./ig.test(text)
239
+ linkNodes.push $node
240
+
241
+ findLinkNode $el
242
+
243
+ re = /(https?:\/\/|www\.)[\w\-\.\?&=\/#%:]+/ig
244
+ for $node in linkNodes
245
+ text = $node.text();
246
+ replaceEls = [];
247
+ match = null;
248
+ lastIndex = 0;
249
+
250
+ while (match = re.exec(text)) != null
251
+ replaceEls.push document.createTextNode(text.substring(lastIndex, match.index))
252
+ lastIndex = re.lastIndex
253
+ 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]
255
+
256
+ replaceEls.push document.createTextNode(text.substring(lastIndex))
257
+ $node.replaceWith $(replaceEls)
258
+
259
+ $el
260
+
261
+ # make sure the direct children is block node
262
+ format: ($el = @editor.body) ->
263
+ if $el.is ':empty'
264
+ $el.append '<p>' + @editor.util.phBr + '</p>'
265
+ return $el
266
+
267
+ @cleanNode(n, true) for n in $el.contents()
268
+
269
+ for node in $el.contents()
270
+ $node = $(node)
271
+ if $node.is('br')
272
+ blockNode = null if blockNode?
273
+ $node.remove()
274
+ else if @editor.util.isBlockNode(node) or $node.is('img')
275
+ if $node.is('li')
276
+ if blockNode and blockNode.is('ul, ol')
277
+ blockNode.append node
278
+ else
279
+ blockNode = $('<ul/>').insertBefore(node)
280
+ blockNode.append node
281
+ else
282
+ blockNode = null
283
+ else
284
+ blockNode = $('<p/>').insertBefore(node) if !blockNode or blockNode.is('ul, ol')
285
+ blockNode.append(node)
286
+
287
+ $el
288
+
289
+ cleanNode: (node, recursive) ->
290
+ $node = $(node)
291
+
292
+ if $node[0].nodeType == 3
293
+ text = $node.text().replace(/(\r\n|\n|\r)/gm, '')
294
+ textNode = document.createTextNode text
295
+ $node.replaceWith textNode
296
+ return
297
+
298
+ contents = $node.contents()
299
+ isDecoration = $node.is('[class^="simditor-"]')
300
+
301
+ if $node.is(@_allowedTags.join(',')) or isDecoration
302
+ # img inside a is not allowed
303
+ if $node.is('a') and $node.find('img').length > 0
304
+ contents.first().unwrap()
305
+
306
+ # Clean attributes except `src` `alt` on `img` tag and `href` `target` on `a` tag
307
+ unless isDecoration
308
+ allowedAttributes = @_allowedAttributes[$node[0].tagName.toLowerCase()]
309
+ for attr in $.makeArray($node[0].attributes)
310
+ $node.removeAttr(attr.name) unless allowedAttributes? and attr.name in allowedAttributes
311
+ else if $node[0].nodeType == 1 and !$node.is ':empty'
312
+ if $node.is('div, article, dl, header, footer, tr')
313
+ $node.append('<br/>')
314
+ contents.first().unwrap()
315
+ else if $node.is 'table'
316
+ $p = $('<p/>')
317
+ $node.find('tr').each (i, tr) =>
318
+ $p.append($(tr).text() + '<br/>')
319
+ $node.replaceWith $p
320
+ contents = null
321
+ else if $node.is 'thead, tfoot'
322
+ $node.remove()
323
+ contents = null
324
+ else if $node.is 'th'
325
+ $td = $('<td/>').append $node.contents()
326
+ $node.replaceWith $td
327
+ else
328
+ contents.first().unwrap()
329
+ else
330
+ $node.remove()
331
+ contents = null
332
+
333
+ @cleanNode(n, true) for n in contents if recursive and contents? and !$node.is('pre')
334
+ null
335
+
336
+ clearHtml: (html, lineBreak = true) ->
337
+ container = $('<div/>').append(html)
338
+ result = ''
339
+
340
+ container.contents().each (i, node) =>
341
+ if node.nodeType == 3
342
+ result += node.nodeValue
343
+ else if node.nodeType == 1
344
+ $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'
348
+ result += '\n'
349
+
350
+ result
351
+
352
+ # remove empty nodes and useless paragraph
353
+ beautify: ($contents) ->
354
+ uselessP = ($el) ->
355
+ !!($el.is('p') and !$el.text() and $el.children(':not(br)').length < 1)
356
+
357
+ $contents.each (i, el) =>
358
+ $el = $(el)
359
+ $el.remove() if $el.is(':not(img, br):empty')
360
+ $el.remove() if uselessP($el) #and uselessP($el.prev())
361
+ $el.find(':not(img, br):empty').remove()
362
+
363
+
364
+
365
+
366
+
367
+ class InputManager extends Plugin
368
+
369
+ @className: 'InputManager'
370
+
371
+ opts:
372
+ pasteImage: false
373
+
374
+ constructor: (args...) ->
375
+ super args...
376
+ @editor = @widget
377
+ @opts.pasteImage = 'inline' if @opts.pasteImage and typeof @opts.pasteImage != 'string'
378
+
379
+ _modifierKeys: [16, 17, 18, 91, 93, 224]
380
+
381
+ _arrowKeys: [37..40]
382
+
383
+ _init: ->
384
+ @_pasteArea = $('<div/>')
385
+ .css({
386
+ width: '1px',
387
+ height: '1px',
388
+ overflow: 'hidden',
389
+ position: 'fixed',
390
+ right: '0',
391
+ bottom: '100px'
392
+ })
393
+ .attr({
394
+ tabIndex: '-1',
395
+ contentEditable: true
396
+ })
397
+ .addClass('simditor-paste-area')
398
+ .appendTo(@editor.el)
399
+
400
+ @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) =>
403
+ $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)
407
+
408
+ @editor.body.on('keydown', $.proxy(@_onKeyDown, @))
409
+ .on('keypress', $.proxy(@_onKeyPress, @))
410
+ .on('keyup', $.proxy(@_onKeyUp, @))
411
+ .on('mouseup', $.proxy(@_onMouseUp, @))
412
+ .on('focus', $.proxy(@_onFocus, @))
413
+ .on('blur', $.proxy(@_onBlur, @))
414
+ .on('paste', $.proxy(@_onPaste, @))
415
+
416
+ # fix firefox cmd+left/right bug
417
+ if @editor.util.browser.firefox
418
+ @addShortcut 'cmd+37', (e) =>
419
+ e.preventDefault()
420
+ @editor.selection.sel.modify('move', 'backward', 'lineboundary')
421
+ @addShortcut 'cmd+39', (e) =>
422
+ e.preventDefault()
423
+ @editor.selection.sel.modify('move', 'forward', 'lineboundary')
424
+
425
+ if @editor.textarea.attr 'autofocus'
426
+ setTimeout =>
427
+ @editor.focus()
428
+ , 0
429
+
430
+ _onFocus: (e) ->
431
+ @editor.el.addClass('focus')
432
+ .removeClass('error')
433
+ @focused = true
434
+ @lastCaretPosition = null
435
+
436
+ @editor.body.find('.selected').removeClass('selected')
437
+
438
+ setTimeout =>
439
+ @editor.triggerHandler 'focus'
440
+ #@editor.trigger 'selectionchanged'
441
+ , 0
442
+
443
+ _onBlur: (e) ->
444
+ @editor.el.removeClass 'focus'
445
+ @editor.sync()
446
+ @focused = false
447
+ @lastCaretPosition = @editor.undoManager.currentState()?.caret
448
+
449
+ @editor.triggerHandler 'blur'
450
+
451
+ _onMouseUp: (e) ->
452
+ return if $(e.target).is('img, .simditor-image')
453
+ @editor.trigger 'selectionchanged'
454
+ @editor.undoManager.update()
455
+
456
+ _onKeyDown: (e) ->
457
+ if @editor.triggerHandler(e) == false
458
+ return false
459
+
460
+ # handle predefined shortcuts
461
+ shortcutKey = @editor.util.getShortcutKey e
462
+ if @_shortcuts[shortcutKey]
463
+ @_shortcuts[shortcutKey].call(this, e)
464
+ return false
465
+
466
+ # Check the condictional handlers
467
+ if e.which of @_keystrokeHandlers
468
+ result = @_keystrokeHandlers[e.which]['*']?(e)
469
+ if result
470
+ @editor.trigger 'valuechanged'
471
+ @editor.trigger 'selectionchanged'
472
+ return false
473
+
474
+ @editor.util.traverseUp (node) =>
475
+ return unless node.nodeType == 1
476
+ handler = @_keystrokeHandlers[e.which]?[node.tagName.toLowerCase()]
477
+ result = handler?(e, $(node))
478
+ !result
479
+ if result
480
+ @editor.trigger 'valuechanged'
481
+ @editor.trigger 'selectionchanged'
482
+ return false
483
+
484
+ if e.which in @_modifierKeys or e.which in @_arrowKeys
485
+ return
486
+
487
+ metaKey = @editor.util.metaKey e
488
+ $blockEl = @editor.util.closestBlockEl()
489
+
490
+ # paste shortcut
491
+ return if metaKey and e.which == 86
492
+
493
+ if @_typing
494
+ clearTimeout @_typing if @_typing != true
495
+ @_typing = setTimeout =>
496
+ @editor.trigger 'valuechanged'
497
+ @editor.trigger 'selectionchanged'
498
+ @_typing = false
499
+ , 200
500
+ else
501
+ setTimeout =>
502
+ @editor.trigger 'valuechanged'
503
+ @editor.trigger 'selectionchanged'
504
+ , 10
505
+ @_typing = true
506
+
507
+ null
508
+
509
+ _onKeyPress: (e) ->
510
+ if @editor.triggerHandler(e) == false
511
+ return false
512
+
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
+ _onKeyUp: (e) ->
530
+ if @editor.triggerHandler(e) == false
531
+ return false
532
+
533
+ if e.which in @_arrowKeys
534
+ @editor.trigger 'selectionchanged'
535
+ @editor.undoManager.update()
536
+ return
537
+
538
+ if e.which == 8 and (@editor.body.is(':empty') or (@editor.body.children().length == 1 and @editor.body.children().is('br')))
539
+ @editor.body.empty()
540
+ p = $('<p/>').append(@editor.util.phBr)
541
+ .appendTo(@editor.body)
542
+ @editor.selection.setRangeAtStartOf p
543
+ return
544
+
545
+ _onPaste: (e) ->
546
+ if @editor.triggerHandler(e) == false
547
+ return false
548
+
549
+ if e.originalEvent.clipboardData && e.originalEvent.clipboardData.items && e.originalEvent.clipboardData.items.length > 0
550
+ pasteItem = e.originalEvent.clipboardData.items[0]
551
+
552
+ # paste file in chrome
553
+ if /^image\//.test pasteItem.type
554
+ imageFile = pasteItem.getAsFile()
555
+ return unless imageFile? and @opts.pasteImage
556
+
557
+ unless imageFile.name
558
+ imageFile.name = "来自剪贴板的图片.png";
559
+
560
+ uploadOpt = {}
561
+ uploadOpt[@opts.pasteImage] = true
562
+ @editor.uploader?.upload(imageFile, uploadOpt)
563
+ return false
564
+
565
+
566
+ $blockEl = @editor.util.closestBlockEl()
567
+ codePaste = $blockEl.is 'pre'
568
+ @editor.selection.deleteRangeContents()
569
+ @editor.selection.save()
570
+
571
+ @_pasteArea.focus()
572
+
573
+ setTimeout =>
574
+ if @_pasteArea.is(':empty')
575
+ pasteContent = null
576
+ else if codePaste
577
+ pasteContent = @editor.formatter.clearHtml @_pasteArea.html()
578
+ else
579
+ pasteContent = $('<div/>').append(@_pasteArea.contents())
580
+ @editor.formatter.format pasteContent
581
+ @editor.formatter.decorate pasteContent
582
+ @editor.formatter.beautify pasteContent.children()
583
+ pasteContent = pasteContent.contents()
584
+
585
+ @_pasteArea.empty()
586
+ range = @editor.selection.restore()
587
+
588
+ if @editor.triggerHandler('pasting', [pasteContent]) == false
589
+ return
590
+
591
+ if !pasteContent
592
+ return
593
+ else if codePaste
594
+ node = document.createTextNode(pasteContent)
595
+ @editor.selection.insertNode node, range
596
+ else if pasteContent.length < 1
597
+ return
598
+ else if pasteContent.length == 1
599
+ if pasteContent.is('p')
600
+ 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
621
+ else if $blockEl.is('p') and @editor.util.isEmptyNode $blockEl
622
+ $blockEl.replaceWith pasteContent
623
+ @editor.selection.setRangeAtEndOf(pasteContent, range)
624
+ else if pasteContent.is('ul, ol') and $blockEl.is 'li'
625
+ $blockEl.parent().after pasteContent
626
+ @editor.selection.setRangeAtEndOf(pasteContent, range)
627
+ else
628
+ $blockEl.after pasteContent
629
+ @editor.selection.setRangeAtEndOf(pasteContent, range)
630
+ else
631
+ $blockEl = $blockEl.parent() if $blockEl.is 'li'
632
+
633
+ if @editor.selection.rangeAtStartOf($blockEl, range)
634
+ insertPosition = 'before'
635
+ else if @editor.selection.rangeAtEndOf($blockEl, range)
636
+ insertPosition = 'after'
637
+ else
638
+ @editor.selection.breakBlockEl($blockEl, range)
639
+ insertPosition = 'before'
640
+
641
+ $blockEl[insertPosition](pasteContent)
642
+ @editor.selection.setRangeAtEndOf(pasteContent.last(), range)
643
+
644
+ @editor.trigger 'valuechanged'
645
+ @editor.trigger 'selectionchanged'
646
+ , 10
647
+
648
+
649
+ # handlers which will be called when specific key is pressed in specific node
650
+ _keystrokeHandlers: {}
651
+
652
+ addKeystrokeHandler: (key, node, handler) ->
653
+ @_keystrokeHandlers[key] = {} unless @_keystrokeHandlers[key]
654
+ @_keystrokeHandlers[key][node] = handler
655
+
656
+
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
+ addShortcut: (keys, handler) ->
677
+ @_shortcuts[keys] = $.proxy(handler, this)
678
+
679
+
680
+
681
+
682
+
683
+
684
+
685
+
686
+ # Standardize keystroke actions across browsers
687
+
688
+ class Keystroke extends Plugin
689
+
690
+ @className: 'Keystroke'
691
+
692
+ constructor: (args...) ->
693
+ super args...
694
+ @editor = @widget
695
+
696
+ _init: ->
697
+
698
+ # safari doesn't support shift + enter default behavior
699
+ if @editor.util.browser.safari
700
+ @editor.inputManager.addKeystrokeHandler '13', '*', (e) =>
701
+ return unless e.shiftKey
702
+ $br = $('<br/>')
703
+
704
+ if @editor.selection.rangeAtEndOf $blockEl
705
+ @editor.selection.insertNode $br
706
+ @editor.selection.insertNode $('<br/>')
707
+ @editor.selection.setRangeBefore $br
708
+ else
709
+ @editor.selection.insertNode $br
710
+
711
+ true
712
+
713
+
714
+ # Remove hr and img node
715
+ @editor.inputManager.addKeystrokeHandler '8', '*', (e) =>
716
+ $rootBlock = @editor.util.furthestBlockEl()
717
+ $prevBlockEl = $rootBlock.prev()
718
+ if $prevBlockEl.is('hr, .simditor-image') and @editor.selection.rangeAtStartOf $rootBlock
719
+ # TODO: need to test on IE
720
+ @editor.selection.save()
721
+ $prevBlockEl.remove()
722
+ @editor.selection.restore()
723
+ return true
724
+
725
+
726
+ # Tab to indent
727
+ @editor.inputManager.addKeystrokeHandler '9', '*', (e) =>
728
+ return unless @editor.opts.tabIndent
729
+
730
+ if e.shiftKey
731
+ @editor.util.outdent()
732
+ else
733
+ @editor.util.indent()
734
+ true
735
+
736
+
737
+ # press enter in a empty list item
738
+ @editor.inputManager.addKeystrokeHandler '13', 'li', (e, $node) =>
739
+ $cloneNode = $node.clone()
740
+ $cloneNode.find('ul, ol').remove()
741
+ return unless @editor.util.isEmptyNode($cloneNode) and $node.is(@editor.util.closestBlockEl())
742
+ listEl = $node.parent()
743
+
744
+ # item in the middle of list
745
+ if $node.next('li').length > 0
746
+ return unless @editor.util.isEmptyNode($node)
747
+
748
+ # in a nested list
749
+ if listEl.parent('li').length > 0
750
+ newBlockEl = $('<li/>').append(@editor.util.phBr).insertAfter(listEl.parent('li'))
751
+ newListEl = $('<' + listEl[0].tagName + '/>').append($node.nextAll('li'))
752
+ newBlockEl.append newListEl
753
+ # in a root list
754
+ else
755
+ newBlockEl = $('<p/>').append(@editor.util.phBr).insertAfter(listEl)
756
+ newListEl = $('<' + listEl[0].tagName + '/>').append($node.nextAll('li'))
757
+ newBlockEl.after newListEl
758
+
759
+ # item at the end of list
760
+ else
761
+ # in a nested list
762
+ if listEl.parent('li').length > 0
763
+ newBlockEl = $('<li/>').insertAfter(listEl.parent('li'))
764
+ if $node.contents().length > 0
765
+ newBlockEl.append $node.contents()
766
+ else
767
+ newBlockEl.append @editor.util.phBr
768
+ # in a root list
769
+ else
770
+ newBlockEl = $('<p/>').append(@editor.util.phBr).insertAfter(listEl)
771
+ newBlockEl.after $node.children('ul, ol') if $node.children('ul, ol').length > 0
772
+
773
+ if $node.prev('li').length
774
+ $node.remove()
775
+ else
776
+ listEl.remove()
777
+
778
+ @editor.selection.setRangeAtStartOf newBlockEl
779
+ true
780
+
781
+
782
+ # press enter in a code block: insert \n instead of br
783
+ @editor.inputManager.addKeystrokeHandler '13', 'pre', (e, $node) =>
784
+ e.preventDefault()
785
+ range = @editor.selection.getRange()
786
+ breakNode = null
787
+
788
+ range.deleteContents()
789
+
790
+ if !@editor.util.browser.msie && @editor.selection.rangeAtEndOf $node
791
+ breakNode = document.createTextNode('\n\n')
792
+ range.insertNode breakNode
793
+ range.setEnd breakNode, 1
794
+ else
795
+ breakNode = document.createTextNode('\n')
796
+ range.insertNode breakNode
797
+ range.setStartAfter breakNode
798
+
799
+ range.collapse(false)
800
+ @editor.selection.selectRange range
801
+ true
802
+
803
+
804
+ # press enter in the last paragraph of blockquote, just leave the block quote
805
+ @editor.inputManager.addKeystrokeHandler '13', 'blockquote', (e, $node) =>
806
+ $closestBlock = @editor.util.closestBlockEl()
807
+ return unless $closestBlock.is('p') and !$closestBlock.next().length and @editor.util.isEmptyNode $closestBlock
808
+ $node.after $closestBlock
809
+ @editor.selection.setRangeAtStartOf $closestBlock
810
+ true
811
+
812
+
813
+ # press delete in a empty li which has a nested list
814
+ @editor.inputManager.addKeystrokeHandler '8', 'li', (e, $node) =>
815
+ $childList = $node.children('ul, ol')
816
+ $prevNode = $node.prev('li')
817
+ return unless $childList.length > 0 and $prevNode.length > 0
818
+
819
+ text = ''
820
+ $textNode = null
821
+ $node.contents().each (i, n) =>
822
+ if n.nodeType == 3 and n.nodeValue
823
+ text += n.nodeValue
824
+ $textNode = $(n)
825
+ if $textNode and text.length == 1 and @editor.util.browser.firefox and !$textNode.next('br').length
826
+ $br = $(@editor.util.phBr).insertAfter $textNode
827
+ $textNode.remove()
828
+ @editor.selection.setRangeBefore $br
829
+ return true
830
+ else if text.length > 0
831
+ return
832
+
833
+ range = document.createRange()
834
+ $prevChildList = $prevNode.children('ul, ol')
835
+ if $prevChildList.length > 0
836
+ $newLi = $('<li/>').append(@editor.util.phBr).appendTo($prevChildList)
837
+ $prevChildList.append $childList.children('li')
838
+ $node.remove()
839
+ @editor.selection.setRangeAtEndOf $newLi, range
840
+ else
841
+ @editor.selection.setRangeAtEndOf $prevNode, range
842
+ $prevNode.append $childList
843
+ $node.remove()
844
+ @editor.selection.selectRange range
845
+ true
846
+
847
+
848
+ # press delete at start of code block
849
+ @editor.inputManager.addKeystrokeHandler '8', 'pre', (e, $node) =>
850
+ return unless @editor.selection.rangeAtStartOf $node
851
+ codeStr = $node.html().replace('\n', '<br/>')
852
+ $newNode = $('<p/>').append(codeStr || @editor.util.phBr).insertAfter $node
853
+ $node.remove()
854
+ @editor.selection.setRangeAtStartOf $newNode
855
+ true
856
+
857
+
858
+ # press delete at start of blockquote
859
+ @editor.inputManager.addKeystrokeHandler '8', 'blockquote', (e, $node) =>
860
+ return unless @editor.selection.rangeAtStartOf $node
861
+ $firstChild = $node.children().first().unwrap()
862
+ @editor.selection.setRangeAtStartOf $firstChild
863
+ true
864
+
865
+
866
+
867
+ class UndoManager extends Plugin
868
+
869
+ @className: 'UndoManager'
870
+
871
+ _stack: []
872
+
873
+ _index: -1
874
+
875
+ _capacity: 50
876
+
877
+ _timer: null
878
+
879
+ constructor: (args...) ->
880
+ super args...
881
+ @editor = @widget
882
+
883
+ _init: ->
884
+ if @editor.util.os.mac
885
+ undoShortcut = 'cmd+90'
886
+ redoShortcut = 'shift+cmd+89'
887
+ else
888
+ undoShortcut = 'ctrl+90'
889
+ redoShortcut = 'ctrl+89'
890
+
891
+ @editor.inputManager.addShortcut undoShortcut, (e) =>
892
+ e.preventDefault()
893
+ @undo()
894
+
895
+ @editor.inputManager.addShortcut redoShortcut, (e) =>
896
+ e.preventDefault()
897
+ @redo()
898
+
899
+ @editor.on 'valuechanged', (e, src) =>
900
+ return if src == 'undo'
901
+
902
+ if @_timer
903
+ clearTimeout @_timer
904
+ @_timer = null
905
+
906
+ @_timer = setTimeout =>
907
+ @_pushUndoState()
908
+ , 200
909
+
910
+ _pushUndoState: ->
911
+ currentState = @currentState()
912
+ html = @editor.body.html()
913
+ return if currentState and currentState.html == html
914
+
915
+ @_index += 1
916
+ @_stack.length = @_index
917
+
918
+ @_stack.push
919
+ html: html
920
+ caret: @caretPosition()
921
+
922
+ if @_stack.length > @_capacity
923
+ @_stack.shift()
924
+ @_index -= 1
925
+
926
+ currentState: ->
927
+ if @_stack.length and @_index > -1
928
+ @_stack[@_index]
929
+ else
930
+ null
931
+
932
+ undo: ->
933
+ return if @_index < 1 or @_stack.length < 2
934
+
935
+ @editor.hidePopover()
936
+
937
+ @_index -= 1
938
+
939
+ state = @_stack[@_index]
940
+ @editor.body.html state.html
941
+ @caretPosition state.caret
942
+ @editor.sync()
943
+
944
+ @editor.trigger 'valuechanged', ['undo']
945
+ @editor.trigger 'selectionchanged', ['undo']
946
+
947
+ redo: ->
948
+ return if @_index < 0 or @_stack.length < @_index + 2
949
+
950
+ @editor.hidePopover()
951
+
952
+ @_index += 1
953
+
954
+ state = @_stack[@_index]
955
+ @editor.body.html state.html
956
+ @caretPosition state.caret
957
+ @editor.sync()
958
+
959
+ @editor.trigger 'valuechanged', ['undo']
960
+ @editor.trigger 'selectionchanged', ['undo']
961
+
962
+ update: () ->
963
+ currentState = @currentState()
964
+ return unless currentState
965
+
966
+ html = @editor.body.html()
967
+ currentState.html = html
968
+ currentState.caret = @caretPosition()
969
+
970
+ _getNodeOffset: (node, index) ->
971
+ if index
972
+ $parent = $(node)
973
+ else
974
+ $parent = $(node).parent()
975
+
976
+ offset = 0
977
+ merging = false
978
+ $parent.contents().each (i, child) =>
979
+ if index == i or node == child
980
+ return false
981
+
982
+ if child.nodeType == 3
983
+ if !merging
984
+ offset += 1
985
+ merging = true
986
+ else
987
+ offset += 1
988
+ merging = false
989
+
990
+ null
991
+
992
+ offset
993
+
994
+ _getNodePosition: (node, offset) ->
995
+ if node.nodeType == 3
996
+ prevNode = node.previousSibling
997
+ while prevNode and prevNode.nodeType == 3
998
+ node = prevNode
999
+ offset += @editor.util.getNodeLength prevNode
1000
+ prevNode = prevNode.previousSibling
1001
+ else
1002
+ offset = @_getNodeOffset(node, offset)
1003
+
1004
+ position = []
1005
+ position.unshift offset
1006
+ @editor.util.traverseUp (n) =>
1007
+ position.unshift @_getNodeOffset(n)
1008
+ , node
1009
+
1010
+ position
1011
+
1012
+ _getNodeByPosition: (position) ->
1013
+ node = @editor.body[0]
1014
+
1015
+ for offset, i in position[0...position.length - 1]
1016
+ childNodes = node.childNodes
1017
+ if offset > childNodes.length - 1
1018
+ # when pre is empty, the text node will be lost
1019
+ if i == position.length - 2 and $(node).is('pre')
1020
+ child = document.createTextNode ''
1021
+ node.appendChild child
1022
+ childNodes = node.childNodes
1023
+ else
1024
+ node = null
1025
+ break
1026
+ node = childNodes[offset]
1027
+
1028
+ node
1029
+
1030
+ caretPosition: (caret) ->
1031
+ # calculate current caret state
1032
+ if !caret
1033
+ range = @editor.selection.getRange()
1034
+ return {} unless @editor.inputManager.focused and range?
1035
+
1036
+ caret =
1037
+ start: []
1038
+ end: null
1039
+ collapsed: true
1040
+
1041
+ caret.start = @_getNodePosition(range.startContainer, range.startOffset)
1042
+
1043
+ unless range.collapsed
1044
+ caret.end = @_getNodePosition(range.endContainer, range.endOffset)
1045
+ caret.collapsed = false
1046
+
1047
+ return caret
1048
+
1049
+ # restore caret state
1050
+ else
1051
+ @editor.body.focus() unless @editor.inputManager.focused
1052
+
1053
+ unless caret.start
1054
+ @editor.body.blur()
1055
+ return
1056
+
1057
+ startContainer = @_getNodeByPosition caret.start
1058
+ startOffset = caret.start[caret.start.length - 1]
1059
+
1060
+ if caret.collapsed
1061
+ endContainer = startContainer
1062
+ endOffset = startOffset
1063
+ else
1064
+ endContainer = @_getNodeByPosition caret.end
1065
+ endOffset = caret.start[caret.start.length - 1]
1066
+
1067
+ if !startContainer or !endContainer
1068
+ throw new Error 'simditor: invalid caret state'
1069
+ return
1070
+
1071
+ range = document.createRange()
1072
+ range.setStart(startContainer, startOffset)
1073
+ range.setEnd(endContainer, endOffset)
1074
+
1075
+ @editor.selection.selectRange range
1076
+
1077
+
1078
+
1079
+
1080
+
1081
+
1082
+ class Util extends Plugin
1083
+
1084
+ @className: 'Util'
1085
+
1086
+ constructor: (args...) ->
1087
+ super args...
1088
+ @phBr = '' if @browser.msie and @browser.version < 11
1089
+ @editor = @widget
1090
+
1091
+ _init: ->
1092
+
1093
+ phBr: '<br/>'
1094
+
1095
+ os: (->
1096
+ if /Mac/.test navigator.appVersion
1097
+ mac: true
1098
+ else if /Linux/.test navigator.appVersion
1099
+ linux: true
1100
+ else if /Win/.test navigator.appVersion
1101
+ win: true
1102
+ else if /X11/.test navigator.appVersion
1103
+ unix: true
1104
+ else
1105
+ {}
1106
+ )()
1107
+
1108
+ browser: (->
1109
+ ua = navigator.userAgent
1110
+ ie = /(msie|trident)/i.test(ua)
1111
+ chrome = /chrome|crios/i.test(ua)
1112
+ safari = /safari/i.test(ua) && !chrome
1113
+ firefox = /firefox/i.test(ua)
1114
+
1115
+ if ie
1116
+ msie: true
1117
+ version: ua.match(/(msie |rv:)(\d+(\.\d+)?)/i)[2]
1118
+ else if chrome
1119
+ webkit: true
1120
+ chrome: true
1121
+ version: ua.match(/(?:chrome|crios)\/(\d+(\.\d+)?)/i)[1]
1122
+ else if safari
1123
+ webkit: true
1124
+ safari: true
1125
+ version: ua.match(/version\/(\d+(\.\d+)?)/i)[1]
1126
+ else if firefox
1127
+ mozilla: true
1128
+ firefox: true
1129
+ version: ua.match(/firefox\/(\d+(\.\d+)?)/i)[1]
1130
+ else
1131
+ {}
1132
+ )()
1133
+
1134
+ metaKey: (e) ->
1135
+ isMac = /Mac/.test navigator.userAgent
1136
+ if isMac then e.metaKey else e.ctrlKey
1137
+
1138
+ isEmptyNode: (node) ->
1139
+ $node = $(node)
1140
+ !$node.text() and !$node.find(':not(br, span)').length
1141
+
1142
+ isBlockNode: (node) ->
1143
+ node = $(node)[0]
1144
+ if !node or node.nodeType == 3
1145
+ return false
1146
+
1147
+ /^(div|p|ul|ol|li|blockquote|hr|pre|h1|h2|h3|h4|table)$/.test node.nodeName.toLowerCase()
1148
+
1149
+ closestBlockEl: (node) ->
1150
+ unless node?
1151
+ range = @editor.selection.getRange()
1152
+ node = range?.commonAncestorContainer
1153
+
1154
+ $node = $(node)
1155
+
1156
+ return null unless $node.length
1157
+
1158
+ blockEl = $node.parentsUntil(@editor.body).addBack()
1159
+ blockEl = blockEl.filter (i) =>
1160
+ @isBlockNode blockEl.eq(i)
1161
+
1162
+ if blockEl.length then blockEl.last() else null
1163
+
1164
+ furthestNode: (node, filter) ->
1165
+ unless node?
1166
+ range = @editor.selection.getRange()
1167
+ node = range?.commonAncestorContainer
1168
+
1169
+ $node = $(node)
1170
+
1171
+ return null unless $node.length
1172
+
1173
+ blockEl = $node.parentsUntil(@editor.body).addBack()
1174
+ blockEl = blockEl.filter (i) =>
1175
+ $n = blockEl.eq(i)
1176
+ if $.isFunction filter
1177
+ return filter $n
1178
+ else
1179
+ return $n.is(filter)
1180
+
1181
+ if blockEl.length then blockEl.first() else null
1182
+
1183
+
1184
+ furthestBlockEl: (node) ->
1185
+ @furthestNode(node, @isBlockNode)
1186
+ #unless node?
1187
+ #range = @editor.selection.getRange()
1188
+ #node = range?.commonAncestorContainer
1189
+
1190
+ #$node = $(node)
1191
+
1192
+ #return null unless $node.length
1193
+
1194
+ #blockEl = $node.parentsUntil(@editor.body).addBack()
1195
+ #blockEl = blockEl.filter (i) =>
1196
+ #@isBlockNode blockEl.eq(i)
1197
+
1198
+ #if blockEl.length then blockEl.first() else null
1199
+
1200
+ getNodeLength: (node) ->
1201
+ switch node.nodeType
1202
+ when 7, 10 then 0
1203
+ when 3, 8 then node.length
1204
+ else node.childNodes.length
1205
+
1206
+
1207
+ traverseUp:(callback, node) ->
1208
+ unless node?
1209
+ range = @editor.selection.getRange()
1210
+ node = range?.commonAncestorContainer
1211
+
1212
+ if !node? or !$.contains(@editor.body[0], node)
1213
+ return false
1214
+
1215
+ nodes = $(node).parentsUntil(@editor.body).get()
1216
+ nodes.unshift node
1217
+ for n in nodes
1218
+ result = callback n
1219
+ break if result == false
1220
+
1221
+ getShortcutKey: (e) ->
1222
+ shortcutName = []
1223
+ shortcutName.push 'shift' if e.shiftKey
1224
+ shortcutName.push 'ctrl' if e.ctrlKey
1225
+ shortcutName.push 'alt' if e.altKey
1226
+ shortcutName.push 'cmd' if e.metaKey
1227
+ shortcutName.push e.which
1228
+ shortcutName.join '+'
1229
+
1230
+ indent: () ->
1231
+ $blockEl = @editor.util.closestBlockEl()
1232
+ return false unless $blockEl and $blockEl.length > 0
1233
+
1234
+ if $blockEl.is('pre')
1235
+ spaceNode = document.createTextNode '\u00A0\u00A0'
1236
+ @editor.selection.insertNode spaceNode
1237
+ else if $blockEl.is('li')
1238
+ $parentLi = $blockEl.prev('li')
1239
+ return false if $parentLi.length < 1
1240
+
1241
+ @editor.selection.save()
1242
+ tagName = $blockEl.parent()[0].tagName
1243
+ $childList = $parentLi.children('ul, ol')
1244
+
1245
+ if $childList.length > 0
1246
+ $childList.append $blockEl
1247
+ else
1248
+ $('<' + tagName + '/>')
1249
+ .append($blockEl)
1250
+ .appendTo($parentLi)
1251
+
1252
+ @editor.selection.restore()
1253
+ else if $blockEl.is 'p, h1, h2, h3, h4'
1254
+ indentLevel = $blockEl.attr('data-indent') ? 0
1255
+ indentLevel = indentLevel * 1 + 1
1256
+ indentLevel = 10 if indentLevel > 10
1257
+ $blockEl.attr 'data-indent', indentLevel
1258
+ else if $blockEl.is 'table'
1259
+ range = @editor.selection.getRange()
1260
+ $td = $(range.commonAncestorContainer).closest('td')
1261
+ $nextTd = $td.next('td')
1262
+ $nextTd = $td.parent('tr').next('tr').find('td:first') unless $nextTd.length > 0
1263
+ return false unless $td.length > 0 and $nextTd.length > 0
1264
+ @editor.selection.setRangeAtEndOf $nextTd
1265
+ else
1266
+ spaceNode = document.createTextNode '\u00A0\u00A0\u00A0\u00A0'
1267
+ @editor.selection.insertNode spaceNode
1268
+
1269
+ @editor.trigger 'valuechanged'
1270
+ @editor.trigger 'selectionchanged'
1271
+ true
1272
+
1273
+ outdent: () ->
1274
+ $blockEl = @editor.util.closestBlockEl()
1275
+ return false unless $blockEl and $blockEl.length > 0
1276
+
1277
+ if $blockEl.is('pre')
1278
+ # TODO: outdent in code block
1279
+ return false
1280
+ else if $blockEl.is('li')
1281
+ $parent = $blockEl.parent()
1282
+ $parentLi = $parent.parent('li')
1283
+
1284
+ if $parentLi.length < 1
1285
+ button = @editor.toolbar.findButton $parent[0].tagName.toLowerCase()
1286
+ button?.command()
1287
+ return false
1288
+
1289
+ @editor.selection.save()
1290
+
1291
+ if $blockEl.next('li').length > 0
1292
+ $('<' + $parent[0].tagName + '/>')
1293
+ .append($blockEl.nextAll('li'))
1294
+ .appendTo($blockEl)
1295
+
1296
+ $blockEl.insertAfter $parentLi
1297
+ $parent.remove() if $parent.children('li').length < 1
1298
+ @editor.selection.restore()
1299
+ else if $blockEl.is 'p, h1, h2, h3, h4'
1300
+ indentLevel = $blockEl.attr('data-indent') ? 0
1301
+ indentLevel = indentLevel * 1 - 1
1302
+ indentLevel = 0 if indentLevel < 0
1303
+ $blockEl.attr 'data-indent', indentLevel
1304
+ else if $blockEl.is 'table'
1305
+ range = @editor.selection.getRange()
1306
+ $td = $(range.commonAncestorContainer).closest('td')
1307
+ $prevTd = $td.prev('td')
1308
+ $prevTd = $td.parent('tr').prev('tr').find('td:last') unless $prevTd.length > 0
1309
+ return false unless $td.length > 0 and $prevTd.length > 0
1310
+ @editor.selection.setRangeAtEndOf $prevTd
1311
+ else
1312
+ return false
1313
+
1314
+ @editor.trigger 'valuechanged'
1315
+ @editor.trigger 'selectionchanged'
1316
+ true
1317
+
1318
+
1319
+ class Toolbar extends Plugin
1320
+
1321
+ @className: 'Toolbar'
1322
+
1323
+ opts:
1324
+ toolbar: true
1325
+ toolbarFloat: true
1326
+
1327
+ _tpl:
1328
+ wrapper: '<div class="simditor-toolbar"><ul></ul></div>'
1329
+ separator: '<li><span class="separator"></span></li>'
1330
+
1331
+ constructor: (args...) ->
1332
+ super args...
1333
+ @editor = @widget
1334
+
1335
+ _init: ->
1336
+ return unless @opts.toolbar
1337
+
1338
+ unless $.isArray @opts.toolbar
1339
+ @opts.toolbar = ['bold', 'italic', 'underline', '|', 'ol', 'ul', 'blockquote', 'code', '|', 'link', 'image', '|', 'indent', 'outdent']
1340
+
1341
+ @_render()
1342
+
1343
+ @list.on 'click', (e) =>
1344
+ false
1345
+
1346
+ @wrapper.on 'mousedown', (e) =>
1347
+ @list.find('.menu-on').removeClass('.menu-on')
1348
+
1349
+ $(document).on 'mousedown.simditor', (e) =>
1350
+ @list.find('.menu-on').removeClass('.menu-on')
1351
+
1352
+ if @opts.toolbarFloat
1353
+ @wrapper.width @wrapper.outerWidth()
1354
+ @wrapper.css 'left', @wrapper.offset().left
1355
+ $(window).on 'scroll.simditor-' + @editor.id, (e) =>
1356
+ topEdge = @editor.wrapper.offset().top
1357
+ bottomEdge = topEdge + @editor.wrapper.outerHeight() - 80
1358
+ scrollTop = $(document).scrollTop()
1359
+
1360
+ if scrollTop <= topEdge or scrollTop >= bottomEdge
1361
+ @editor.wrapper.removeClass('toolbar-floating')
1362
+ else
1363
+ @editor.wrapper.addClass('toolbar-floating')
1364
+
1365
+ @editor.on 'selectionchanged focus', =>
1366
+ @toolbarStatus()
1367
+
1368
+ @editor.on 'destroy', =>
1369
+ @buttons.length = 0
1370
+
1371
+ $(document).on 'mousedown.simditor-' + @editor.id, (e) =>
1372
+ @list.find('li.menu-on').removeClass('menu-on')
1373
+
1374
+ _render: ->
1375
+ @buttons = []
1376
+ @wrapper = $(@_tpl.wrapper).prependTo(@editor.wrapper)
1377
+ @list = @wrapper.find('ul')
1378
+
1379
+ for name in @opts.toolbar
1380
+ if name == '|'
1381
+ $(@_tpl.separator).appendTo @list
1382
+ continue
1383
+
1384
+ unless @constructor.buttons[name]
1385
+ throw new Error 'simditor: invalid toolbar button "' + name + '"'
1386
+ continue
1387
+
1388
+ @buttons.push new @constructor.buttons[name](@editor)
1389
+
1390
+ toolbarStatus: (name) ->
1391
+ return unless @editor.inputManager.focused
1392
+
1393
+ buttons = @buttons[..]
1394
+ @editor.util.traverseUp (node) =>
1395
+ removeButtons = []
1396
+ for button, i in buttons
1397
+ continue if name? and button.name isnt name
1398
+ removeButtons.push button if !button.status or button.status($(node)) is true
1399
+
1400
+ for button in removeButtons
1401
+ i = $.inArray(button, buttons)
1402
+ buttons.splice(i, 1)
1403
+ return false if buttons.length == 0
1404
+
1405
+ #button.setActive false for button in buttons unless success
1406
+
1407
+ findButton: (name) ->
1408
+ button = @list.find('.toolbar-item-' + name).data('button')
1409
+ button ? null
1410
+
1411
+ @addButton: (btn) ->
1412
+ @buttons[btn::name] = btn
1413
+
1414
+ @buttons: {}
1415
+
1416
+
1417
+
1418
+
1419
+ class Simditor extends Widget
1420
+ @connect Util
1421
+ @connect UndoManager
1422
+ @connect InputManager
1423
+ @connect Keystroke
1424
+ @connect Formatter
1425
+ @connect Selection
1426
+ @connect Toolbar
1427
+
1428
+ @count: 0
1429
+
1430
+ opts:
1431
+ textarea: null
1432
+ placeholder: false
1433
+ defaultImage: 'images/image.png'
1434
+ params: null
1435
+ upload: false
1436
+ tabIndent: true
1437
+
1438
+ _init: ->
1439
+ @textarea = $(@opts.textarea);
1440
+ @opts.placeholder = @opts.placeholder ? @textarea.attr('placeholder')
1441
+
1442
+ unless @textarea.length
1443
+ throw new Error 'simditor: param textarea is required.'
1444
+ return
1445
+
1446
+ editor = @textarea.data 'simditor'
1447
+ if editor?
1448
+ editor.destroy()
1449
+
1450
+ @id = ++ Simditor.count
1451
+ @_render()
1452
+
1453
+ if @opts.upload and Uploader
1454
+ uploadOpts = if typeof @opts.upload == 'object' then @opts.upload else {}
1455
+ @uploader = new Uploader(uploadOpts)
1456
+
1457
+ form = @textarea.closest 'form'
1458
+ if form.length
1459
+ form.on 'submit.simditor-' + @id, =>
1460
+ @sync()
1461
+ form.on 'reset.simditor-' + @id, =>
1462
+ @setValue ''
1463
+
1464
+ # set default value after all plugins are connected
1465
+ @on 'pluginconnected', =>
1466
+ @setValue @textarea.val() || ''
1467
+
1468
+ if @opts.placeholder
1469
+ @on 'valuechanged', =>
1470
+ @_placeholder()
1471
+
1472
+ setTimeout =>
1473
+ @trigger 'valuechanged'
1474
+ , 0
1475
+
1476
+ # Disable the resizing of `img` and `table`
1477
+ #if @browser.mozilla
1478
+ #document.execCommand "enableObjectResizing", false, "false"
1479
+ #document.execCommand "enableInlineTableEditing", false, "false"
1480
+
1481
+ _tpl:"""
1482
+ <div class="simditor">
1483
+ <div class="simditor-wrapper">
1484
+ <div class="simditor-placeholder"></div>
1485
+ <div class="simditor-body" contenteditable="true">
1486
+ </div>
1487
+ </div>
1488
+ </div>
1489
+ """
1490
+
1491
+ _render: ->
1492
+ @el = $(@_tpl).insertBefore @textarea
1493
+ @wrapper = @el.find '.simditor-wrapper'
1494
+ @body = @wrapper.find '.simditor-body'
1495
+ @placeholderEl = @wrapper.find('.simditor-placeholder').append(@opts.placeholder)
1496
+
1497
+ @el.append(@textarea)
1498
+ .data 'simditor', this
1499
+ @textarea.data('simditor', this)
1500
+ .hide()
1501
+ .blur()
1502
+ @body.attr 'tabindex', @textarea.attr('tabindex')
1503
+
1504
+ if @util.os.mac
1505
+ @el.addClass 'simditor-mac'
1506
+ else if @util.os.linux
1507
+ @el.addClass 'simditor-linux'
1508
+
1509
+ if @opts.params
1510
+ for key, val of @opts.params
1511
+ $('<input/>', {
1512
+ type: 'hidden'
1513
+ name: key,
1514
+ value: val
1515
+ }).insertAfter(@textarea)
1516
+
1517
+ _placeholder: ->
1518
+ children = @body.children()
1519
+ if children.length == 0 or (children.length == 1 and @util.isEmptyNode(children) and (children.data('indent') ? 0) < 1)
1520
+ @placeholderEl.show()
1521
+ else
1522
+ @placeholderEl.hide()
1523
+
1524
+ setValue: (val) ->
1525
+ @textarea.val val
1526
+ @body.html val
1527
+
1528
+ @formatter.format()
1529
+ @formatter.decorate()
1530
+
1531
+ getValue: () ->
1532
+ @sync()
1533
+
1534
+ sync: ->
1535
+ cloneBody = @body.clone()
1536
+ @formatter.undecorate cloneBody
1537
+ @formatter.format cloneBody
1538
+
1539
+ # generate `a` tag automatically
1540
+ @formatter.autolink cloneBody
1541
+
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
1545
+ emptyP = lastP
1546
+ lastP = lastP.prev 'p'
1547
+ emptyP.remove()
1548
+
1549
+ val = $.trim(cloneBody.html())
1550
+ @textarea.val val
1551
+ val
1552
+
1553
+ 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()
1559
+
1560
+ blur: ->
1561
+ @body.blur()
1562
+
1563
+ hidePopover: ->
1564
+ @wrapper.find('.simditor-popover').each (i, popover) =>
1565
+ popover = $(popover).data('popover')
1566
+ popover.hide() if popover.active
1567
+
1568
+ destroy: ->
1569
+ @triggerHandler 'destroy'
1570
+
1571
+ @textarea.closest('form')
1572
+ .off('.simditor .simditor-' + @id)
1573
+
1574
+ @selection.clear()
1575
+
1576
+ @textarea.insertBefore(@el)
1577
+ .hide()
1578
+ .val('')
1579
+ .removeData 'simditor'
1580
+
1581
+ @el.remove()
1582
+ $(document).off '.simditor-' + @id
1583
+ $(window).off '.simditor-' + @id
1584
+ @off()
1585
+
1586
+
1587
+ window.Simditor = Simditor
1588
+
1589
+
1590
+ class TestPlugin extends Plugin
1591
+
1592
+ class Test extends Widget
1593
+ @connect TestPlugin
1594
+
1595
+
1596
+ class Button extends Module
1597
+
1598
+ _tpl:
1599
+ item: '<li><a tabindex="-1" unselectable="on" class="toolbar-item" href="javascript:;"><span></span></a></li>'
1600
+ menuWrapper: '<div class="toolbar-menu"></div>'
1601
+ menuItem: '<li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;"><span></span></a></li>'
1602
+ separator: '<li><span class="separator"></span></li>'
1603
+
1604
+ name: ''
1605
+
1606
+ icon: ''
1607
+
1608
+ title: ''
1609
+
1610
+ text: ''
1611
+
1612
+ htmlTag: ''
1613
+
1614
+ disableTag: ''
1615
+
1616
+ menu: false
1617
+
1618
+ active: false
1619
+
1620
+ disabled: false
1621
+
1622
+ needFocus: true
1623
+
1624
+ shortcut: null
1625
+
1626
+ constructor: (@editor) ->
1627
+ @render()
1628
+
1629
+ @el.on 'mousedown', (e) =>
1630
+ e.preventDefault()
1631
+ if @menu
1632
+ @wrapper.toggleClass('menu-on')
1633
+ .siblings('li')
1634
+ .removeClass('menu-on')
1635
+ return false
1636
+
1637
+ return false if @el.hasClass('disabled') or (@needFocus and !@editor.inputManager.focused)
1638
+
1639
+ param = @el.data('param')
1640
+ @command(param)
1641
+ false
1642
+
1643
+ @wrapper.on 'click', 'a.menu-item', (e) =>
1644
+ e.preventDefault()
1645
+ btn = $(e.currentTarget)
1646
+ @wrapper.removeClass('menu-on')
1647
+ return false if btn.hasClass('disabled') or (@needFocus and !@editor.inputManager.focused)
1648
+
1649
+ @editor.toolbar.wrapper.removeClass('menu-on')
1650
+ param = btn.data('param')
1651
+ @command(param)
1652
+ false
1653
+
1654
+ @wrapper.on 'mousedown', 'a.menu-item', (e) =>
1655
+ false
1656
+
1657
+ @editor.on 'blur', =>
1658
+ @setActive false
1659
+ @setDisabled false
1660
+
1661
+
1662
+ if @shortcut?
1663
+ @editor.inputManager.addShortcut @shortcut, (e) =>
1664
+ @el.mousedown()
1665
+
1666
+ for tag in @htmlTag.split ','
1667
+ tag = $.trim tag
1668
+ if tag && $.inArray(tag, @editor.formatter._allowedTags) < 0
1669
+ @editor.formatter._allowedTags.push tag
1670
+
1671
+ render: ->
1672
+ @wrapper = $(@_tpl.item).appendTo @editor.toolbar.list
1673
+ @el = @wrapper.find 'a.toolbar-item'
1674
+
1675
+ @el.attr('title', @title)
1676
+ .addClass('toolbar-item-' + @name)
1677
+ .data('button', @)
1678
+
1679
+ @el.find('span')
1680
+ .addClass(if @icon then 'fa fa-' + @icon else '')
1681
+ .text(@text)
1682
+
1683
+ return unless @menu
1684
+
1685
+ @menuWrapper = $(@_tpl.menuWrapper).appendTo(@wrapper)
1686
+ @menuWrapper.addClass 'toolbar-menu-' + @name
1687
+ @renderMenu()
1688
+
1689
+ renderMenu: ->
1690
+ return unless $.isArray @menu
1691
+
1692
+ @menuEl = $('<ul/>').appendTo @menuWrapper
1693
+ for menuItem in @menu
1694
+ if menuItem == '|'
1695
+ $(@_tpl.separator).appendTo @menuEl
1696
+ continue
1697
+
1698
+ $menuItemEl = $(@_tpl.menuItem).appendTo @menuEl
1699
+ $menuBtntnEl = $menuItemEl.find('a.menu-item')
1700
+ .attr(
1701
+ 'title': menuItem.title ? menuItem.text,
1702
+ 'data-param': menuItem.param
1703
+ )
1704
+ .addClass('menu-item-' + menuItem.name)
1705
+ .find('span')
1706
+ .text(menuItem.text)
1707
+
1708
+ setActive: (active) ->
1709
+ @active = active
1710
+ @el.toggleClass('active', @active)
1711
+
1712
+ setDisabled: (disabled) ->
1713
+ @disabled = disabled
1714
+ @el.toggleClass('disabled', @disabled)
1715
+
1716
+ status: ($node) ->
1717
+ @setDisabled $node.is(@disableTag) if $node?
1718
+ return true if @disabled
1719
+
1720
+ @setActive $node.is(@htmlTag) if $node?
1721
+ @active
1722
+
1723
+ command: (param) ->
1724
+
1725
+
1726
+ window.SimditorButton = Button
1727
+
1728
+
1729
+ class Popover extends Module
1730
+
1731
+ offset:
1732
+ top: 4
1733
+ left: 0
1734
+
1735
+ target: null
1736
+
1737
+ active: false
1738
+
1739
+ constructor: (@editor) ->
1740
+ @el = $('<div class="simditor-popover"></div>')
1741
+ .appendTo(@editor.wrapper)
1742
+ .data('popover', @)
1743
+ @render()
1744
+
1745
+ @editor.on 'blur.linkpopover', =>
1746
+ @target.addClass('selected') if @active and @target?
1747
+
1748
+ @el.on 'mouseenter', (e) =>
1749
+ @el.addClass 'hover'
1750
+ @el.on 'mouseleave', (e) =>
1751
+ @el.removeClass 'hover'
1752
+
1753
+ render: ->
1754
+
1755
+ show: ($target, position = 'bottom') ->
1756
+ return unless $target?
1757
+ @target = $target
1758
+
1759
+ @el.siblings('.simditor-popover').each (i, el) =>
1760
+ popover = $(el).data('popover')
1761
+ popover.hide()
1762
+
1763
+ if @active
1764
+ @refresh(position)
1765
+ @trigger 'popovershow'
1766
+ else
1767
+ @active = true
1768
+
1769
+ @el.css({
1770
+ left: -9999
1771
+ }).show()
1772
+
1773
+ setTimeout =>
1774
+ @refresh(position)
1775
+ @trigger 'popovershow'
1776
+ , 0
1777
+
1778
+ hide: ->
1779
+ return unless @active
1780
+ @target.removeClass('selected') if @target
1781
+ @target = null
1782
+ @active = false
1783
+ @el.hide()
1784
+ @trigger 'popoverhide'
1785
+
1786
+ refresh: (position = 'bottom') ->
1787
+ wrapperOffset = @editor.wrapper.offset()
1788
+ targetOffset = @target.offset()
1789
+ targetH = @target.outerHeight()
1790
+
1791
+ if position is 'bottom'
1792
+ top = targetOffset.top - wrapperOffset.top + targetH
1793
+ else if position is 'top'
1794
+ top = targetOffset.top - wrapperOffset.top - @el.height()
1795
+
1796
+ left = Math.min(targetOffset.left - wrapperOffset.left, @editor.wrapper.width() - @el.outerWidth() - 10)
1797
+
1798
+ @el.css({
1799
+ top: top + @offset.top,
1800
+ left: left + @offset.left
1801
+ })
1802
+
1803
+ destroy: () ->
1804
+ @target = null
1805
+ @active = false
1806
+ @editor.off('.linkpopover')
1807
+ @el.remove()
1808
+
1809
+
1810
+ class TitleButton extends Button
1811
+
1812
+ name: 'title'
1813
+
1814
+ title: '加粗文字'
1815
+
1816
+ htmlTag: 'h1, h2, h3, h4'
1817
+
1818
+ disableTag: 'pre, table'
1819
+
1820
+ menu: [{
1821
+ name: 'normal',
1822
+ text: '普通文本',
1823
+ param: 'p'
1824
+ }, '|', {
1825
+ name: 'h1',
1826
+ text: '标题 1',
1827
+ param: 'h1'
1828
+ }, {
1829
+ name: 'h2',
1830
+ text: '标题 2',
1831
+ param: 'h2'
1832
+ }, {
1833
+ name: 'h3',
1834
+ text: '标题 3',
1835
+ param: 'h3'
1836
+ }]
1837
+
1838
+ setActive: (active, param) ->
1839
+ @active = active
1840
+ if active
1841
+ @el.addClass('active active-' + param)
1842
+ else
1843
+ @el.removeClass('active active-p active-h1 active-h2 active-h3')
1844
+
1845
+ status: ($node) ->
1846
+ @setDisabled $node.is(@disableTag) if $node?
1847
+ return true if @disabled
1848
+
1849
+ if $node?
1850
+ param = $node[0].tagName?.toLowerCase()
1851
+ @setActive $node.is(@htmlTag), param
1852
+ @active
1853
+
1854
+ command: (param) ->
1855
+ range = @editor.selection.getRange()
1856
+ startNode = range.startContainer
1857
+ endNode = range.endContainer
1858
+ $startBlock = @editor.util.closestBlockEl(startNode)
1859
+ $endBlock = @editor.util.closestBlockEl(endNode)
1860
+
1861
+ @editor.selection.save()
1862
+
1863
+ range.setStartBefore $startBlock[0]
1864
+ range.setEndAfter $endBlock[0]
1865
+
1866
+ $contents = $(range.extractContents())
1867
+
1868
+ results = []
1869
+ $contents.children().each (i, el) =>
1870
+ converted = @_convertEl el, param
1871
+ results.push(c) for c in converted
1872
+
1873
+ range.insertNode node[0] for node in results.reverse()
1874
+ @editor.selection.restore()
1875
+
1876
+ @editor.trigger 'valuechanged'
1877
+ @editor.trigger 'selectionchanged'
1878
+
1879
+ _convertEl: (el, param) ->
1880
+ $el = $(el)
1881
+ results = []
1882
+
1883
+ if $el.is param
1884
+ results.push $el
1885
+ else
1886
+ $block = $('<' + param + '/>').append($el.contents())
1887
+ results.push($block)
1888
+
1889
+ results
1890
+
1891
+
1892
+ Simditor.Toolbar.addButton(TitleButton)
1893
+
1894
+
1895
+
1896
+ class BoldButton extends Button
1897
+
1898
+ name: 'bold'
1899
+
1900
+ icon: 'bold'
1901
+
1902
+ title: '加粗文字'
1903
+
1904
+ htmlTag: 'b, strong'
1905
+
1906
+ disableTag: 'pre'
1907
+
1908
+ shortcut: 'cmd+66'
1909
+
1910
+ status: ($node) ->
1911
+ @setDisabled $node.is(@disableTag) if $node?
1912
+ return true if @disabled
1913
+
1914
+ active = document.queryCommandState('bold') is true
1915
+ @setActive active
1916
+ active
1917
+
1918
+ command: ->
1919
+ document.execCommand 'bold'
1920
+ @editor.trigger 'valuechanged'
1921
+ @editor.trigger 'selectionchanged'
1922
+
1923
+
1924
+ Simditor.Toolbar.addButton(BoldButton)
1925
+
1926
+
1927
+ class ItalicButton extends Button
1928
+
1929
+ name: 'italic'
1930
+
1931
+ icon: 'italic'
1932
+
1933
+ title: '斜体文字'
1934
+
1935
+ htmlTag: 'i'
1936
+
1937
+ disableTag: 'pre'
1938
+
1939
+ shortcut: 'cmd+73'
1940
+
1941
+ status: ($node) ->
1942
+ @setDisabled $node.is(@disableTag) if $node?
1943
+ return @disabled if @disabled
1944
+
1945
+ active = document.queryCommandState('italic') is true
1946
+ @setActive active
1947
+ active
1948
+
1949
+ command: ->
1950
+ document.execCommand 'italic'
1951
+ @editor.trigger 'valuechanged'
1952
+ @editor.trigger 'selectionchanged'
1953
+
1954
+
1955
+ Simditor.Toolbar.addButton(ItalicButton)
1956
+
1957
+
1958
+
1959
+ class UnderlineButton extends Button
1960
+
1961
+ name: 'underline'
1962
+
1963
+ icon: 'underline'
1964
+
1965
+ title: '下划线文字'
1966
+
1967
+ htmlTag: 'u'
1968
+
1969
+ disableTag: 'pre'
1970
+
1971
+ shortcut: 'cmd+85'
1972
+
1973
+ status: ($node) ->
1974
+ @setDisabled $node.is(@disableTag) if $node?
1975
+ return @disabled if @disabled
1976
+
1977
+ active = document.queryCommandState('underline') is true
1978
+ @setActive active
1979
+ active
1980
+
1981
+ command: ->
1982
+ document.execCommand 'underline'
1983
+ @editor.trigger 'valuechanged'
1984
+ @editor.trigger 'selectionchanged'
1985
+
1986
+
1987
+ Simditor.Toolbar.addButton(UnderlineButton)
1988
+
1989
+
1990
+
1991
+
1992
+ class ListButton extends Button
1993
+
1994
+ type: ''
1995
+
1996
+ disableTag: 'pre, table'
1997
+
1998
+ status: ($node) ->
1999
+ @setDisabled $node.is(@disableTag) if $node?
2000
+ return true if @disabled
2001
+ return @active unless $node?
2002
+
2003
+ anotherType = if @type == 'ul' then 'ol' else 'ul'
2004
+ if $node.is anotherType
2005
+ @setActive false
2006
+ return true
2007
+ else
2008
+ @setActive $node.is(@htmlTag)
2009
+ return @active
2010
+
2011
+ command: (param) ->
2012
+ range = @editor.selection.getRange()
2013
+ startNode = range.startContainer
2014
+ endNode = range.endContainer
2015
+ $startBlock = @editor.util.closestBlockEl(startNode)
2016
+ $endBlock = @editor.util.closestBlockEl(endNode)
2017
+
2018
+ @editor.selection.save()
2019
+
2020
+ range.setStartBefore $startBlock[0]
2021
+ range.setEndAfter $endBlock[0]
2022
+
2023
+ if $startBlock.is('li') and $endBlock.is('li')
2024
+ $furthestStart = @editor.util.furthestNode $startBlock, 'ul, ol'
2025
+ $furthestEnd = @editor.util.furthestNode $endBlock, 'ul, ol'
2026
+ if $furthestStart.is $furthestEnd
2027
+ getListLevel = ($li) ->
2028
+ lvl = 1
2029
+ while !$li.parent().is $furthestStart
2030
+ lvl += 1
2031
+ $li = $li.parent()
2032
+ return lvl
2033
+
2034
+ startLevel = getListLevel $startBlock
2035
+ endLevel = getListLevel $endBlock
2036
+
2037
+ if startLevel > endLevel
2038
+ $parent = $endBlock.parent()
2039
+ else
2040
+ $parent = $startBlock.parent()
2041
+
2042
+ range.setStartBefore $parent[0]
2043
+ range.setEndAfter $parent[0]
2044
+ else
2045
+ range.setStartBefore $furthestStart[0]
2046
+ range.setEndAfter $furthestEnd[0]
2047
+
2048
+ $contents = $(range.extractContents())
2049
+
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
+ results = []
2065
+ $contents.children().each (i, el) =>
2066
+ converted = @_convertEl el
2067
+ for c in converted
2068
+ if results.length and results[results.length - 1].is(@type) and c.is(@type)
2069
+ results[results.length - 1].append(c.children())
2070
+ else
2071
+ results.push(c)
2072
+
2073
+ range.insertNode node[0] for node in results.reverse()
2074
+ @editor.selection.restore()
2075
+
2076
+ @editor.trigger 'valuechanged'
2077
+ @editor.trigger 'selectionchanged'
2078
+
2079
+ _convertEl: (el) ->
2080
+ $el = $(el)
2081
+ results = []
2082
+ anotherType = if @type == 'ul' then 'ol' else 'ul'
2083
+
2084
+ if $el.is @type
2085
+ $el.children('li').each (i, li) =>
2086
+ $li = $(li)
2087
+ $childList = $li.children('ul, ol').remove()
2088
+ block = $('<p/>').append($(li).html() || @editor.util.phBr)
2089
+ results.push block
2090
+ results.push $childList if $childList.length > 0
2091
+ else if $el.is anotherType
2092
+ block = $('<' + @type + '/>').append($el.html())
2093
+ results.push(block)
2094
+ else if $el.is 'blockquote'
2095
+ children = @_convertEl child for child in $el.children().get()
2096
+ $.merge results, children
2097
+ else if $el.is 'table'
2098
+ # TODO
2099
+ else
2100
+ block = $('<' + @type + '><li></li></' + @type + '>')
2101
+ block.find('li').append($el.html() || @editor.util.phBr)
2102
+ results.push(block)
2103
+
2104
+ results
2105
+
2106
+
2107
+ class OrderListButton extends ListButton
2108
+ type: 'ol'
2109
+ name: 'ol'
2110
+ title: '有序列表'
2111
+ icon: 'list-ol'
2112
+ htmlTag: 'ol'
2113
+
2114
+ class UnorderListButton extends ListButton
2115
+ type: 'ul'
2116
+ name: 'ul'
2117
+ title: '无序列表'
2118
+ icon: 'list-ul'
2119
+ htmlTag: 'ul'
2120
+
2121
+ Simditor.Toolbar.addButton(OrderListButton)
2122
+ Simditor.Toolbar.addButton(UnorderListButton)
2123
+
2124
+
2125
+
2126
+ class BlockquoteButton extends Button
2127
+
2128
+ name: 'blockquote'
2129
+
2130
+ icon: 'quote-left'
2131
+
2132
+ title: '引用'
2133
+
2134
+ htmlTag: 'blockquote'
2135
+
2136
+ disableTag: 'pre, table'
2137
+
2138
+ command: ->
2139
+ range = @editor.selection.getRange()
2140
+ startNode = range.startContainer
2141
+ endNode = range.endContainer
2142
+ $startBlock = @editor.util.furthestBlockEl(startNode)
2143
+ $endBlock = @editor.util.furthestBlockEl(endNode)
2144
+
2145
+ @editor.selection.save()
2146
+
2147
+ range.setStartBefore $startBlock[0]
2148
+ range.setEndAfter $endBlock[0]
2149
+
2150
+ $contents = $(range.extractContents())
2151
+
2152
+ results = []
2153
+ $contents.children().each (i, el) =>
2154
+ converted = @_convertEl el
2155
+ for c in converted
2156
+ if results.length and results[results.length - 1].is(@htmlTag) and c.is(@htmlTag)
2157
+ results[results.length - 1].append(c.children())
2158
+ else
2159
+ results.push(c)
2160
+
2161
+ range.insertNode node[0] for node in results.reverse()
2162
+ @editor.selection.restore()
2163
+
2164
+ @editor.trigger 'valuechanged'
2165
+ @editor.trigger 'selectionchanged'
2166
+
2167
+ _convertEl: (el) ->
2168
+ $el = $(el)
2169
+ results = []
2170
+
2171
+ if $el.is @htmlTag
2172
+ $el.children().each (i, node) =>
2173
+ results.push $(node)
2174
+ else
2175
+ block = $('<' + @htmlTag + '/>').append($el)
2176
+ results.push(block)
2177
+
2178
+ results
2179
+
2180
+
2181
+
2182
+ Simditor.Toolbar.addButton(BlockquoteButton)
2183
+
2184
+
2185
+
2186
+ class CodeButton extends Button
2187
+
2188
+ name: 'code'
2189
+
2190
+ icon: 'code'
2191
+
2192
+ title: '插入代码'
2193
+
2194
+ htmlTag: 'pre'
2195
+
2196
+ disableTag: 'li, table'
2197
+
2198
+ render: (args...) ->
2199
+ super args...
2200
+ @popover = new CodePopover(@editor)
2201
+
2202
+ status: ($node) ->
2203
+ result = super $node
2204
+
2205
+ if @active
2206
+ @popover.show($node)
2207
+ else if @editor.util.isBlockNode($node)
2208
+ @popover.hide()
2209
+
2210
+ result
2211
+
2212
+ command: ->
2213
+ range = @editor.selection.getRange()
2214
+ startNode = range.startContainer
2215
+ endNode = range.endContainer
2216
+ $startBlock = @editor.util.closestBlockEl(startNode)
2217
+ $endBlock = @editor.util.closestBlockEl(endNode)
2218
+
2219
+ range.setStartBefore $startBlock[0]
2220
+ range.setEndAfter $endBlock[0]
2221
+
2222
+ $contents = $(range.extractContents())
2223
+
2224
+ results = []
2225
+ $contents.children().each (i, el) =>
2226
+ converted = @_convertEl el
2227
+ for c in converted
2228
+ if results.length and results[results.length - 1].is(@htmlTag) and c.is(@htmlTag)
2229
+ results[results.length - 1].append(c.contents())
2230
+ else
2231
+ results.push(c)
2232
+
2233
+ range.insertNode node[0] for node in results.reverse()
2234
+ @editor.selection.setRangeAtEndOf results[0]
2235
+
2236
+ @editor.trigger 'valuechanged'
2237
+ @editor.trigger 'selectionchanged'
2238
+
2239
+ _convertEl: (el) ->
2240
+ $el = $(el)
2241
+ results = []
2242
+
2243
+ if $el.is @htmlTag
2244
+ block = $('<p/>').append($el.html().replace('\n', '<br/>'))
2245
+ results.push block
2246
+ else
2247
+ if !$el.text() and $el.children().length == 1 and $el.children().is('br')
2248
+ codeStr = '\n'
2249
+ else
2250
+ codeStr = @editor.formatter.clearHtml($el)
2251
+ block = $('<' + @htmlTag + '/>').append(codeStr)
2252
+ results.push(block)
2253
+
2254
+ results
2255
+
2256
+
2257
+ class CodePopover extends Popover
2258
+
2259
+ _tpl: """
2260
+ <div class="code-settings">
2261
+ <div class="settings-field">
2262
+ <select class="select-lang">
2263
+ <option value="-1">选择程序语言</option>
2264
+ <option value="c++">C++</option>
2265
+ <option value="css">CSS</option>
2266
+ <option value="coffeeScript">CoffeeScript</option>
2267
+ <option value="html">Html,XML</option>
2268
+ <option value="json">JSON</option>
2269
+ <option value="java">Java</option>
2270
+ <option value="js">JavaScript</option>
2271
+ <option value="markdown">Markdown</option>
2272
+ <option value="oc">Objective C</option>
2273
+ <option value="php">PHP</option>
2274
+ <option value="perl">Perl</option>
2275
+ <option value="python">Python</option>
2276
+ <option value="ruby">Ruby</option>
2277
+ <option value="sql">SQL</option>
2278
+ </select>
2279
+ </div>
2280
+ </div>
2281
+ """
2282
+
2283
+ render: ->
2284
+ @el.addClass('code-popover')
2285
+ .append(@_tpl)
2286
+ @selectEl = @el.find '.select-lang'
2287
+
2288
+ @selectEl.on 'change', (e) =>
2289
+ lang = @selectEl.val()
2290
+ oldLang = @target.attr('data-lang')
2291
+ @target.removeClass('lang-' + oldLang)
2292
+ .removeAttr('data-lang')
2293
+
2294
+ if @lang isnt -1
2295
+ @target.addClass('lang-' + lang)
2296
+ @target.attr('data-lang', lang)
2297
+
2298
+ show: (args...) ->
2299
+ super args...
2300
+ @lang = @target.attr('data-lang')
2301
+ @selectEl.val(@lang) if @lang?
2302
+
2303
+
2304
+ Simditor.Toolbar.addButton(CodeButton)
2305
+
2306
+
2307
+
2308
+
2309
+ class LinkButton extends Button
2310
+
2311
+ name: 'link'
2312
+
2313
+ icon: 'link'
2314
+
2315
+ title: '插入链接'
2316
+
2317
+ htmlTag: 'a'
2318
+
2319
+ disableTag: 'pre'
2320
+
2321
+ render: (args...) ->
2322
+ super args...
2323
+ @popover = new LinkPopover(@editor)
2324
+
2325
+ status: ($node) ->
2326
+ @setDisabled $node.is(@disableTag) if $node?
2327
+ return true if @disabled
2328
+
2329
+ return @active unless $node?
2330
+
2331
+ showPopover = true
2332
+ if !$node.is(@htmlTag) or $node.is('[class^="simditor-"]')
2333
+ @setActive false
2334
+ showPopover = false
2335
+ else if @editor.selection.rangeAtEndOf($node)
2336
+ @setActive true
2337
+ showPopover = false
2338
+ else
2339
+ @setActive true
2340
+
2341
+ if showPopover
2342
+ @popover.show($node)
2343
+ else if @editor.util.isBlockNode($node)
2344
+ @popover.hide()
2345
+
2346
+ @active
2347
+
2348
+ command: ->
2349
+ range = @editor.selection.getRange()
2350
+
2351
+ if @active
2352
+ $link = $(range.commonAncestorContainer).closest('a')
2353
+ txtNode = document.createTextNode $link.text()
2354
+ $link.replaceWith txtNode
2355
+ range.selectNode txtNode
2356
+ else
2357
+ startNode = range.startContainer
2358
+ endNode = range.endContainer
2359
+ $startBlock = @editor.util.closestBlockEl(startNode)
2360
+ $endBlock = @editor.util.closestBlockEl(endNode)
2361
+
2362
+ $contents = $(range.extractContents())
2363
+ linkText = @editor.formatter.clearHtml($contents.contents(), false)
2364
+ $link = $('<a/>', {
2365
+ href: 'http://www.example.com',
2366
+ target: '_blank',
2367
+ text: linkText || '链接文字'
2368
+ })
2369
+
2370
+ if $startBlock[0] == $endBlock[0]
2371
+ range.insertNode $link[0]
2372
+ else
2373
+ $newBlock = $('<p/>').append($link)
2374
+ range.insertNode $newBlock[0]
2375
+
2376
+ range.selectNodeContents $link[0]
2377
+
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()
2387
+
2388
+ @editor.trigger 'valuechanged'
2389
+ @editor.trigger 'selectionchanged'
2390
+
2391
+
2392
+ class LinkPopover extends Popover
2393
+
2394
+ _tpl: """
2395
+ <div class="link-settings">
2396
+ <div class="settings-field">
2397
+ <label>文本</label>
2398
+ <input class="link-text" type="text"/>
2399
+ <a class="btn-unlink" href="javascript:;" title="取消链接" tabindex="-1"><span class="fa fa-unlink"></span></a>
2400
+ </div>
2401
+ <div class="settings-field">
2402
+ <label>链接</label>
2403
+ <input class="link-url" type="text"/>
2404
+ </div>
2405
+ </div>
2406
+ """
2407
+
2408
+ render: ->
2409
+ @el.addClass('link-popover')
2410
+ .append(@_tpl)
2411
+ @textEl = @el.find '.link-text'
2412
+ @urlEl = @el.find '.link-url'
2413
+ @unlinkEl = @el.find '.btn-unlink'
2414
+
2415
+ @textEl.on 'keyup', (e) =>
2416
+ return if e.which == 13
2417
+ @target.text @textEl.val()
2418
+
2419
+ @urlEl.on 'keyup', (e) =>
2420
+ return if e.which == 13
2421
+ @target.attr 'href', @urlEl.val()
2422
+
2423
+ $([@urlEl[0], @textEl[0]]).on 'keydown', (e) =>
2424
+ if e.which == 13 or e.which == 27 or (!e.shiftKey and e.which == 9 and $(e.target).hasClass('link-url'))
2425
+ e.preventDefault()
2426
+ setTimeout =>
2427
+ range = document.createRange()
2428
+ @editor.selection.setRangeAfter @target, range
2429
+ @editor.body.focus() if @editor.util.browser.firefox
2430
+ @hide()
2431
+ @editor.trigger 'valuechanged'
2432
+ , 0
2433
+
2434
+ @unlinkEl.on 'click', (e) =>
2435
+ txtNode = document.createTextNode @target.text()
2436
+ @target.replaceWith txtNode
2437
+ @hide()
2438
+
2439
+ range = document.createRange()
2440
+ @editor.selection.setRangeAfter txtNode, range
2441
+ @editor.body.focus() if @editor.util.browser.firefox and !@editor.inputManager.focused
2442
+
2443
+ show: (args...) ->
2444
+ super args...
2445
+ @textEl.val @target.text()
2446
+ @urlEl.val @target.attr('href')
2447
+
2448
+
2449
+
2450
+ Simditor.Toolbar.addButton(LinkButton)
2451
+
2452
+
2453
+
2454
+ class ImageButton extends Button
2455
+
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
+ name: 'image'
2465
+
2466
+ icon: 'picture-o'
2467
+
2468
+ title: '插入图片'
2469
+
2470
+ htmlTag: 'img'
2471
+
2472
+ disableTag: 'pre, table'
2473
+
2474
+ defaultImage: ''
2475
+
2476
+ maxWidth: 0
2477
+
2478
+ maxHeight: 0
2479
+
2480
+ menu: [{
2481
+ name: 'upload-image',
2482
+ text: '本地图片'
2483
+ }, {
2484
+ name: 'external-image',
2485
+ text: '外链图片'
2486
+ }]
2487
+
2488
+ constructor: (@editor) ->
2489
+ @menu = false unless @editor.uploader?
2490
+ super @editor
2491
+
2492
+ @defaultImage = @editor.opts.defaultImage
2493
+ @maxWidth = @editor.opts.maxImageWidth || @editor.body.width()
2494
+ @maxHeight = @editor.opts.maxImageHeight || $(window).height()
2495
+
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)
2503
+
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
2519
+
2520
+ false
2521
+
2522
+ @editor.body.on 'click', '.simditor-image', (e) =>
2523
+ false
2524
+
2525
+ @editor.on 'selectionchanged.image', =>
2526
+ range = @editor.selection.getRange()
2527
+ return unless range?
2528
+ $container = $(range.commonAncestorContainer)
2529
+
2530
+ if range.collapsed and $container.is('.simditor-image')
2531
+ $container.mousedown()
2532
+ else if @popover.active
2533
+ @popover.hide() if @popover.active
2534
+
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
+
2542
+ render: (args...) ->
2543
+ super args...
2544
+ @popover = new ImagePopover(@)
2545
+
2546
+ renderMenu: ->
2547
+ super()
2548
+
2549
+ $uploadItem = @menuEl.find('.menu-item-upload-image')
2550
+ $input = $('<input type="file" title="上传图片" name="upload_file" accept="image/*">')
2551
+ .appendTo($uploadItem)
2552
+
2553
+ $input.on 'click mousedown', (e) =>
2554
+ e.stopPropagation()
2555
+
2556
+ $input.on 'change', (e) =>
2557
+ if @editor.inputManager.focused
2558
+ @editor.uploader.upload($input, {
2559
+ inline: true
2560
+ })
2561
+ $input.val ''
2562
+ else if @editor.inputManager.lastCaretPosition
2563
+ @editor.one 'focus', (e) =>
2564
+ @editor.uploader.upload($input, {
2565
+ inline: true
2566
+ })
2567
+ $input = $input.clone(true).replaceAll($input)
2568
+ @editor.undoManager.caretPosition @editor.inputManager.lastCaretPosition
2569
+ @wrapper.removeClass('menu-on')
2570
+
2571
+ @_initUploader()
2572
+
2573
+ _initUploader: ->
2574
+ unless @editor.uploader?
2575
+ @el.find('.btn-upload').remove()
2576
+ return
2577
+
2578
+ @editor.uploader.on 'beforeupload', (e, file) =>
2579
+ return unless file.inline
2580
+
2581
+ if file.imgWrapper
2582
+ $img = file.imgWrapper.find("img")
2583
+ else
2584
+ $img = @createImage()
2585
+ $img.mousedown()
2586
+ file.imgWrapper = $img.parent '.simditor-image'
2587
+
2588
+ @editor.uploader.readImageFile file.obj, (img) =>
2589
+ prepare = () =>
2590
+ @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()
2601
+
2602
+ @editor.uploader.on 'uploadprogress', (e, file, loaded, total) =>
2603
+ return unless file.inline
2604
+
2605
+ 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)
2613
+
2614
+ @editor.uploader.on 'uploadsuccess', (e, file, result) =>
2615
+ return unless file.inline
2616
+
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'
2622
+
2623
+ @editor.uploader.on 'uploaderror', (e, file, xhr) =>
2624
+ return if xhr.statusText == 'abort'
2625
+
2626
+ if xhr.responseText
2627
+ try
2628
+ result = $.parseJSON xhr.responseText
2629
+ msg = result.msg
2630
+ catch e
2631
+ msg = '上传出错了'
2632
+
2633
+ if simple? and simple.message?
2634
+ simple.message(msg)
2635
+ else
2636
+ alert(msg)
2637
+
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'
2644
+
2645
+ status: ($node) ->
2646
+ @setDisabled $node.is(@disableTag) if $node?
2647
+ return true if @disabled
2648
+
2649
+ decorate: ($img) ->
2650
+ $wrapper = $img.parent('.simditor-image')
2651
+ return if $wrapper.length > 0
2652
+
2653
+ $wrapper = $(@_wrapperTpl)
2654
+ .insertBefore($img)
2655
+ .prepend($img)
2656
+
2657
+ undecorate: ($img) ->
2658
+ $wrapper = $img.parent('.simditor-image')
2659
+ return if $wrapper.length < 1
2660
+
2661
+ $img.insertAfter $wrapper unless $img.is('img[src^="data:image/png;base64"]')
2662
+ $wrapper.remove()
2663
+
2664
+ loadImage: ($img, src, callback) ->
2665
+ $wrapper = $img.parent('.simditor-image')
2666
+ img = new Image()
2667
+
2668
+ img.onload = =>
2669
+ width = img.width
2670
+ 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
2677
+
2678
+ $img.attr({
2679
+ src: src,
2680
+ width: width,
2681
+ 'data-origin-src': src,
2682
+ 'data-origin-name': '图片',
2683
+ 'data-origin-size': img.width + ',' + img.height
2684
+ })
2685
+
2686
+ $wrapper.width(width)
2687
+ .height(height)
2688
+
2689
+ callback(true)
2690
+
2691
+ img.onerror = =>
2692
+ callback(false)
2693
+
2694
+ img.src = src
2695
+
2696
+ createImage: () ->
2697
+ 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
+ range.deleteContents()
2704
+
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)
2725
+
2726
+ $img = $('<img/>')
2727
+ range.insertNode $img[0]
2728
+ @decorate $img
2729
+ $img
2730
+
2731
+ command: () ->
2732
+ $img = @createImage()
2733
+
2734
+ @loadImage $img, @defaultImage, =>
2735
+ @editor.trigger 'valuechanged'
2736
+ $img.mousedown()
2737
+
2738
+ @popover.one 'popovershow', =>
2739
+ @popover.srcEl.focus()
2740
+ @popover.srcEl[0].select()
2741
+
2742
+
2743
+ class ImagePopover extends Popover
2744
+
2745
+ _tpl: """
2746
+ <div class="link-settings">
2747
+ <div class="settings-field">
2748
+ <label>图片地址</label>
2749
+ <input class="image-src" type="text"/>
2750
+ <a class="btn-upload" href="javascript:;" title="上传图片" tabindex="-1">
2751
+ <span class="fa fa-upload"></span>
2752
+ <input type="file" title="上传图片" name="upload_file" accept="image/*">
2753
+ </a>
2754
+ </div>
2755
+ </div>
2756
+ """
2757
+
2758
+ offset:
2759
+ top: 6
2760
+ left: -4
2761
+
2762
+ constructor: (@button) ->
2763
+ super @button.editor
2764
+
2765
+ render: ->
2766
+ @el.addClass('image-popover')
2767
+ .append(@_tpl)
2768
+ @srcEl = @el.find '.image-src'
2769
+
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
+ @srcEl.on 'keydown', (e) =>
2786
+ if e.which == 13 or e.which == 27 or e.which == 9
2787
+ e.preventDefault()
2788
+ @srcEl.blur()
2789
+ @target.removeClass('selected')
2790
+ @hide()
2791
+
2792
+ @_initUploader()
2793
+
2794
+ _initUploader: ->
2795
+ unless @editor.uploader?
2796
+ @el.find('.btn-upload').remove()
2797
+ return
2798
+
2799
+ @input = @el.find 'input:file'
2800
+
2801
+ @input.on 'click mousedown', (e) =>
2802
+ e.stopPropagation()
2803
+
2804
+ @input.on 'change', (e) =>
2805
+ @editor.uploader.upload(@input, {
2806
+ inline: true,
2807
+ imgWrapper: @target
2808
+ })
2809
+
2810
+ @input = @input.clone(true).replaceAll(@input)
2811
+
2812
+ show: (args...) ->
2813
+ super args...
2814
+ $img = @target.find('img')
2815
+ @srcEl.val $img.attr('src')
2816
+
2817
+
2818
+ Simditor.Toolbar.addButton(ImageButton)
2819
+
2820
+
2821
+
2822
+
2823
+ class IndentButton extends Button
2824
+
2825
+ name: 'indent'
2826
+
2827
+ icon: 'indent'
2828
+
2829
+ title: '向右缩进(Tab)'
2830
+
2831
+ status: ($node) ->
2832
+ true
2833
+
2834
+ command: ->
2835
+ @editor.util.indent()
2836
+
2837
+
2838
+ Simditor.Toolbar.addButton(IndentButton)
2839
+
2840
+
2841
+
2842
+ class OutdentButton extends Button
2843
+
2844
+ name: 'outdent'
2845
+
2846
+ icon: 'outdent'
2847
+
2848
+ title: '向左缩进(Shift + Tab)'
2849
+
2850
+ status: ($node) ->
2851
+ true
2852
+
2853
+ command: ->
2854
+ @editor.util.outdent()
2855
+
2856
+
2857
+ Simditor.Toolbar.addButton(OutdentButton)
2858
+
2859
+
2860
+
2861
+
2862
+ class HrButton extends Button
2863
+
2864
+ name: 'hr'
2865
+
2866
+ icon: 'minus'
2867
+
2868
+ title: '分隔线'
2869
+
2870
+ htmlTag: 'hr'
2871
+
2872
+ status: ($node) ->
2873
+ true
2874
+
2875
+ command: ->
2876
+ $rootBlock = @editor.util.furthestBlockEl()
2877
+ $nextBlock = $rootBlock.next()
2878
+
2879
+ if $nextBlock.length > 0
2880
+ @editor.selection.save()
2881
+ else
2882
+ $newBlock = $('<p/>').append @editor.util.phBr
2883
+
2884
+ $hr = $('<hr/>').insertAfter $rootBlock
2885
+
2886
+ if $newBlock
2887
+ $newBlock.insertAfter $hr
2888
+ @editor.selection.setRangeAtStartOf $newBlock
2889
+ else
2890
+ @editor.selection.restore()
2891
+
2892
+ @editor.trigger 'valuechanged'
2893
+ @editor.trigger 'selectionchanged'
2894
+
2895
+
2896
+ Simditor.Toolbar.addButton(HrButton)
2897
+
2898
+
2899
+
2900
+ class TableButton extends Button
2901
+
2902
+ name: 'table'
2903
+
2904
+ icon: 'table'
2905
+
2906
+ title: '表格'
2907
+
2908
+ htmlTag: 'table'
2909
+
2910
+ disableTag: 'pre, li, blockquote'
2911
+
2912
+ menu: true
2913
+
2914
+ constructor: (args...) ->
2915
+ super args...
2916
+
2917
+ $.merge @editor.formatter._allowedTags, ['tbody', 'tr', 'td', 'colgroup', 'col']
2918
+ $.extend(@editor.formatter._allowedAttributes, {
2919
+ td: ['rowspan', 'colspan'],
2920
+ col: ['width']
2921
+ })
2922
+
2923
+ @editor.on 'decorate', (e, $el) =>
2924
+ $el.find('table').each (i, table) =>
2925
+ @decorate $(table)
2926
+
2927
+ @editor.on 'undecorate', (e, $el) =>
2928
+ $el.find('table').each (i, table) =>
2929
+ @undecorate $(table)
2930
+
2931
+ @editor.on 'selectionchanged.table', (e) =>
2932
+ @editor.body.find('.simditor-table td').removeClass('active')
2933
+ range = @editor.selection.getRange()
2934
+ return unless range?
2935
+ $container = $(range.commonAncestorContainer)
2936
+
2937
+ if range.collapsed and $container.is('.simditor-table')
2938
+ if @editor.selection.rangeAtStartOf $container
2939
+ $container = $container.find('td:first')
2940
+ else
2941
+ $container = $container.find('td:last')
2942
+ @editor.selection.setRangeAtEndOf $container
2943
+
2944
+ $container.closest('td', @editor.body)
2945
+ .addClass('active')
2946
+
2947
+
2948
+ @editor.on 'blur.table', (e) =>
2949
+ @editor.body.find('.simditor-table td').removeClass('active')
2950
+
2951
+ # press left arrow in td
2952
+ #@editor.inputManager.addKeystrokeHandler '37', 'td', (e, $node) =>
2953
+ #@editor.util.outdent()
2954
+ #true
2955
+
2956
+ # press right arrow in td
2957
+ #@editor.inputManager.addKeystrokeHandler '39', 'td', (e, $node) =>
2958
+ #@editor.util.indent()
2959
+ #true
2960
+
2961
+ # press up arrow in td
2962
+ @editor.inputManager.addKeystrokeHandler '38', 'td', (e, $node) =>
2963
+ $tr = $node.parent 'tr'
2964
+ $prevTr = $tr.prev 'tr'
2965
+ return true unless $prevTr.length > 0
2966
+ index = $tr.find('td').index($node)
2967
+ @editor.selection.setRangeAtEndOf $prevTr.find('td').eq(index)
2968
+ true
2969
+
2970
+ # press down arrow in td
2971
+ @editor.inputManager.addKeystrokeHandler '40', 'td', (e, $node) =>
2972
+ $tr = $node.parent 'tr'
2973
+ $nextTr = $tr.next 'tr'
2974
+ return true unless $nextTr.length > 0
2975
+ index = $tr.find('td').index($node)
2976
+ @editor.selection.setRangeAtEndOf $nextTr.find('td').eq(index)
2977
+ true
2978
+
2979
+ initResize: ($table) ->
2980
+ $wrapper = $table.parent '.simditor-table'
2981
+
2982
+ $colgroup = $table.find 'colgroup'
2983
+ if $colgroup.length < 1
2984
+ $colgroup = $('<colgroup/>').prependTo $table
2985
+ $table.find('tr:first td').each (i, td) =>
2986
+ $col = $('<col/>').appendTo $colgroup
2987
+
2988
+ @refreshTableWidth $table
2989
+
2990
+ $resizeHandle = $('<div class="resize-handle" contenteditable="false"></div>')
2991
+ .appendTo($wrapper)
2992
+
2993
+ $wrapper.on 'mousemove', 'td', (e) =>
2994
+ return if $wrapper.hasClass('resizing')
2995
+ $td = $(e.currentTarget)
2996
+ x = e.pageX - $(e.currentTarget).offset().left;
2997
+ $td = $td.prev() if x < 5 and $td.prev().length > 0
2998
+
2999
+ if $td.next('td').length < 1
3000
+ $resizeHandle.hide()
3001
+ return
3002
+
3003
+ if $resizeHandle.data('td')?.is($td)
3004
+ $resizeHandle.show()
3005
+ return
3006
+
3007
+ index = $td.parent().find('td').index($td)
3008
+ $col = $colgroup.find('col').eq(index)
3009
+
3010
+ if $resizeHandle.data('col')?.is($col)
3011
+ $resizeHandle.show()
3012
+ return
3013
+
3014
+ $resizeHandle
3015
+ .css( 'left', $td.position().left + $td.outerWidth() - 5)
3016
+ .data('td', $td)
3017
+ .data('col', $col)
3018
+ .show()
3019
+
3020
+ $wrapper.on 'mouseleave', (e) =>
3021
+ $resizeHandle.hide()
3022
+
3023
+ $wrapper.on 'mousedown', '.resize-handle', (e) =>
3024
+ $handle = $(e.currentTarget)
3025
+ $leftTd = $handle.data 'td'
3026
+ $leftCol = $handle.data 'col'
3027
+ $rightTd = $leftTd.next('td')
3028
+ $rightCol = $leftCol.next('col')
3029
+ startX = e.pageX
3030
+ startLeftWidth = $leftTd.outerWidth() * 1
3031
+ startRightWidth = $rightTd.outerWidth() * 1
3032
+ startHandleLeft = parseFloat $handle.css('left')
3033
+ tableWidth = $leftTd.closest('table').width()
3034
+ minWidth = 50
3035
+
3036
+ $(document).on 'mousemove.simditor-resize-table', (e) =>
3037
+ deltaX = e.pageX - startX
3038
+ leftWidth = startLeftWidth + deltaX
3039
+ rightWidth = startRightWidth - deltaX
3040
+ if leftWidth < minWidth
3041
+ leftWidth = minWidth
3042
+ deltaX = minWidth - startLeftWidth
3043
+ rightWidth = startRightWidth - deltaX
3044
+ else if rightWidth < minWidth
3045
+ rightWidth = minWidth
3046
+ deltaX = startRightWidth - minWidth
3047
+ leftWidth = startLeftWidth + deltaX
3048
+
3049
+ $leftCol.attr 'width', (leftWidth / tableWidth * 100) + '%'
3050
+ $rightCol.attr 'width', (rightWidth / tableWidth * 100) + '%'
3051
+ $handle.css 'left', startHandleLeft + deltaX
3052
+
3053
+ $(document).one 'mouseup.simditor-resize-table', (e) =>
3054
+ $(document).off '.simditor-resize-table'
3055
+ $wrapper.removeClass 'resizing'
3056
+
3057
+ $wrapper.addClass 'resizing'
3058
+ false
3059
+
3060
+ decorate: ($table) ->
3061
+ return if $table.parent('.simditor-table').length > 0
3062
+ $table.wrap '<div class="simditor-table"></div>'
3063
+ @initResize $table
3064
+ $table.parent()
3065
+
3066
+ undecorate: ($table) ->
3067
+ return unless $table.parent('.simditor-table').length > 0
3068
+ $table.parent().replaceWith($table)
3069
+
3070
+ renderMenu: ->
3071
+ $('''
3072
+ <div class="menu-create-table">
3073
+ </div>
3074
+ <div class="menu-edit-table">
3075
+ <ul>
3076
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="deleteRow"><span>删除行</span></a></li>
3077
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="insertRowAbove"><span>在上面插入行</span></a></li>
3078
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="insertRowBelow"><span>在下面插入行</span></a></li>
3079
+ <li><span class="separator"></span></li>
3080
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="deleteCol"><span>删除列</span></a></li>
3081
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="insertColLeft"><span>在左边插入列</span></a></li>
3082
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="insertColRight"><span>在右边插入列</span></a></li>
3083
+ <li><span class="separator"></span></li>
3084
+ <li><a tabindex="-1" unselectable="on" class="menu-item" href="javascript:;" data-param="deleteTable"><span>删除表格</span></a></li>
3085
+ </ul>
3086
+ </div>
3087
+ ''').appendTo(@menuWrapper)
3088
+
3089
+ @createMenu = @menuWrapper.find('.menu-create-table')
3090
+ @editMenu = @menuWrapper.find('.menu-edit-table')
3091
+ @createTable(6, 6).appendTo @createMenu
3092
+
3093
+ @createMenu.on 'mouseenter', 'td', (e) =>
3094
+ @createMenu.find('td').removeClass('selected')
3095
+
3096
+ $td = $(e.currentTarget)
3097
+ $tr = $td.parent()
3098
+ num = $tr.find('td').index($td) + 1
3099
+ $tr.prevAll('tr').addBack().find('td:lt(' + num + ')').addClass('selected')
3100
+
3101
+ @createMenu.on 'mouseleave', (e) =>
3102
+ $(e.currentTarget).find('td').removeClass('selected')
3103
+
3104
+ @createMenu.on 'mousedown', 'td', (e) =>
3105
+ @wrapper.removeClass('menu-on')
3106
+ return unless @editor.inputManager.focused
3107
+
3108
+ $td = $(e.currentTarget)
3109
+ $tr = $td.parent()
3110
+ colNum = $tr.find('td').index($td) + 1
3111
+ rowNum = $tr.prevAll('tr').length + 1
3112
+ $table = @createTable(rowNum, colNum, true)
3113
+
3114
+ $closestBlock = @editor.util.closestBlockEl()
3115
+ if @editor.util.isEmptyNode $closestBlock
3116
+ $closestBlock.replaceWith $table
3117
+ else
3118
+ $closestBlock.after $table
3119
+
3120
+ @decorate $table
3121
+ @editor.selection.setRangeAtStartOf $table.find('td:first')
3122
+ @editor.trigger 'valuechanged'
3123
+ @editor.trigger 'selectionchanged'
3124
+ false
3125
+
3126
+ createTable: (row, col, phBr) ->
3127
+ $table = $('<table/>')
3128
+ $tbody = $('<tbody/>').appendTo $table
3129
+ for r in [0...row]
3130
+ $tr = $('<tr/>').appendTo $tbody
3131
+ for c in [0...col]
3132
+ $td = $('<td/>').appendTo $tr
3133
+ $td.append(@editor.util.phBr) if phBr
3134
+ $table
3135
+
3136
+ refreshTableWidth: ($table)->
3137
+ tableWidth = $table.width()
3138
+ cols = $table.find('col')
3139
+ $table.find('tr:first td').each (i, td) =>
3140
+ $col = cols.eq(i)
3141
+ $col.attr 'width', ($(td).outerWidth() / tableWidth * 100) + '%'
3142
+
3143
+ setActive: (active) ->
3144
+ super active
3145
+
3146
+ if active
3147
+ @createMenu.hide()
3148
+ @editMenu.show()
3149
+ else
3150
+ @createMenu.show()
3151
+ @editMenu.hide()
3152
+
3153
+ deleteRow: ($td) ->
3154
+ $tr = $td.parent 'tr'
3155
+ if $tr.siblings('tr').length < 1
3156
+ @deleteTable $td
3157
+ else
3158
+ $newTr = $tr.next 'tr'
3159
+ $newTr = $tr.prev 'tr' unless $newTr.length > 0
3160
+ index = $tr.find('td').index($td)
3161
+ $tr.remove()
3162
+ @editor.selection.setRangeAtEndOf $newTr.find('td').eq(index)
3163
+
3164
+ insertRow: ($td, direction = 'after') ->
3165
+ $tr = $td.parent 'tr'
3166
+ $table = $tr.closest 'table'
3167
+
3168
+ colNum = 0
3169
+ $table.find('tr').each (i, tr) =>
3170
+ colNum = Math.max colNum, $(tr).find('td').length
3171
+
3172
+ $newTr = $('<tr/>')
3173
+ for i in [1..colNum]
3174
+ $('<td/>').append(@editor.util.phBr).appendTo($newTr)
3175
+
3176
+ $tr[direction] $newTr
3177
+ index = $tr.find('td').index($td)
3178
+ @editor.selection.setRangeAtStartOf $newTr.find('td').eq(index)
3179
+
3180
+ deleteCol: ($td) ->
3181
+ $tr = $td.parent 'tr'
3182
+ if $tr.siblings('tr').length < 1 and $td.siblings('td').length < 1
3183
+ @deleteTable $td
3184
+ else
3185
+ index = $tr.find('td').index($td)
3186
+ $newTd = $td.next 'td'
3187
+ $newTd = $tr.prev 'td' unless $newTd.length > 0
3188
+ $table = $tr.closest 'table'
3189
+
3190
+ $table.find('col').eq(index).remove()
3191
+ $table.find('tr').each (i, tr) =>
3192
+ $(tr).find('td').eq(index).remove()
3193
+ @refreshTableWidth $table
3194
+
3195
+ @editor.selection.setRangeAtEndOf $newTd
3196
+
3197
+ insertCol: ($td, direction = 'after') ->
3198
+ $tr = $td.parent 'tr'
3199
+ index = $tr.find('td').index($td)
3200
+ $table = $td.closest 'table'
3201
+ $col = $table.find('col').eq(index)
3202
+
3203
+ $table.find('tr').each (i, tr) =>
3204
+ $newTd = $('<td/>').append(@editor.util.phBr)
3205
+ $(tr).find('td').eq(index)[direction] $newTd
3206
+
3207
+ $newCol = $('<col/>')
3208
+ $col[direction] $newCol
3209
+
3210
+ tableWidth = $table.width()
3211
+ width = Math.max parseFloat($col.attr('width')) / 2, 50 / tableWidth * 100
3212
+ $col.attr 'width', width + '%'
3213
+ $newCol.attr 'width', width + '%'
3214
+ @refreshTableWidth $table
3215
+
3216
+ $newTd = if direction == 'after' then $td.next('td') else $td.prev('td')
3217
+ @editor.selection.setRangeAtStartOf $newTd
3218
+
3219
+ deleteTable: ($td) ->
3220
+ $table = $td.closest '.simditor-table'
3221
+ $block = $table.next('p')
3222
+ $table.remove()
3223
+ @editor.selection.setRangeAtStartOf($block) if $block.length > 0
3224
+
3225
+ command: (param) ->
3226
+ range = @editor.selection.getRange()
3227
+ $td = $(range.commonAncestorContainer).closest('td')
3228
+ return unless $td.length > 0
3229
+
3230
+ if param == 'deleteRow'
3231
+ @deleteRow $td
3232
+ else if param == 'insertRowAbove'
3233
+ @insertRow $td, 'before'
3234
+ else if param == 'insertRowBelow'
3235
+ @insertRow $td
3236
+ else if param == 'deleteCol'
3237
+ @deleteCol $td
3238
+ else if param == 'insertColLeft'
3239
+ @insertCol $td, 'before'
3240
+ else if param == 'insertColRight'
3241
+ @insertCol $td
3242
+ else if param == 'deleteTable'
3243
+ @deleteTable $td
3244
+ else
3245
+ return
3246
+
3247
+ @editor.trigger 'valuechanged'
3248
+ @editor.trigger 'selectionchanged'
3249
+
3250
+
3251
+ Simditor.Toolbar.addButton TableButton
3252
+