simditoredit-rails 0.0.1

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