JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
events don't reach editable content, no selecting
[peach-html5-editor.git] / editor.coffee
index 224b2f9..ceffb9a 100644 (file)
@@ -23,7 +23,7 @@ TYPE_COMMENT = peach_parser.TYPE_COMMENT
 TYPE_DOCTYPE = peach_parser.TYPE_DOCTYPE
 
 debug_dot_at = (doc, x, y) ->
-       return
+       return # disabled
        el = doc.createElement 'div'
        el.setAttribute 'style', "position: absolute; left: #{x}px; top: #{y}px; width: 1px; height: 3px; background-color: red"
        doc.body.appendChild el
@@ -56,6 +56,15 @@ is_display_block = (el) ->
        else
                return window.getComputedStyle(el, null).getPropertyValue('display') is 'block'
 
+# Pass return value from dom event handlers to this.
+# If they return false, this will addinionally stop propagation and default.
+event_return = (e, bool) ->
+       if bool is false
+               if e.stopPropagation?
+                       e.stopPropagation()
+               if e.preventDefault?
+                       e.preventDefault()
+       return bool
 # Warning: currently assumes you're asking about a single character
 # Note: chromium returns multiple bounding rects for a space at a line-break
 # Note: chromium's getBoundingClientRect() is broken (when zero-area client rects)
@@ -175,58 +184,78 @@ domify = (doc, hash) ->
                                el.setAttribute k, v
        return el
 
-css = ''
-css += 'body {'
-css +=     'margin: 0;'
-css +=     'padding: 0;'
-css += '}'
-css += '#wrap1 {'
-css +=     'border: 1px solid black;'
-css +=     "padding: #{overlay_padding}px;"
-css += '}'
-css += '#wrap2 {'
-css +=     'border: 1px solid black;'
-css +=     "padding: #{overlay_padding}px;"
-css += '}'
-css += '#wrap3 {'
-css +=     'position: relative;'
-css += '}'
-css += 'iframe {'
-css +=     'box-sizing: border-box;'
-css +=     'margin: 0;'
-css +=     'border: none;'
-css +=     'padding: 0;'
-css +=     "width: #{300 - 4 - 4 * overlay_padding}px;"
-css +=     "height: #{300 - 4 - 4 * overlay_padding}px;"
-css += '}'
-css += '#overlay {'
-css +=     'position: absolute;'
-css +=     "left: -#{overlay_padding}px;"
-css +=     "top: -#{overlay_padding}px;"
-css +=     "right: -#{overlay_padding}px;"
-css +=     "bottom: -#{overlay_padding}px;"
-css +=     'overflow: hidden;'
-css += '}'
-css += '.lightbox {'
-css +=     'position: absolute;'
-css +=     'background: rgba(100,100,100,0.2);'
-css += '}'
-css += '#cursor {'
-css +=     'position: absolute;'
-css +=     'height: 1em;'
-css +=     'width: 2px;'
-css +=     'margin-left: -1px;'
-css +=     'margin-right: -1px;'
-css +=     'background: #444;'
-css +=     '-webkit-animation: blink 1s steps(2, start) infinite;'
-css +=     'animation: blink 1s steps(2, start) infinite;'
-css += '}'
-css += '@-webkit-keyframes blink {'
-css +=     'to { visibility: hidden; }'
-css += '}'
-css += '@keyframes blink {'
-css +=     'to { visibility: hidden; }'
-css += '}'
+outer_css = (args) ->
+       w = args.w ? 300
+       h = args.h ? 300
+       inner_padding = args.inner_padding ? overlay_padding
+       frame_width = args.frame_width ? inner_padding
+       # TODO editor controls height...
+       occupy = (left, top = left, right = left, bottom = top) ->
+               w -= left + right
+               h -= top + bottom
+               return Math.max(left, top, right, bottom)
+       ret = ''
+       ret += 'body {'
+       ret +=     'margin: 0;'
+       ret +=     'padding: 0;'
+       ret += '}'
+       ret += '#wrap1 {'
+       ret +=     "border: #{occupy 1}px solid black;"
+       ret +=     "padding: #{occupy frame_width}px;"
+       ret += '}'
+       ret += '#wrap2 {'
+       ret +=     "border: #{occupy 1}px solid black;"
+       ret +=     "padding: #{occupy inner_padding}px;"
+       ret +=     "padding-right: #{inner_padding + occupy 0, 0, 15, 0}px;" # for scroll bar
+       ret +=     "width: #{w}px;"
+       ret +=     "height: #{h}px;"
+       ret +=     'overflow-x: hidden;'
+       ret +=     'overflow-y: scroll;'
+       ret += '}'
+       ret += '#wrap3 {'
+       ret +=     'position: relative;'
+       ret +=     "width: #{w}px;"
+       ret +=     "min-height: #{h}px;"
+       ret += '}'
+       ret += 'iframe {'
+       ret +=     'box-sizing: border-box;'
+       ret +=     'margin: 0;'
+       ret +=     'border: none;'
+       ret +=     'padding: 0;'
+       ret +=     "width: #{w}px;"
+       #ret +=     "height: #{h}px;" # height auto-set when content set/changed
+       ret +=     '-ms-user-select: none;'
+       ret +=     '-webkit-user-select: none;'
+       ret +=     '-moz-user-select: none;'
+       ret +=     'user-select: none;'
+       ret += '}'
+       ret += '#overlay {'
+       ret +=     'position: absolute;'
+       ret +=     "left: -#{inner_padding}px;"
+       ret +=     "top: -#{inner_padding}px;"
+       ret +=     "right: -#{inner_padding}px;"
+       ret +=     "bottom: -#{inner_padding}px;"
+       ret +=     'overflow: hidden;'
+       ret += '}'
+       ret += '.lightbox {'
+       ret +=     'position: absolute;'
+       ret +=     'background: rgba(100,100,100,0.2);'
+       ret += '}'
+       ret += '#cursor {'
+       ret +=     'position: absolute;'
+       ret +=     'height: 1em;' # FIXME adjust for hight of text
+       ret +=     'width: 2px;'
+       ret +=     'background: #444;'
+       ret +=     '-webkit-animation: blink 1s steps(2, start) infinite;'
+       ret +=     'animation: blink 1s steps(2, start) infinite;'
+       ret += '}'
+       ret += '@-webkit-keyframes blink {'
+       ret +=     'to { visibility: hidden; }'
+       ret += '}'
+       ret += '@keyframes blink {'
+       ret +=     'to { visibility: hidden; }'
+       ret += '}'
+       return ret
 
 # key codes:
 KEY_LEFT = 37
@@ -268,12 +297,16 @@ control_key_codes = # we react to these, but they aren't typing
        '9':  KEY_TAB
 
 instantiate_tree = (tree, parent) ->
-       for c in tree
+       remove = []
+       for c, i in tree
                switch c.type
                        when TYPE_TEXT
                                c.el = parent.ownerDocument.createTextNode c.text
                                parent.appendChild c.el
                        when TYPE_TAG
+                               if c.name in ['script', 'object', 'iframe', 'link']
+                                       # TODO put placeholders instead
+                                       remove.unshift i
                                # TODO create in correct namespace
                                c.el = parent.ownerDocument.createElement c.name
                                for k, v of c.attrs
@@ -282,6 +315,8 @@ instantiate_tree = (tree, parent) ->
                                parent.appendChild c.el
                                if c.children.length
                                        instantiate_tree c.children, c.el
+       for i in remove
+               tree.splice i, 1
 
 traverse_tree = (tree, state, cb) ->
        for c in tree
@@ -527,11 +562,12 @@ tree_dedup_space = (tree) ->
 class PeachHTML5Editor
        # Options: (all optional)
        #   editor_id: "id" attribute for outer-most element created by/for editor
+       #   on_init: callback for when the editable content is in place
        constructor: (in_el, options) ->
                @options = options ? {}
                @in_el = in_el
                @tree = []
-               @initialized = false # when iframes have loaded
+               @inited = false # when iframes have loaded
                @outer_iframe # iframe to hold editor
                @outer_idoc # "document" object for @outer_iframe
                @iframe = null # iframe to hold editable content
@@ -544,7 +580,8 @@ class PeachHTML5Editor
                if opt_fragment
                        @parser_opts.fragment = 'body'
 
-               @outer_iframe = domify document, iframe: class: 'peach_html5_editor'
+               @outer_iframe = domify document, iframe: {}
+               outer_iframe_style = 'border: none !important; margin: 0 !important; padding: 0 !important; height: 100% !important; width: 100% !important;'
                if @options.editor_id?
                        @outer_iframe.setAttribute 'id', @options.editor_id
                @outer_iframe.onload = =>
@@ -553,11 +590,10 @@ class PeachHTML5Editor
                                domify @outer_idoc, text: css
                        ]
                        @outer_idoc.head.appendChild icss
-                       # FIXME continue
-
                        @iframe = domify @outer_idoc, iframe: {}
                        @iframe.onload = =>
                                @init()
+                       setTimeout (=> @init() unless @inited), 200 # firefox never fires this onload
                        @outer_idoc.body.appendChild(
                                domify @outer_idoc, div: id: 'wrap1', children: [
                                        domify @outer_idoc, div: id: 'wrap2', children: [
@@ -568,30 +604,45 @@ class PeachHTML5Editor
                                        ]
                                ]
                        )
-               @in_el.parentNode.appendChild @outer_iframe
-       init: -> # called by @iframe's onload
+               outer_wrap = domify document, div: class: 'peach_html5_editor'
+               @in_el.parentNode.appendChild outer_wrap
+               outer_bounds = get_el_bounds outer_wrap
+               if outer_bounds.w < 300
+                       outer_bounds.w = 300
+               if outer_bounds.h < 300
+                       outer_bounds.h = 300
+               outer_iframe_style += "width: #{outer_bounds.w}px; height: #{outer_bounds.h}px;"
+               @outer_iframe.setAttribute 'style', outer_iframe_style
+               css = outer_css w: outer_bounds.w, h: outer_bounds.h
+               outer_wrap.appendChild @outer_iframe
+       init: -> # called by @iframe's onload (or timeout on firefox)
                @idoc = @iframe.contentDocument
                @overlay.onclick = (e) =>
-                       return @onclick e
+                       return event_return @onclick e
+               @overlay.ondoubleclick = (e) =>
+                       return event_return @ondoubleclick e
                @outer_idoc.body.onkeyup = (e) =>
-                       return @onkeyup e
+                       return event_return @onkeyup e
                @outer_idoc.body.onkeydown = (e) =>
-                       return @onkeydown e
+                       return event_return @onkeydown e
                @outer_idoc.body.onkeypress = (e) =>
-                       return @onkeypress e
+                       return event_return @onkeypress e
                if @options.stylesheet
                        # TODO test this
                        @idoc.head.appendChild domify @idoc, style: src: @options.stylesheet
                @load_html @in_el.value
-               @initialized = true
-               if @options.initialized_cb?
-                       @options.initialized_cb()
+               @inited = true
+               if @options.on_init?
+                       @options.on_init()
        onclick: (e) ->
                x = (e.offsetX ? e.layerX) - overlay_padding
                y = (e.offsetY ? e.layerY) - overlay_padding
                new_cursor = find_loc_cursor_position @tree, x: x, y: y
                if new_cursor?
                        @move_cursor new_cursor
+               return false
+       ondoubleclick: (e) ->
+               return false
        onkeyup: (e) ->
                return if e.ctrlKey
                return false if ignore_key_codes[e.keyCode]?
@@ -696,6 +747,8 @@ class PeachHTML5Editor
                @in_el.value = dom_to_html @tree
                @in_el.onchange = =>
                        @load_html @in_el.value
+               @iframe.style.height = "0"
+               @iframe.style.height = "#{@idoc.body.scrollHeight}px"
        kill_cursor: -> # remove it, forget where it was
                if @cursor_visible
                        @cursor_el.parentNode.removeChild @cursor_el
@@ -714,7 +767,7 @@ class PeachHTML5Editor
                @overlay.appendChild @cursor_el
                @cursor_visible = true
                # TODO figure out x,y coords for cursor
-               @cursor_el.style.left = "#{loc.x + overlay_padding}px"
+               @cursor_el.style.left = "#{loc.x + overlay_padding - 1}px"
                @cursor_el.style.top = "#{loc.y + overlay_padding}px"
 
 window.peach_html5_editor = (args...) ->