X-Git-Url: https://jasonwoof.com/gitweb/?a=blobdiff_plain;f=editor.coffee;h=b6f28f058bf8165f5b77404c779c0d25b1fd1faf;hb=57b00d36f6efe5e9133ffb4346086c81eb9d89c0;hp=0d9d495ccaf68f729ca471a2ef670f754416ab9f;hpb=60bdb823e686628c9831bb16f7f1b27abd4a6610;p=peach-html5-editor.git diff --git a/editor.coffee b/editor.coffee index 0d9d495..b6f28f0 100644 --- a/editor.coffee +++ b/editor.coffee @@ -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,24 +838,70 @@ 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 + @remove_character @cursor.n, @cursor.i - 1 + @adjust_whitespace_style @cursor.n @changed() + new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i - 1 + if new_cursor? + @move_cursor new_cursor + else + @kill_cursor() 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 + @remove_character @cursor.n, @cursor.i + @adjust_whitespace_style @cursor.n @changed() + new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i + if new_cursor? + @move_cursor new_cursor + else + @kill_cursor() return false when KEY_ENTER return false @@ -842,22 +920,18 @@ 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 - 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] + @insert_character @cursor.n, @cursor.i, char + @adjust_whitespace_style @cursor.n @changed() + new_cursor = new_cursor_position n: @cursor.n, i: @cursor.i + 1 + if new_cursor + @move_cursor new_cursor + else + console.log "ERROR: couldn't find cursor position after insert" + @kill_cursor() return false clear_dom: -> # remove all the editable content (and cursor, overlays, etc) while @idoc.body.childNodes.length @@ -884,12 +958,93 @@ class PeachHTML5Editor @iframe.style.height = "0" @iframe.style.height = "#{h}px" @wrap2.scrollTop = s + # does this node have whitespace that would be collapsed by white-space: normal? + # note: this checks direct text children, and does _not_ recurse into child tags + # tag is a node with type:"tag" + has_collapsable_space: (tag) -> + for n in tag.children + if n.type is 'text' + for i in [0...n.text.length] + code = n.text.charCodeAt i + if code isnt 32 and is_space_code code + # tab, return + return true + # check for double spaces that don't surround insert location + continue if i is 0 + if n.text.substr(i - 1, 2) is ' ' + return true + if n.text.length > 0 + if is_space_code n.text.charCodeAt 0 + return true + if is_space_code n.text.charCodeAt n.text.length - 1 + return true + # add/remove "white-space: pre[-wrap]" to/from style="" on tags with direct + # child text nodes with multiple spaces in a row, or spaces at the + # start/end. + # + # text inside child tags are not consulted. Child tags are expected to have + # this function applied to them when their content changes. + adjust_whitespace_style: (n) -> + if n.type is 'text' + n = n.parent + return unless n?.el? + # which css rule should be used to preserve spaces (should we need to) + style = @iframe.contentWindow.getComputedStyle n.el, null + ws = style.getPropertyValue 'white-space' + if ws_props[ws].space + preserve_rule = ws + else + preserve_rule = ws_props[ws].to_preserve + preserve_rule = "white-space: #{preserve_rule}" + if @has_collapsable_space n + # make sure preserve_rule exists + if n.el.style['white-space'] + # FIXME check that it matches + return + if n.attrs[style]? + n.attrs.style += "; #{preserve_rule}" + else + n.attrs.style = preserve_rule + n.el.setAttribute 'style', n.attrs.style + else + # remove preserve_rule if it exists + return unless n.attrs.style? + # FIXME don't assume whitespace is just so + if n.attrs.style is "white-space: #{ws}" + delete n.attrs.style + n.el.removeAttribute 'style' + else + # FIXME find it in the middle and at the start + needle = "; white-space: #{ws}" + if needle is n.attrs.style.substr n.attrs.style.length - needle + n.attrs.style = n.attrs.style.substr 0, n.attrs.style.length - needle + n.el.setAttribute n.attrs.style + # after calling this, you MUST call changed() and adjust_whitespace_style() + insert_character: (n, i, char) -> + parent = @cursor.n.parent + return unless parent + return unless parent.el? + # insert the character + if i is 0 + n.text = char + n.text + else if i is n.text.length + n.text += char + else + n.text = + n.text.substr(0, i) + + char + + n.text.substr(i) + n.el.nodeValue = n.text + # after calling this, you MUST call changed() and adjust_whitespace_style() + remove_character: (n, i) -> + n.text = n.text.substr(0, i) + n.text.substr(i + 1) + n.el.nodeValue = n.text 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) ->