+ @adjust_iframe_height()
+ adjust_iframe_height: ->
+ s = @wrap2.scrollTop
+ # when the content gets shorter, the idoc's body tag will continue to
+ # report the old (too big) height in Chrome. The workaround is to
+ # shrink the iframe before the content height:
+ @iframe.style.height = "10px"
+ h = parseInt(@idoc.body.scrollHeight, 10)
+ @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
+ # true if n is text node with just a space in it, and the only child of a tag
+ is_lone_space: (n, i) ->
+ return false unless n.type is 'text'
+ return false unless n.text is ' '
+ return false if n.parent is @tree_parent
+ if n.parent.children.length is 1
+ if n.parent.children[0] is n
+ # n is only child
+ return true
+ return false
+ # detect special case: typing before a space that's the only thing in a block/doc
+ # reason: enter key creates blocks with just a space in them
+ insert_should_replace: (n, i) ->
+ return false unless i is 0
+ return false unless n.text is ' '
+ return true if n.parent is @tree_parent
+ if n.parent.children.length is 1
+ if n.parent.children[0] is n
+ # n is only child
+ return true
+ return false
+ # after calling this, you MUST call changed() and adjust_whitespace_style()
+ insert_character: (n, i, char) ->
+ return if @cursor.n.parent is @tree_parent # FIXME implement text nodes at top level
+ parent = @cursor.n.parent
+ # insert the character
+ if @insert_should_replace n, i
+ n.text = char
+ else if i is 0
+ n.text = char + n.text
+ else if i is n.text.length
+ # replace the space
+ 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