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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +2 -0
- data/lib/simditor/rails/engine.rb +9 -0
- data/lib/simditor/rails/version.rb +5 -0
- data/lib/simditor/rails.rb +8 -0
- data/simditoredit-rails.gemspec +21 -0
- data/vendor/assets/images/loading-upload.gif +0 -0
- data/vendor/assets/javascripts/simditor/module.js +151 -0
- data/vendor/assets/javascripts/simditor/simditor.coffee +3649 -0
- data/vendor/assets/javascripts/simditor/uploader.js +287 -0
- data/vendor/assets/javascripts/simditor.js +3 -0
- data/vendor/assets/stylesheets/simditor.scss +3 -0
- data/vendor/assets/stylesheets/simditors/font-awesome.css.scss.erb +1567 -0
- data/vendor/assets/stylesheets/simditors/simditor.scss +675 -0
- metadata +88 -0
@@ -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)
|