mirror of
https://github.com/paradoxxxzero/butterfly.git
synced 2025-01-12 12:28:13 +00:00
257 lines
6.8 KiB
CoffeeScript
257 lines
6.8 KiB
CoffeeScript
# *-* coding: utf-8 *-*
|
||
# This file is part of butterfly
|
||
#
|
||
# butterfly Copyright (C) 2014 Florian Mounier
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
selection = null
|
||
|
||
cancel = (ev) ->
|
||
ev.preventDefault() if ev.preventDefault
|
||
ev.stopPropagation() if ev.stopPropagation
|
||
ev.cancelBubble = true
|
||
false
|
||
|
||
previous_leaf = (node) ->
|
||
previous = node.previousSibling
|
||
if not previous
|
||
previous = node.parentNode.previousSibling
|
||
if not previous
|
||
previous = node.parentNode.parentNode.previousSibling
|
||
while previous.lastChild
|
||
previous = previous.lastChild
|
||
previous
|
||
|
||
next_leaf = (node) ->
|
||
next = node.nextSibling
|
||
if not next
|
||
next = node.parentNode.nextSibling
|
||
if not next
|
||
next = node.parentNode.parentNode.nextSibling
|
||
while next?.firstChild
|
||
next = next.firstChild
|
||
next
|
||
|
||
class Selection
|
||
constructor: ->
|
||
butterfly.element.classList.add('selection')
|
||
@selection = getSelection()
|
||
|
||
reset: ->
|
||
@selection = getSelection()
|
||
fake_range = document.createRange()
|
||
fake_range.setStart(@selection.anchorNode, @selection.anchorOffset)
|
||
fake_range.setEnd(@selection.focusNode, @selection.focusOffset)
|
||
@start =
|
||
node: @selection.anchorNode
|
||
offset: @selection.anchorOffset
|
||
@end =
|
||
node: @selection.focusNode
|
||
offset: @selection.focusOffset
|
||
|
||
if fake_range.collapsed
|
||
[@start, @end] = [@end, @start]
|
||
|
||
@start_line = @start.node
|
||
while not @start_line.classList or 'line' not in @start_line.classList
|
||
@start_line = @start_line.parentNode
|
||
|
||
@end_line = @end.node
|
||
while not @end_line.classList or 'line' not in @end_line.classList
|
||
@end_line = @end_line.parentNode
|
||
|
||
clear: ->
|
||
@selection.removeAllRanges()
|
||
|
||
destroy: ->
|
||
butterfly.element.classList.remove('selection')
|
||
@clear()
|
||
|
||
text: ->
|
||
@selection.toString().replace(/\u00A0/g, ' ').replace(/\u2007/g, ' ')
|
||
|
||
up: ->
|
||
@go -1
|
||
|
||
down: ->
|
||
@go +1
|
||
|
||
go: (n) ->
|
||
index = butterfly.children.indexOf(@start_line) + n
|
||
return unless 0 <= index < butterfly.children.length
|
||
|
||
until butterfly.children[index].textContent.match /\S/
|
||
index += n
|
||
return unless 0 <= index < butterfly.children.length
|
||
|
||
@select_line index
|
||
|
||
apply: ->
|
||
@clear()
|
||
range = document.createRange()
|
||
range.setStart @start.node, @start.offset
|
||
range.setEnd @end.node, @end.offset
|
||
@selection.addRange range
|
||
|
||
select_line: (index) ->
|
||
line = butterfly.children[index]
|
||
line_start =
|
||
node: line.firstChild
|
||
offset: 0
|
||
|
||
line_end =
|
||
node: line.lastChild
|
||
offset: line.lastChild.textContent.length
|
||
|
||
@start = @walk line_start, /\S/
|
||
@end = @walk line_end, /\S/, true
|
||
|
||
collapsed: (start, end) ->
|
||
fake_range = document.createRange()
|
||
fake_range.setStart(start.node, start.offset)
|
||
fake_range.setEnd(end.node, end.offset)
|
||
fake_range.collapsed
|
||
|
||
shrink_right: ->
|
||
node = @walk @end, /\s/, true
|
||
end = @walk node, /\S/, true
|
||
if not @collapsed(@start, end)
|
||
@end = end
|
||
|
||
shrink_left: ->
|
||
node = @walk @start, /\s/
|
||
start = @walk node, /\S/
|
||
if not @collapsed(start, @end)
|
||
@start = start
|
||
|
||
expand_right: ->
|
||
node = @walk @end, /\S/
|
||
@end = @walk node, /\s/
|
||
|
||
expand_left: ->
|
||
node = @walk @start, /\S/, true
|
||
@start = @walk node, /\s/, true
|
||
|
||
walk: (needle, til, backward=false) ->
|
||
if needle.node.firstChild
|
||
node = needle.node.firstChild
|
||
else
|
||
node = needle.node
|
||
|
||
text = node.textContent
|
||
i = needle.offset
|
||
if backward
|
||
while node
|
||
while i > 0
|
||
if text[--i].match til
|
||
return node: node, offset: i + 1
|
||
node = previous_leaf node
|
||
text = node.textContent
|
||
i = text.length
|
||
else
|
||
while node
|
||
while i < text.length
|
||
if text[i++].match til
|
||
return node: node, offset: i - 1
|
||
node = next_leaf node
|
||
text = node.textContent
|
||
i = 0
|
||
|
||
return needle
|
||
|
||
document.addEventListener 'keydown', (e) ->
|
||
return true if e.keyCode in [16..19]
|
||
|
||
# Paste natural selection too if shiftkey
|
||
if e.shiftKey and e.keyCode is 13 and
|
||
not selection and not getSelection().isCollapsed
|
||
butterfly.send getSelection().toString()
|
||
getSelection().removeAllRanges()
|
||
return cancel e
|
||
|
||
if selection
|
||
selection.reset()
|
||
if not e.ctrlKey and e.shiftKey and 37 <= e.keyCode <= 40
|
||
return true
|
||
if e.shiftKey and e.ctrlKey
|
||
if e.keyCode == 38
|
||
selection.up()
|
||
else if e.keyCode == 40
|
||
selection.down()
|
||
else if e.keyCode == 39
|
||
selection.shrink_left()
|
||
else if e.keyCode == 38
|
||
selection.expand_left()
|
||
else if e.keyCode == 37
|
||
selection.shrink_right()
|
||
else if e.keyCode == 40
|
||
selection.expand_right()
|
||
else
|
||
return cancel e
|
||
|
||
selection?.apply()
|
||
return cancel e
|
||
|
||
# Start selection mode with shift up
|
||
if not selection and e.ctrlKey and e.shiftKey and e.keyCode == 38
|
||
selection = new Selection()
|
||
selection.select_line butterfly.y - 1
|
||
selection.apply()
|
||
return cancel e
|
||
true
|
||
|
||
document.addEventListener 'keyup', (e) ->
|
||
return true if e.keyCode in [16..19]
|
||
|
||
if selection
|
||
if e.keyCode == 13
|
||
butterfly.send selection.text()
|
||
selection.destroy()
|
||
selection = null
|
||
return cancel e
|
||
if e.keyCode not in [37..40]
|
||
selection.destroy()
|
||
selection = null
|
||
return true
|
||
true
|
||
|
||
document.addEventListener 'dblclick', (e) ->
|
||
return if e.ctrlKey or e.altkey
|
||
sel = getSelection()
|
||
return if sel.isCollapsed or sel.toString().match /\s/
|
||
|
||
range = document.createRange()
|
||
range.setStart(sel.anchorNode, sel.anchorOffset)
|
||
range.setEnd(sel.focusNode, sel.focusOffset)
|
||
if range.collapsed
|
||
sel.removeAllRanges()
|
||
new_range = document.createRange()
|
||
new_range.setStart(sel.focusNode, sel.focusOffset)
|
||
new_range.setEnd(sel.anchorNode, sel.anchorOffset)
|
||
sel.addRange(new_range)
|
||
|
||
until sel.toString().match(/\s/) or not sel.toString()
|
||
sel.modify 'extend', 'forward', 'character'
|
||
|
||
sel.modify 'extend', 'backward', 'character'
|
||
|
||
# Return selection
|
||
anchorNode = sel.anchorNode
|
||
anchorOffset = sel.anchorOffset
|
||
sel.collapseToEnd()
|
||
sel.extend(anchorNode, anchorOffset)
|
||
|
||
until sel.toString().match(/\s/) or not sel.toString()
|
||
sel.modify 'extend', 'backward', 'character'
|
||
|
||
sel.modify 'extend', 'forward', 'character'
|