JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
cursor blinks black/white
[peach-html5-editor.git] / editor.coffee
index 92fc1e0..870c61b 100644 (file)
 overlay_padding = 10
 
 timeout = (ms, cb) -> return setTimeout cb, ms
+next_frame = (cb) ->
+       if (window.requestAnimationFrame?)
+               window.requestAnimationFrame cb
+       else
+               timeout 16, cb
+
+this_url_sans_path = ->
+       ret = "#{window.location.href}"
+       clip = ret.lastIndexOf '#'
+       if clip > -1
+               ret = ret.substr 0, clip
+       clip = ret.lastIndexOf '?'
+       if clip > -1
+               ret = ret.substr 0, clip
+       clip = ret.lastIndexOf '/'
+       if clip > -1
+               ret = ret.substr 0, clip + 1
+       return ret
 
-# xml 1.0 says:
+# 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"
 js_attr_regex = new RegExp '^[oO][nN].'
@@ -227,14 +245,20 @@ outer_css = (args) ->
        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 +=     '-webkit-animation: blink 1s linear infinite;'
+       ret +=     'animation: blink 1s linear infinite;'
        ret += '}'
        ret += '@-webkit-keyframes blink {'
-       ret +=     'to { visibility: hidden; }'
+       ret +=     '0% { background-color: rgba(0,0,0,0.7); }'
+       ret +=     '50% { background-color: rgba(0,0,0,0.7); }'
+       ret +=     '50.001% { background-color: rgba(255,255,255,0.7); }'
+       ret +=     '100% { background-color: rgba(255,255,255,0.7); }'
        ret += '}'
        ret += '@keyframes blink {'
-       ret +=     'to { visibility: hidden; }'
+       ret +=     '0% { background-color: rgba(0,0,0,0.7); }'
+       ret +=     '50% { background-color: rgba(0,0,0,0.7); }'
+       ret +=     '50.001% { background-color: rgba(255,255,255,0.7); }'
+       ret +=     '100% { background-color: rgba(255,255,255,0.7); }'
        ret += '}'
        ret += '.ann_box {'
        ret +=     'z-index: 5;'
@@ -248,10 +272,22 @@ outer_css = (args) ->
        ret +=     'font-size: 8px;'
        ret +=     'white-space: pre;'
        ret +=     'background: rgba(255,255,255,0.4);'
+       ret +=     '-ms-user-select: none;'
+       ret +=     '-webkit-user-select: none;'
+       ret +=     '-moz-user-select: none;'
+       ret +=     'user-select: none;'
        ret += '}'
        return ret
 
-# key codes:
+
+ignore_key_codes =
+       '18': true # alt
+       '20': true # capslock
+       '17': true # ctrl
+       '144': true # numlock
+       '16': true # shift
+       '91': true # windows "start" key
+# key codes: (valid on keydown, not keypress)
 KEY_LEFT = 37
 KEY_UP = 38
 KEY_RIGHT = 39
@@ -266,14 +302,6 @@ KEY_INSERT = 45
 KEY_PAGE_UP = 33
 KEY_PAGE_DOWN = 34
 KEY_TAB = 9
-
-ignore_key_codes =
-       '18': true # alt
-       '20': true # capslock
-       '17': true # ctrl
-       '144': true # numlock
-       '16': true # shift
-       '91': true # windows "start" key
 control_key_codes = # we react to these, but they aren't typing
        '37': KEY_LEFT
        '38': KEY_UP
@@ -578,6 +606,7 @@ tree_dedup_space = (tree) ->
 class PeachHTML5Editor
        # Options: (all optional)
        #   editor_id: "id" attribute for outer-most element created by/for editor
+       #   css_file: filename of a css file to style editable content
        #   on_init: callback for when the editable content is in place
        constructor: (in_el, options) ->
                @options = options ? {}
@@ -593,6 +622,7 @@ class PeachHTML5Editor
                @cursor = null
                @cursor_el = null
                @cursor_visible = false
+               @poll_for_blur_timeout = null
                @iframe_offset = null
                opt_fragment = @options.fragment ? true
                @parser_opts = {}
@@ -609,7 +639,7 @@ class PeachHTML5Editor
                                domify @outer_idoc, text: css
                        ]
                        @outer_idoc.head.appendChild icss
-                       @iframe = domify @outer_idoc, iframe: {}
+                       @iframe = domify @outer_idoc, iframe: sandbox: 'allow-same-origin allow-scripts'
                        @iframe.onload = =>
                                @init()
                        setTimeout (=> @init() unless @inited), 200 # firefox never fires this onload
@@ -637,18 +667,26 @@ class PeachHTML5Editor
        init: -> # called by @iframe's onload (or timeout on firefox)
                @idoc = @iframe.contentDocument
                @overlay.onclick = (e) =>
+                       @have_focus()
                        return event_return e, @onclick e
                @overlay.ondoubleclick = (e) =>
+                       @have_focus()
                        return event_return e, @ondoubleclick e
                @outer_idoc.body.onkeyup = (e) =>
+                       @have_focus()
                        return event_return e, @onkeyup e
                @outer_idoc.body.onkeydown = (e) =>
+                       @have_focus()
                        return event_return e, @onkeydown e
                @outer_idoc.body.onkeypress = (e) =>
+                       @have_focus()
                        return event_return e, @onkeypress e
-               if @options.stylesheet
+               # chromium doesn't resolve relative urls as though they were at the same domain
+               # so add a <base> tag
+               @idoc.head.appendChild domify @idoc, base: href: this_url_sans_path()
+               if @options.css_file
                        # TODO test this
-                       @idoc.head.appendChild domify @idoc, style: src: @options.stylesheet
+                       @idoc.head.appendChild domify @idoc, link: rel: 'stylesheet', type: 'text/css', href: @options.css_file
                @load_html @in_el.value
                @inited = true
                if @options.on_init?
@@ -665,6 +703,8 @@ class PeachHTML5Editor
                new_cursor = find_loc_cursor_position @tree, xy
                if new_cursor?
                        @move_cursor new_cursor
+               else
+                       @kill_cursor()
                return false
        ondoubleclick: (e) ->
                return false
@@ -740,7 +780,7 @@ 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
+               # return false if control_key_codes[e.keyCode]? # handled in keydown
                char = e.charCode ? e.keyCode
                if char and @cursor?
                        char = String.fromCharCode char
@@ -845,12 +885,14 @@ class PeachHTML5Editor
                                                display = cs['display']
                                                position = cs['position']
                                                float = cs['float']
+                                               visibility = cs['visibility']
                                        else
                                                cs = @iframe.contentWindow.getComputedStyle(n.el, null)
                                                whitespace = cs.getPropertyValue 'white-space'
                                                display = cs.getPropertyValue 'display'
                                                position = cs.getPropertyValue 'position'
                                                float = cs.getPropertyValue 'float'
+                                               visibility = cs.getPropertyValue 'visibility'
                                        if n.name is 'textarea'
                                                inner_flags.pre_ish = true
                                        else
@@ -866,7 +908,11 @@ class PeachHTML5Editor
                                                                        if 'display' is 'none'
                                                                                in_flow = false
                                                                        else
-                                                                               in_flow = true
+                                                                               switch visibility
+                                                                                       when 'hidden', 'collapse'
+                                                                                               in_flow = false
+                                                                                       else # visible
+                                                                                               in_flow = true
                                        switch display
                                                when 'inline', 'none'
                                                        inner_flags.block = false
@@ -936,6 +982,21 @@ class PeachHTML5Editor
                                if prev_in_flow_is_block or parent_flags.block
                                        ret += "\n#{indent.substr 4}"
                return ret
+       onblur: ->
+               @kill_cursor()
+       have_focus: ->
+               @editor_is_focused = true
+               @poll_for_blur()
+       poll_for_blur: ->
+               return if @poll_for_blur_timeout? # already polling
+               @poll_for_blur_timeout = timeout 150, =>
+                       next_frame => # pause polling when browser knows we're not active/visible/etc.
+                               @poll_for_blur_timeout = null
+                               if document.activeElement is @outer_iframe
+                                       @poll_for_blur()
+                               else
+                                       @editor_is_focused = false
+                                       @onblur()
 
 window.peach_html5_editor = (args...) ->
        return new PeachHTML5Editor args...