JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
fix stopping of event propegation/default
[peach-html5-editor.git] / editor.coffee
index 07bf2c7..b7406dd 100644 (file)
@@ -22,6 +22,8 @@ TYPE_TEXT = peach_parser.TYPE_TEXT
 TYPE_COMMENT = peach_parser.TYPE_COMMENT
 TYPE_DOCTYPE = peach_parser.TYPE_DOCTYPE
 
+timeout = (ms, cb) -> return setTimeout cb, ms
+
 debug_dot_at = (doc, x, y) ->
        return # disabled
        el = doc.createElement 'div'
@@ -56,6 +58,19 @@ is_display_block = (el) ->
        else
                return window.getComputedStyle(el, null).getPropertyValue('display') is 'block'
 
+# pass a node (not an element) for a tag
+# returns if this is the sort of tag that cares about leading/trailing whitespace
+# FIXME this probably doesn't work all the time
+is_whitespace_significant = (n) ->
+       if n.name is 'textarea'
+               return true
+       if n.name is 'pre'
+               return true
+       if n.el.currentStyle?
+               return n.el.currentStyle['white-space'].substr(0, 3) is 'pre'
+       else
+               return window.getComputedStyle(n.el, null).getPropertyValue('white-space').substr(0, 3) is 'pre'
+
 # Pass return value from dom event handlers to this.
 # If they return false, this will addinionally stop propagation and default.
 event_return = (e, bool) ->
@@ -139,35 +154,70 @@ void_elements = {
        track: true
        wbr: true
 }
-dom_to_html = (dom) ->
+# TODO make these always pretty-print (on the inside) like blocks
+no_text_elements = { # these elements never contain text
+       select: true
+       table: true
+       tr: true
+       thead: true
+       tbody: true
+}
+# FIXME terminology: s/dom/tree/; s/el/n/
+tree_to_html = (tree, indent = '', parent_is_block = false) ->
        ret = ''
-       for el in dom
-               switch el.type
+       for n, i in tree
+               switch n.type
                        when TYPE_TAG
-                               ret += '<' + el.name
+                               is_block = is_display_block n.el
+                               if is_block
+                                       is_tiny_block = false
+                                       if is_whitespace_significant n
+                                               is_tiny_block = true
+                                       else
+                                               if n.children.length is 1
+                                                       if n.children[0].type is TYPE_TEXT
+                                                               if n.children[0].text.length < 35
+                                                                       is_tiny_block = true
+                               if is_block or (parent_is_block and i is 0)
+                                       ret += indent
+                               ret += '<' + n.name
                                attr_keys = []
-                               for k of el.attrs
+                               for k of n.attrs
                                        attr_keys.unshift k
                                #attr_keys.sort()
                                for k in attr_keys
                                        ret += " #{k}"
-                                       if el.attrs[k].length > 0
-                                               ret += "=\"#{enc_attr el.attrs[k]}\""
+                                       if n.attrs[k].length > 0
+                                               ret += "=\"#{enc_attr n.attrs[k]}\""
                                ret += '>'
-                               unless void_elements[el.name]
-                                       if el.children.length
-                                               ret += dom_to_html el.children
-                                       ret += "</#{el.name}>"
+                               unless void_elements[n.name]?
+                                       if is_block
+                                               next_indent = indent + '    '
+                                       else
+                                               next_indent = indent
+                                       if n.children.length
+                                               if is_block and not is_tiny_block
+                                                       ret += "\n"
+                                               ret += tree_to_html n.children, next_indent, is_block and not is_tiny_block
+                                               if is_block and not is_tiny_block
+                                                       ret += indent
+                                       ret += "</#{n.name}>"
+                               if is_block or (parent_is_block and i is tree.length - 1)
+                                       ret += "\n"
                        when TYPE_TEXT
-                               ret += enc_text el.text
+                               if parent_is_block and i is 0
+                                       ret += indent
+                               ret += enc_text n.text
+                               if parent_is_block and i is tree.length - 1
+                                       ret += "\n"
                        when TYPE_COMMENT
-                               ret += "<!--#{el.text}-->"
+                               ret += "<!--#{n.text}-->"
                        when TYPE_DOCTYPE
-                               ret += "<!DOCTYPE #{el.name}"
-                               if el.public_identifier? and el.public_identifier.length > 0
-                                       ret += " \"#{el.public_identifier}\""
-                               if el.system_identifier? and el.system_identifier.length > 0
-                                       ret += " \"#{el.system_identifier}\""
+                               ret += "<!DOCTYPE #{n.name}"
+                               if n.public_identifier? and n.public_identifier.length > 0
+                                       ret += " \"#{n.public_identifier}\""
+                               if n.system_identifier? and n.system_identifier.length > 0
+                                       ret += " \"#{n.system_identifier}\""
                                ret += ">\n"
        return ret
 
@@ -318,17 +368,18 @@ instantiate_tree = (tree, parent) ->
        for i in remove
                tree.splice i, 1
 
-traverse_tree = (tree, state, cb) ->
+traverse_tree = (tree, cb) ->
+       done = false
        for c in tree
-               cb c, state
-               break if state.done?
+               done = cb c
+               return done if done
                if c.children.length
-                       traverse_tree c.children, state, cb
-                       break if state.done?
-       return state
+                       done = traverse_tree c.children, cb
+                       return done if done
+       return done
 
 find_next_cursor_position = (tree, n, i) ->
-       if n? and n.type is TYPE_TEXT and n.text.length > i
+       if n.type is TYPE_TEXT and n.text.length > i
                orig_xyh = cursor_to_xyh n, i
                unless orig_xyh?
                        console.log "ERROR: couldn't find xy for current cursor location"
@@ -338,15 +389,18 @@ find_next_cursor_position = (tree, n, i) ->
                        if next_xyh?
                                if next_xyh.x > orig_xyh.x or next_xyh.y > orig_xyh.y
                                        return [n, next_i]
-       found = traverse_tree tree, before: n?, (node, state) ->
-               if node.type is TYPE_TEXT and state.before is false
+       state_before = true
+       found = null
+       traverse_tree tree, (node, state) ->
+               if node.type is TYPE_TEXT and state_before is false
                        if cursor_to_xyh(node, 0)?
-                               state.node = node
-                               state.done = true
+                               found = node
+                               return true
                if node is n
-                       state.before = false
-       if found.node?
-               return [found.node, 0]
+                       state_before = false
+               return false
+       if found?
+               return [found, 0]
        return null
 
 find_prev_cursor_position = (tree, n, i) ->
@@ -361,23 +415,19 @@ find_prev_cursor_position = (tree, n, i) ->
                                if prev_xyh.x < orig_xyh.x or prev_xyh.y < orig_xyh.y
                                        return [n, prev_i]
                return [n, i - 1]
-       found = traverse_tree tree, before: n?, (node, state) ->
+       found_prev = n?
+       found = null
+       traverse_tree tree, (node) ->
                if node.type is TYPE_TEXT
-                       unless n?
-                               state.node = node
-                               state.done = true
                        if node is n
-                               if state.prev?
-                                       state.node = state.prev
-                               state.done = true
-                       if node
-                               state.prev = node
-       if found.node?
-               ret = [found.node, found.node.text.length]
-               # check for unusual case: text not visible
-               loc = cursor_to_xyh ret[0], ret[1]
-               if loc?
-                       return ret
+                               if found_prev?
+                                       found = found_prev
+                               return true
+                       found_prev = node
+               return false
+       if found?
+               if cursor_to_xyh found, found.text.length # text visible?
+                       return [found, found.text.length]
                return find_prev_cursor_position tree, ret[0], 0
        return null
 
@@ -445,6 +495,25 @@ is_space_code = (char_code) ->
 is_space = (chr) ->
        return is_space_code chr.charCodeAt 0
 
+tree_remove_empty_text_nodes = (tree) ->
+       empties = []
+       traverse_tree tree, (n) ->
+               if n.type is TYPE_TEXT
+                       if n.text.length is 0
+                               empties.unshift n
+               return false
+       for n in empties
+               # don't completely empty the tree
+               if tree.length is 1
+                       if tree[0].type is TYPE_TEXT
+                               console.log "oop, leaving a blank node because it's the only thing"
+                               return
+               n.el.parentNode.removeChild n.el
+               for c, i in n.parent.children
+                       if c is n
+                               n.parent.children.splice i, 1
+                               break
+
 # pass a array of nodes (from parser library, ie it should have .el and .text)
 tree_dedup_space = (tree) ->
        prev = cur = next = null
@@ -454,6 +523,8 @@ tree_dedup_space = (tree) ->
        first = true
        removed_char = null
 
+       tree_remove_empty_text_nodes(tree)
+
        iterate = (tree, cb) ->
                for n in tree
                        if n.type is TYPE_TEXT
@@ -527,14 +598,6 @@ tree_dedup_space = (tree) ->
                                        return put_it_back()
                        #else
                        #       console.log "removing space becase space after it is collapsed"
-               # if there's no prev or next (single space inside a block-level element?) check
-               # TODO scrapt this, or fix it so it works when there's no parent
-               # if prev is null and next is null
-               #       new_parent_px = cur.parent.el.getBoundingClientRect()
-               #       if new_parent_px.left isnt parent_px.left or new_parent_px.top isnt parent_px.top or new_parent_px.right isnt parent_px.right or new_parent_px.bottom isnt parent_px.bottom
-               #               console.log "WEIRD: parent moved"
-               #               return put_it_back()
-               # we didn't put it back
                return true
        # pass null at start/end of display:block
        queue = (n, i) ->
@@ -563,6 +626,8 @@ tree_dedup_space = (tree) ->
        iterate tree, queue
        queue null
 
+       tree_remove_empty_text_nodes(tree)
+
 class PeachHTML5Editor
        # Options: (all optional)
        #   editor_id: "id" attribute for outer-most element created by/for editor
@@ -622,15 +687,15 @@ class PeachHTML5Editor
        init: -> # called by @iframe's onload (or timeout on firefox)
                @idoc = @iframe.contentDocument
                @overlay.onclick = (e) =>
-                       return event_return @onclick e
+                       return event_return e, @onclick e
                @overlay.ondoubleclick = (e) =>
-                       return event_return @ondoubleclick e
+                       return event_return e, @ondoubleclick e
                @outer_idoc.body.onkeyup = (e) =>
-                       return event_return @onkeyup e
+                       return event_return e, @onkeyup e
                @outer_idoc.body.onkeydown = (e) =>
-                       return event_return @onkeydown e
+                       return event_return e, @onkeydown e
                @outer_idoc.body.onkeypress = (e) =>
-                       return event_return @onkeypress e
+                       return event_return e, @onkeypress e
                if @options.stylesheet
                        # TODO test this
                        @idoc.head.appendChild domify @idoc, style: src: @options.stylesheet
@@ -746,9 +811,8 @@ class PeachHTML5Editor
                tree_dedup_space @tree
                @changed()
        changed: ->
-               # FIXME don't export cursor placeholder (when cursor is between space characters)
                @in_el.onchange = null
-               @in_el.value = dom_to_html @tree
+               @in_el.value = tree_to_html @tree
                @in_el.onchange = =>
                        @load_html @in_el.value
                @iframe.style.height = "0"
@@ -764,13 +828,12 @@ class PeachHTML5Editor
                        console.log "error: tried to move cursor to position that has no pixel location", cursor[0], cursor[1]
                        return
                @cursor = cursor
-               # replace cursor, to reset blink animation
+               # replace cursor element, to reset blink animation
                if @cursor_visible
                        @cursor_el.parentNode.removeChild @cursor_el
                @cursor_el = domify @outer_idoc, div: id: 'cursor'
                @overlay.appendChild @cursor_el
                @cursor_visible = true
-               # TODO figure out x,y coords for cursor
                @cursor_el.style.left = "#{loc.x + overlay_padding - 1}px"
                @cursor_el.style.top = "#{loc.y + overlay_padding}px"