JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
chrome bugfix: cursor at end in pre-wrap
[peach-html5-editor.git] / editor.coffee
index 0d9d495..7c57510 100644 (file)
@@ -37,6 +37,32 @@ this_url_sans_path = ->
                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"
@@ -95,7 +121,13 @@ text_range_bounds = (el, start, end) ->
        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
@@ -806,23 +838,69 @@ class PeachHTML5Editor
                                                @move_cursor new_cursor
                                return false
                        when KEY_DOWN
+                               if @cursor?
+                                       new_cursor = @cursor
+                                       # go next until we move on the y axis
+                                       while new_cursor.y <= @cursor.y
+                                               new_cursor = find_next_cursor_position @tree, new_cursor
+                                               return false unless new_cursor?
+                                       # done early if we're already right of old cursor position
+                                       if new_cursor.x >= @cursor.x
+                                               # this would be strange, but could happen due to runaround
+                                               @move_cursor new_cursor
+                                               return false
+                                       target_y = new_cursor.y
+                                       # search rightward, until we find the closest position
+                                       # new_cursor is the next-most position we've checked
+                                       # prev_cursor is the older value, so it's not as next as new_cursor
+                                       while new_cursor.x < @cursor.x and new_cursor.y is target_y
+                                               prev_cursor = new_cursor
+                                               new_cursor = find_next_cursor_position @tree, new_cursor
+                                               break unless new_cursor?
+                                       # move cursor to prev_cursor or new_cursor
+                                       if new_cursor?
+                                               if new_cursor.y is target_y
+                                                       # both valid, and on the same line, use closest
+                                                       if (new_cursor.x - @cursor.x) < (@cursor.x - prev_cursor.x)
+                                                               @move_cursor new_cursor
+                                                       else
+                                                               @move_cursor prev_cursor
+                                               else
+                                                       # new_cursor on wrong line, use prev_cursor
+                                                       @move_cursor prev_cursor
+                                       else
+                                               # can't go any further prev, use prev_cursor
+                                               @move_cursor prev_cursor
+                               else
+                                       # move cursor to first position in document
+                                       new_cursor = last_cursor_position @tree
+                                       if new_cursor?
+                                               @move_cursor new_cursor
                                return false
                        when KEY_END
                                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
+                               @cursor.n.text = @cursor.n.text.substr(0, @cursor.i - 1) + @cursor.n.text.substr(@cursor.i)
+                               @cursor.n.el.nodeValue = @cursor.n.text
+                               new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1
+                               if new_cursor?
+                                       @move_cursor new_cursor
+                               else
+                                       @kill_cursor()
                                @changed()
                                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
+                               @cursor.n.text = @cursor.n.text.substr(0, @cursor.i) + @cursor.n.text.substr(@cursor.i + 1)
+                               @cursor.n.el.nodeValue = @cursor.n.text
+                               new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i
+                               if new_cursor?
+                                       @move_cursor new_cursor
+                               else
+                                       @kill_cursor()
                                @changed()
                                return false
                        when KEY_ENTER
@@ -842,21 +920,16 @@ class PeachHTML5Editor
        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
+                       @insert_character @cursor.n, @cursor.i, char
+                       new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i + 1
+                       if new_cursor
+                               @move_cursor new_cursor
                        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]
+                               console.log "ERROR: couldn't find cursor position after insert"
+                               @kill_cursor()
                        @changed()
                return false
        clear_dom: -> # remove all the editable content (and cursor, overlays, etc)
@@ -884,12 +957,94 @@ class PeachHTML5Editor
                        @iframe.style.height = "0"
                        @iframe.style.height = "#{h}px"
                        @wrap2.scrollTop = s
+       # Warning: this does not call changed() for you
+       insert_character: (n, i, char) ->
+               # TODO handle newlines, tabs, etc
+               parent = @cursor.n.parent
+               return unless parent
+               return unless parent.el?
+               style = @iframe.contentWindow.getComputedStyle parent.el, null
+               ws = style.getPropertyValue 'white-space'
+               if char is ' '
+                       unless ws_props[ws].space
+                               change = false
+                               if i is 0
+                                       # TODO check if a space at the beginning would actually get collapsed
+                                       change = true
+                               else if i is n.text.length
+                                       change = true
+                                       # TODO check if a space at the end would actually get collapsed
+                               else
+                                       if n.text.charAt(i - 1) is ' ' or n.text.charAt(i) is ' '
+                                               change = true
+                               if change
+                                       rule = "white-space: #{ws_props[ws].to_preserve}"
+                                       if parent.attrs[style]?
+                                               parent.attrs.style += "; #{rule}"
+                                       else
+                                               parent.attrs.style = rule
+                                       parent.el.setAttribute 'style', parent.attrs.style
+               else
+                       # TODO test this
+                       # inserting a visible (non-space) character
+                       if ws_props[ws].space
+                               if parent.el.style?['white-space']
+                                       # This node has a "white-space" property on it
+                                       # probably created automatically by this editor
+                                       # when the user pressed space.
+                                       # Check if that's no longer needed.
+                                       need = false
+                                       for ti in [0...n.text.length]
+                                               code = n.text.charCodeAt ti
+                                               if code isnt 32 and is_space_code code
+                                                       # tab, return
+                                                       need = true
+                                                       break
+                                               # check for double spaces that don't surround insert location
+                                               continue if ti is i
+                                               continue if ti is 0
+                                               if n.text.substr(ti - 1, 2) is '  '
+                                                       need = true
+                                                       break
+                                       if i > 0
+                                               if 32 is n.text.charCodeAt 0
+                                                       need = true
+                                       if i < n.text.length
+                                               if 32 is n.text.charCodeAt n.text.length - 1
+                                                       need = true
+                                       unless need
+                                               # TODO don't assume whitespace is just so
+                                               if parent.attrs.style is "white-space: #{ws}"
+                                                       delete parent.attrs.style
+                                                       parent.el.removeAttribute 'style'
+                                               else
+                                                       # FIXME find it in the middle and at the start
+                                                       needle = "; white-space: #{ws}"
+                                                       if needle is parent.attrs.style.substr parent.attrs.style.length - needle
+                                                               parent.attrs.style = parent.attrs.style.substr 0, parent.attrs.style.length - needle
+                                                               parent.el.setAttribute parent.attrs.style
+               # TODO insert the character now
+               if i is 0
+                       n.text = char + n.text
+               else if i is n.text.length - 1
+                       n.text += char
+               else
+                       n.text =
+                               n.text.substr(0, i) +
+                               char +
+                               n.text.substr(i)
+               n.el.nodeValue = n.text
+               # TODO call this when the user types
+               # TODO detect when typing produces a collapsing space
+       remove_character: (n, i) ->
+               # TODO call this from delete and backspace key handlers
+               # TODO detect if this would result in collapsing space
        kill_cursor: -> # remove it, forget where it was
                if @cursor_visible
                        @cursor_el.parentNode.removeChild @cursor_el
                        @cursor_visible = false
                @cursor = null
-               @matt null
+               @annotate null
        move_cursor: (cursor) ->
                @cursor = cursor
                unless @cursor_visible
@@ -903,8 +1058,8 @@ class PeachHTML5Editor
                        height = cursor.h
                @cursor_el.style.top = "#{cursor.y + overlay_padding + Math.round(height * .07)}px"
                @cursor_el.style.height = "#{Math.round height * 0.82}px"
-               @matt cursor.n
-       matt: (n) ->
+               @annotate cursor.n
+       annotate: (n) ->
                while @matting.length > 0
                        @overlay.removeChild @matting[0]
                        @matting.shift()
@@ -920,12 +1075,12 @@ class PeachHTML5Editor
                        if bounds.x is prev_bounds.x and bounds.y is prev_bounds.y and bounds.w is prev_bounds.w and bounds.h is prev_bounds.h
                                n = n.parent
                                continue
-                       matt = domify @outer_idoc, div: class: 'ann_box', style: "left: #{bounds.x - 1 + overlay_padding}px; top: #{bounds.y - 2 + overlay_padding}px; width: #{bounds.w}px; height: #{bounds.h}px" # outline: 1000px solid rgba(0,153,255,#{alpha});
-                       @overlay.appendChild matt
-                       @matting.push matt
-                       ann = domify @outer_idoc, div: class: 'ann_tag', style: "left: #{bounds.x + 1 + overlay_padding}px; top: #{bounds.y - 7 + overlay_padding}px", children: [domify @outer_idoc, text: " #{n.name} "]
-                       @overlay.appendChild ann
-                       @matting.push ann
+                       ann_box = domify @outer_idoc, div: class: 'ann_box', style: "left: #{bounds.x - 1 + overlay_padding}px; top: #{bounds.y - 2 + overlay_padding}px; width: #{bounds.w}px; height: #{bounds.h}px" # outline: 1000px solid rgba(0,153,255,#{alpha});
+                       @overlay.appendChild ann_box
+                       @matting.push ann_box
+                       ann_tag = domify @outer_idoc, div: class: 'ann_tag', style: "left: #{bounds.x + 1 + overlay_padding}px; top: #{bounds.y - 7 + overlay_padding}px", children: [domify @outer_idoc, text: " #{n.name} "]
+                       @overlay.appendChild ann_tag
+                       @matting.push ann_tag
                        n = n.parent
                        alpha *= 1.5
        pretty_html: (tree, indent = '', parent_flags = pre_ish: false, block: true, want_nl: false) ->