ret = ret.substr 0, clip + 1
return ret
+# table too look up the properties of various values for css's white-space
+ws_props =
+ normal:
+ space: false # spaces are not preserved/rendered
+ newline: false # newlines are not preserved/rendered
+ wrap: true # text is word-wrapped
+ to_preserve: 'pre-wrap' # to preservespaces, change white-space to this
+ nowrap:
+ space: false
+ newline: false
+ wrap: false
+ to_preserve: 'pre'
+ 'pre-line':
+ space: false
+ newline: true
+ wrap: true
+ to_preserve: 'pre-wrap'
+ pre:
+ space: true
+ newline: true
+ wrap: false
+ 'pre-wrap':
+ space: true
+ newline: true
+ wrap: true
+
# xml 1.0 spec, chromium and firefox accept these, plus lots of unicode chars
valid_attr_regex = new RegExp '^[a-zA-Z_:][-a-zA-Z0-9_:.]*$'
# html5 spec is much more lax, but chromium won't let me make at attribute with the name "4"
range.setEnd el, end
rects = range.getClientRects()
if rects.length > 0
- rect = rects[0]
+ if rects.length > 1
+ if rects[1].width > rects[0].width
+ rect = rects[1]
+ else
+ rect = rects[0]
+ else
+ rect = rects[0]
else
return null
doc = el.ownerDocument.documentElement
return false
when KEY_BACKSPACE
return false unless @cursor?
- return false unless @cursor[1] > 0
- @cursor[0].text = @cursor[0].text.substr(0, @cursor[1] - 1) + @cursor[0].text.substr(@cursor[1])
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1] - 1]
+ return false unless @cursor.i > 0
+ @remove_character @cursor.n, @cursor.i - 1
+ @adjust_whitespace_style @cursor.n
@changed()
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
return false
when KEY_DELETE
return false unless @cursor?
- return false unless @cursor[1] < @cursor[0].text.length
- @cursor[0].text = @cursor[0].text.substr(0, @cursor[1]) + @cursor[0].text.substr(@cursor[1] + 1)
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1]]
+ return false unless @cursor.i < @cursor.n.text.length
+ @remove_character @cursor.n, @cursor.i
+ @adjust_whitespace_style @cursor.n
@changed()
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i
+ if new_cursor?
+ @move_cursor new_cursor
+ else
+ @kill_cursor()
return false
when KEY_ENTER
return false
onkeypress: (e) ->
return if e.ctrlKey
return false if ignore_key_codes[e.keyCode]?
- # return false if control_key_codes[e.keyCode]? # handled in keydown
char = e.charCode ? e.keyCode
if char and @cursor?
char = String.fromCharCode char
- if @cursor[1] is 0
- @cursor[0].text = char + @cursor[0].text
- else if @cursor[1] is @cursor[0].text.length - 1
- @cursor[0].text += char
- else
- @cursor[0].text =
- @cursor[0].text.substr(0, @cursor[1]) +
- char +
- @cursor[0].text.substr(@cursor[1])
- @cursor[0].el.nodeValue = @cursor[0].text
- @move_cursor [@cursor[0], @cursor[1] + 1]
+ @insert_character @cursor.n, @cursor.i, char
+ @adjust_whitespace_style @cursor.n
@changed()
+ new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i + 1
+ if new_cursor
+ @move_cursor new_cursor
+ else
+ console.log "ERROR: couldn't find cursor position after insert"
+ @kill_cursor()
return false
clear_dom: -> # remove all the editable content (and cursor, overlays, etc)
while @idoc.body.childNodes.length
@iframe.style.height = "0"
@iframe.style.height = "#{h}px"
@wrap2.scrollTop = s
+ # does this node have whitespace that would be collapsed by white-space: normal?
+ # note: this checks direct text children, and does _not_ recurse into child tags
+ # tag is a node with type:"tag"
+ has_collapsable_space: (tag) ->
+ for n in tag.children
+ if n.type is 'text'
+ for i in [0...n.text.length]
+ code = n.text.charCodeAt i
+ if code isnt 32 and is_space_code code
+ # tab, return
+ return true
+ # check for double spaces that don't surround insert location
+ continue if i is 0
+ if n.text.substr(i - 1, 2) is ' '
+ return true
+ if n.text.length > 0
+ if is_space_code n.text.charCodeAt 0
+ return true
+ if is_space_code n.text.charCodeAt n.text.length - 1
+ return true
+ # add/remove "white-space: pre[-wrap]" to/from style="" on tags with direct
+ # child text nodes with multiple spaces in a row, or spaces at the
+ # start/end.
+ #
+ # text inside child tags are not consulted. Child tags are expected to have
+ # this function applied to them when their content changes.
+ adjust_whitespace_style: (n) ->
+ if n.type is 'text'
+ n = n.parent
+ return unless n?.el?
+ # which css rule should be used to preserve spaces (should we need to)
+ style = @iframe.contentWindow.getComputedStyle n.el, null
+ ws = style.getPropertyValue 'white-space'
+ if ws_props[ws].space
+ preserve_rule = ws
+ else
+ preserve_rule = ws_props[ws].to_preserve
+ preserve_rule = "white-space: #{preserve_rule}"
+ if @has_collapsable_space n
+ # make sure preserve_rule exists
+ if n.el.style['white-space']
+ # FIXME check that it matches
+ return
+ if n.attrs[style]?
+ n.attrs.style += "; #{preserve_rule}"
+ else
+ n.attrs.style = preserve_rule
+ n.el.setAttribute 'style', n.attrs.style
+ else
+ # remove preserve_rule if it exists
+ return unless n.attrs.style?
+ # FIXME don't assume whitespace is just so
+ if n.attrs.style is "white-space: #{ws}"
+ delete n.attrs.style
+ n.el.removeAttribute 'style'
+ else
+ # FIXME find it in the middle and at the start
+ needle = "; white-space: #{ws}"
+ if needle is n.attrs.style.substr n.attrs.style.length - needle
+ n.attrs.style = n.attrs.style.substr 0, n.attrs.style.length - needle
+ n.el.setAttribute n.attrs.style
+ # after calling this, you MUST call changed() and adjust_whitespace_style()
+ insert_character: (n, i, char) ->
+ parent = @cursor.n.parent
+ return unless parent
+ return unless parent.el?
+ # insert the character
+ if i is 0
+ n.text = char + n.text
+ else if i is n.text.length
+ n.text += char
+ else
+ n.text =
+ n.text.substr(0, i) +
+ char +
+ n.text.substr(i)
+ n.el.nodeValue = n.text
+ # after calling this, you MUST call changed() and adjust_whitespace_style()
+ remove_character: (n, i) ->
+ n.text = n.text.substr(0, i) + n.text.substr(i + 1)
+ n.el.nodeValue = n.text
kill_cursor: -> # remove it, forget where it was
if @cursor_visible
@cursor_el.parentNode.removeChild @cursor_el