X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=editor.coffee;h=66d1dfedcaa62042c2d17a2982b3fab7fcf6a567;hb=fb81209e78ff39a5469ee9217b35d4edf94a21d5;hp=41d81c37192ed2aaab23cc543430a726d67e793a;hpb=dde84cf239d827cb3a3b412d970e95bd5851216d;p=peach-html5-editor.git diff --git a/editor.coffee b/editor.coffee index 41d81c3..66d1dfe 100644 --- a/editor.coffee +++ b/editor.coffee @@ -17,12 +17,17 @@ # SETTINGS overlay_padding = 10 -TYPE_TAG = peach_parser.TYPE_TAG -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 +next_frame = (cb) -> + if (window.requestAnimationFrame?) + window.requestAnimationFrame cb + else + timeout 16, cb + +# 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].' debug_dot_at = (doc, x, y) -> return # disabled @@ -236,9 +241,34 @@ outer_css = (args) -> ret += '@keyframes blink {' ret += 'to { visibility: hidden; }' ret += '}' + ret += '.ann_box {' + ret += 'z-index: 5;' + ret += 'position: absolute;' + ret += 'border: 1px solid rgba(0,0,0,0.1);' + ret += 'outline: 1px solid rgba(255,255,255,0.1);' # in case there's a black background + ret += '}' + ret += '.ann_tag {' + ret += 'z-index: 10;' + ret += 'position: absolute;' + 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 @@ -253,14 +283,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 @@ -281,18 +303,21 @@ instantiate_tree = (tree, parent) -> remove = [] for c, i in tree switch c.type - when TYPE_TEXT + when 'text' c.el = parent.ownerDocument.createTextNode c.text parent.appendChild c.el - when TYPE_TAG + when 'tag' if c.name in ['script', 'object', 'iframe', 'link'] # TODO put placeholders instead remove.unshift i + continue # TODO create in correct namespace c.el = parent.ownerDocument.createElement c.name for k, v of c.attrs # FIXME if attr_whitelist[k]? - c.el.setAttribute k, v + if valid_attr_regex.test k + unless js_attr_regex.test k + c.el.setAttribute k, v parent.appendChild c.el if c.children.length instantiate_tree c.children, c.el @@ -310,7 +335,7 @@ traverse_tree = (tree, cb) -> return done find_next_cursor_position = (tree, n, i) -> - if n.type is TYPE_TEXT and n.text.length > i + if n.type is '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" @@ -323,7 +348,7 @@ find_next_cursor_position = (tree, n, i) -> state_before = true found = null traverse_tree tree, (node, state) -> - if node.type is TYPE_TEXT and state_before is false + if node.type is 'text' and state_before is false if cursor_to_xyh(node, 0)? found = node return true @@ -335,7 +360,7 @@ find_next_cursor_position = (tree, n, i) -> return null find_prev_cursor_position = (tree, n, i) -> - if n? and n.type is TYPE_TEXT and i > 0 + if n? and n.type is 'text' and i > 0 orig_xyh = cursor_to_xyh n, i unless orig_xyh? console.log "ERROR: couldn't find xy for current cursor location" @@ -349,7 +374,7 @@ find_prev_cursor_position = (tree, n, i) -> found_prev = n? found = null traverse_tree tree, (node) -> - if node.type is TYPE_TEXT + if node.type is 'text' if node is n if found_prev? found = found_prev @@ -359,12 +384,12 @@ find_prev_cursor_position = (tree, n, i) -> 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 find_prev_cursor_position tree, found, 0 return null find_loc_cursor_position = (tree, loc) -> for c in tree - if c.type is TYPE_TAG or c.type is TYPE_TEXT + if c.type is 'tag' or c.type is 'text' bounds = get_el_bounds c.el continue if loc.x < bounds.x continue if loc.x > bounds.x + bounds.w @@ -373,7 +398,7 @@ find_loc_cursor_position = (tree, loc) -> if c.children.length ret = find_loc_cursor_position c.children, loc return ret if ret? - if c.type is TYPE_TEXT + if c.type is 'text' # click is within bounding box that contains all text. return [c, 0] if c.text.length is 0 before_i = 0 @@ -429,14 +454,14 @@ is_space = (chr) -> tree_remove_empty_text_nodes = (tree) -> empties = [] traverse_tree tree, (n) -> - if n.type is TYPE_TEXT + if n.type is '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 + if tree[0].type is 'text' console.log "oop, leaving a blank node because it's the only thing" return n.el.parentNode.removeChild n.el @@ -458,13 +483,13 @@ tree_dedup_space = (tree) -> iterate = (tree, cb) -> for n in tree - if n.type is TYPE_TEXT + if n.type is 'text' i = 0 while i < n.text.length # don't foreach, cb might remove chars advance = cb n, i if advance i += 1 - if n.type is TYPE_TAG + if n.type is 'tag' block = is_display_block n.el if block cb null @@ -577,6 +602,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 = {} @@ -621,14 +647,19 @@ 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 # TODO test this @@ -649,6 +680,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 @@ -724,7 +757,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 @@ -778,7 +811,12 @@ class PeachHTML5Editor @overlay.appendChild @cursor_el @cursor_visible = true @cursor_el.style.left = "#{loc.x + overlay_padding - 1}px" - @cursor_el.style.top = "#{loc.y + overlay_padding}px" + if loc.h < 5 + height = 12 + else + height = loc.h + @cursor_el.style.top = "#{loc.y + overlay_padding + Math.round(height * .07)}px" + @cursor_el.style.height = "#{Math.round height * 0.82}px" @matt cursor[0] matt: (n) -> while @matting.length > 0 @@ -788,7 +826,7 @@ class PeachHTML5Editor prev_bounds = x: 0, y: 0, w: 0, h: 0 alpha = 0.1 while n?.el? - if n.type is TYPE_TEXT + if n.type is 'text' n = n.parent continue bounds = get_el_bounds n.el @@ -796,10 +834,10 @@ 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: style: "position: absolute; left: #{bounds.x - 1 + overlay_padding}px; top: #{bounds.y - 1 + overlay_padding}px; width: #{bounds.w}px; height: #{bounds.h}px; outline: 1000px solid rgba(0,153,255,#{alpha}); border: 1px solid rgba(0,0,0,.1)" + 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: style: "position: absolute; left: #{bounds.x - 2 + overlay_padding}px; top: #{bounds.y - 6 + overlay_padding}px; font-size: 8px", children: [domify @outer_idoc, text: "<#{n.name}>"] + 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 n = n.parent @@ -814,7 +852,7 @@ class PeachHTML5Editor inner_flags = want_nl: true is_br = false switch n.type - when TYPE_TAG + when 'tag' if n.name is 'br' is_br = true is_text = false @@ -824,12 +862,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 @@ -845,7 +885,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 @@ -857,12 +901,12 @@ class PeachHTML5Editor inner_flags.block = true is_block = true in_flow_block = in_flow - when TYPE_TEXT + when 'text' is_text = true is_block = false in_flow = true in_flow_block = false - else # TYPE_COMMENT, TYPE_DOCTYPE + else # 'comment', 'doctype' is_text = false is_block = false in_flow = false @@ -875,7 +919,7 @@ class PeachHTML5Editor ret += "\n" ret += indent switch n.type - when TYPE_TAG + when 'tag' ret += '<' + n.name attr_keys = [] for k of n.attrs @@ -894,11 +938,11 @@ class PeachHTML5Editor if n.children.length ret += @pretty_html n.children, next_indent, inner_flags ret += "" - when TYPE_TEXT + when 'text' ret += enc_text n.text - when TYPE_COMMENT + when 'comment' ret += "" # TODO encode? - when TYPE_DOCTYPE + when 'doctype' ret += " 0 ret += " \"#{n.public_identifier}\"" @@ -915,6 +959,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...