simditor-rails 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+