JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
pretty-print html: one-line mode for small blocks
[peach-html5-editor.git] / editor.coffee
1 # Copyright 2015 Jason Woofenden
2 # This file implements an WYSIWYG editor in the browser (no contenteditable)
3 #
4 # This program is free software: you can redistribute it and/or modify it under
5 # the terms of the GNU Affero General Public License as published by the Free
6 # Software Foundation, either version 3 of the License, or (at your option) any
7 # later version.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 # SETTINGS
18 overlay_padding = 10
19
20 TYPE_TAG = peach_parser.TYPE_TAG
21 TYPE_TEXT = peach_parser.TYPE_TEXT
22 TYPE_COMMENT = peach_parser.TYPE_COMMENT
23 TYPE_DOCTYPE = peach_parser.TYPE_DOCTYPE
24
25 debug_dot_at = (doc, x, y) ->
26         return # disabled
27         el = doc.createElement 'div'
28         el.setAttribute 'style', "position: absolute; left: #{x}px; top: #{y}px; width: 1px; height: 3px; background-color: red"
29         doc.body.appendChild el
30         #console.log(new Error().stack)
31
32 # text nodes don't have getBoundingClientRect(), so use selection api to find
33 # it.
34 get_el_bounds = (el) ->
35         if el.getBoundingClientRect?
36                 rect = el.getBoundingClientRect()
37         else
38                 # text nodes don't have getBoundingClientRect(), so use range api
39                 range = el.ownerDocument.createRange()
40                 range.selectNodeContents el
41                 rect = range.getBoundingClientRect()
42         doc = el.ownerDocument.documentElement
43         win = el.ownerDocument.defaultView
44         y_fix = win.pageYOffset - doc.clientTop
45         x_fix = win.pageXOffset - doc.clientLeft
46         return {
47                 x: rect.left + x_fix
48                 y: rect.top + y_fix
49                 w: rect.width ? (rect.right - rect.left)
50                 h: rect.height ? (rect.top - rect.bottom)
51         }
52
53 is_display_block = (el) ->
54         if el.currentStyle?
55                 return el.currentStyle.display is 'block'
56         else
57                 return window.getComputedStyle(el, null).getPropertyValue('display') is 'block'
58
59 # Pass return value from dom event handlers to this.
60 # If they return false, this will addinionally stop propagation and default.
61 event_return = (e, bool) ->
62         if bool is false
63                 if e.stopPropagation?
64                         e.stopPropagation()
65                 if e.preventDefault?
66                         e.preventDefault()
67         return bool
68 # Warning: currently assumes you're asking about a single character
69 # Note: chromium returns multiple bounding rects for a space at a line-break
70 # Note: chromium's getBoundingClientRect() is broken (when zero-area client rects)
71 # Note: sometimes returns null (eg for whitespace that is not visible)
72 text_range_bounds = (el, start, end) ->
73         range = document.createRange()
74         range.setStart el, start
75         range.setEnd el, end
76         rects = range.getClientRects()
77         if rects.length > 0
78                 rect = rects[0]
79         else
80                 return null
81         doc = el.ownerDocument.documentElement
82         win = el.ownerDocument.defaultView
83         y_fix = win.pageYOffset - doc.clientTop
84         x_fix = win.pageXOffset - doc.clientLeft
85         return {
86                 x: rect.left + x_fix
87                 y: rect.top + y_fix
88                 w: rect.width ? (rect.right - rect.left)
89                 h: rect.height ? (rect.top - rect.bottom)
90                 rects: rects
91                 bounding: range.getBoundingClientRect()
92         }
93
94 # figure out the x/y coordinates of where the cursor should be if it's at
95 # position ``i`` within text node ``n``
96 # sometimes returns null (eg for whitespace that is not visible)
97 window.cursor_to_xyh = cursor_to_xyh = (n, i) ->
98         range = document.createRange()
99         if n.text.length is 0
100                 ret = text_range_bounds n.el, 0, 0
101         else if i is n.text.length
102                 ret = text_range_bounds n.el, i - 1, i
103                 if ret?
104                         ret.x += ret.w
105         else
106                 ret = text_range_bounds n.el, i, i + 1
107         if ret?
108                 debug_dot_at n.el.ownerDocument, ret.x, ret.y
109         return ret
110
111 # encode text so it can be safely placed inside an html attribute
112 enc_attr_regex = new RegExp '(&)|(")|(\u00A0)', 'g'
113 enc_attr = (txt) ->
114         return txt.replace enc_attr_regex, (match, amp, quote) ->
115                 return '&amp;' if (amp)
116                 return '&quot;' if (quote)
117                 return '&nbsp;'
118 enc_text_regex = new RegExp '(&)|(<)|(\u00A0)', 'g'
119 enc_text = (txt) ->
120         return txt.replace enc_text_regex, (match, amp, lt) ->
121                 return '&amp;' if (amp)
122                 return '&lt;' if (lt)
123                 return '&nbsp;'
124
125 void_elements = {
126         area: true
127         base: true
128         br: true
129         col: true
130         embed: true
131         hr: true
132         img: true
133         input: true
134         keygen: true
135         link: true
136         meta: true
137         param: true
138         source: true
139         track: true
140         wbr: true
141 }
142 dom_to_html = (dom, indent = '', parent_is_block = false) ->
143         ret = ''
144         for el, i in dom
145                 switch el.type
146                         when TYPE_TAG
147                                 is_block = is_display_block el.el
148                                 if is_block
149                                         is_tiny_block = false
150                                         if el.children.length is 1
151                                                 if el.children[0].type is TYPE_TEXT
152                                                         if el.children[0].text.length < 35
153                                                                 is_tiny_block = true
154                                 if is_block or (parent_is_block and i is 0)
155                                         ret += indent
156                                 ret += '<' + el.name
157                                 attr_keys = []
158                                 for k of el.attrs
159                                         attr_keys.unshift k
160                                 #attr_keys.sort()
161                                 for k in attr_keys
162                                         ret += " #{k}"
163                                         if el.attrs[k].length > 0
164                                                 ret += "=\"#{enc_attr el.attrs[k]}\""
165                                 ret += '>'
166                                 unless void_elements[el.name]?
167                                         if is_block
168                                                 next_indent = indent + '    '
169                                         else
170                                                 next_indent = indent
171                                         if el.children.length
172                                                 if is_block and not is_tiny_block
173                                                         ret += "\n"
174                                                 ret += dom_to_html el.children, next_indent, is_block and not is_tiny_block
175                                                 if is_block and not is_tiny_block
176                                                         ret += indent
177                                         ret += "</#{el.name}>"
178                                 if is_block or (parent_is_block and i is dom.length - 1)
179                                         ret += "\n"
180                         when TYPE_TEXT
181                                 if parent_is_block and i is 0
182                                         ret += indent
183                                 ret += enc_text el.text
184                                 if parent_is_block and i is dom.length - 1
185                                         ret += "\n"
186                         when TYPE_COMMENT
187                                 ret += "<!--#{el.text}-->"
188                         when TYPE_DOCTYPE
189                                 ret += "<!DOCTYPE #{el.name}"
190                                 if el.public_identifier? and el.public_identifier.length > 0
191                                         ret += " \"#{el.public_identifier}\""
192                                 if el.system_identifier? and el.system_identifier.length > 0
193                                         ret += " \"#{el.system_identifier}\""
194                                 ret += ">\n"
195         return ret
196
197 domify = (doc, hash) ->
198         for tag, attrs of hash
199                 if tag is 'text'
200                         return document.createTextNode attrs
201                 el = document.createElement tag
202                 for k, v of attrs
203                         if k is 'children'
204                                 for child in v
205                                         el.appendChild child
206                         else
207                                 el.setAttribute k, v
208         return el
209
210 outer_css = (args) ->
211         w = args.w ? 300
212         h = args.h ? 300
213         inner_padding = args.inner_padding ? overlay_padding
214         frame_width = args.frame_width ? inner_padding
215         # TODO editor controls height...
216         occupy = (left, top = left, right = left, bottom = top) ->
217                 w -= left + right
218                 h -= top + bottom
219                 return Math.max(left, top, right, bottom)
220         ret = ''
221         ret += 'body {'
222         ret +=     'margin: 0;'
223         ret +=     'padding: 0;'
224         ret += '}'
225         ret += '#wrap1 {'
226         ret +=     "border: #{occupy 1}px solid black;"
227         ret +=     "padding: #{occupy frame_width}px;"
228         ret += '}'
229         ret += '#wrap2 {'
230         ret +=     "border: #{occupy 1}px solid black;"
231         ret +=     "padding: #{occupy inner_padding}px;"
232         ret +=     "padding-right: #{inner_padding + occupy 0, 0, 15, 0}px;" # for scroll bar
233         ret +=     "width: #{w}px;"
234         ret +=     "height: #{h}px;"
235         ret +=     'overflow-x: hidden;'
236         ret +=     'overflow-y: scroll;'
237         ret += '}'
238         ret += '#wrap3 {'
239         ret +=     'position: relative;'
240         ret +=     "width: #{w}px;"
241         ret +=     "min-height: #{h}px;"
242         ret += '}'
243         ret += 'iframe {'
244         ret +=     'box-sizing: border-box;'
245         ret +=     'margin: 0;'
246         ret +=     'border: none;'
247         ret +=     'padding: 0;'
248         ret +=     "width: #{w}px;"
249         #ret +=     "height: #{h}px;" # height auto-set when content set/changed
250         ret +=     '-ms-user-select: none;'
251         ret +=     '-webkit-user-select: none;'
252         ret +=     '-moz-user-select: none;'
253         ret +=     'user-select: none;'
254         ret += '}'
255         ret += '#overlay {'
256         ret +=     'position: absolute;'
257         ret +=     "left: -#{inner_padding}px;"
258         ret +=     "top: -#{inner_padding}px;"
259         ret +=     "right: -#{inner_padding}px;"
260         ret +=     "bottom: -#{inner_padding}px;"
261         ret +=     'overflow: hidden;'
262         ret += '}'
263         ret += '.lightbox {'
264         ret +=     'position: absolute;'
265         ret +=     'background: rgba(100,100,100,0.2);'
266         ret += '}'
267         ret += '#cursor {'
268         ret +=     'position: absolute;'
269         ret +=     'height: 1em;' # FIXME adjust for hight of text
270         ret +=     'width: 2px;'
271         ret +=     'background: #444;'
272         ret +=     '-webkit-animation: blink 1s steps(2, start) infinite;'
273         ret +=     'animation: blink 1s steps(2, start) infinite;'
274         ret += '}'
275         ret += '@-webkit-keyframes blink {'
276         ret +=     'to { visibility: hidden; }'
277         ret += '}'
278         ret += '@keyframes blink {'
279         ret +=     'to { visibility: hidden; }'
280         ret += '}'
281         return ret
282
283 # key codes:
284 KEY_LEFT = 37
285 KEY_UP = 38
286 KEY_RIGHT = 39
287 KEY_DOWN = 40
288 KEY_BACKSPACE = 8 # <--
289 KEY_DELETE = 46 # -->
290 KEY_END = 35
291 KEY_ENTER = 13
292 KEY_ESCAPE = 27
293 KEY_HOME = 36
294 KEY_INSERT = 45
295 KEY_PAGE_UP = 33
296 KEY_PAGE_DOWN = 34
297 KEY_TAB = 9
298
299 ignore_key_codes =
300         '18': true # alt
301         '20': true # capslock
302         '17': true # ctrl
303         '144': true # numlock
304         '16': true # shift
305         '91': true # windows "start" key
306 control_key_codes = # we react to these, but they aren't typing
307         '37': KEY_LEFT
308         '38': KEY_UP
309         '39': KEY_RIGHT
310         '40': KEY_DOWN
311         '35': KEY_END
312         '8':  KEY_BACKSPACE
313         '46': KEY_DELETE
314         '13': KEY_ENTER
315         '27': KEY_ESCAPE
316         '36': KEY_HOME
317         '45': KEY_INSERT
318         '33': KEY_PAGE_UP
319         '34': KEY_PAGE_DOWN
320         '9':  KEY_TAB
321
322 instantiate_tree = (tree, parent) ->
323         remove = []
324         for c, i in tree
325                 switch c.type
326                         when TYPE_TEXT
327                                 c.el = parent.ownerDocument.createTextNode c.text
328                                 parent.appendChild c.el
329                         when TYPE_TAG
330                                 if c.name in ['script', 'object', 'iframe', 'link']
331                                         # TODO put placeholders instead
332                                         remove.unshift i
333                                 # TODO create in correct namespace
334                                 c.el = parent.ownerDocument.createElement c.name
335                                 for k, v of c.attrs
336                                         # FIXME if attr_whitelist[k]?
337                                         c.el.setAttribute k, v
338                                 parent.appendChild c.el
339                                 if c.children.length
340                                         instantiate_tree c.children, c.el
341         for i in remove
342                 tree.splice i, 1
343
344 traverse_tree = (tree, cb) ->
345         done = false
346         for c in tree
347                 done = cb c
348                 return done if done
349                 if c.children.length
350                         done = traverse_tree c.children, cb
351                         return done if done
352         return done
353
354 find_next_cursor_position = (tree, n, i) ->
355         if n.type is TYPE_TEXT and n.text.length > i
356                 orig_xyh = cursor_to_xyh n, i
357                 unless orig_xyh?
358                         console.log "ERROR: couldn't find xy for current cursor location"
359                         return
360                 for next_i in [i+1 .. n.text.length] # inclusive is valid (after last char)
361                         next_xyh = cursor_to_xyh n, next_i
362                         if next_xyh?
363                                 if next_xyh.x > orig_xyh.x or next_xyh.y > orig_xyh.y
364                                         return [n, next_i]
365         state_before = true
366         found = null
367         traverse_tree tree, (node, state) ->
368                 if node.type is TYPE_TEXT and state_before is false
369                         if cursor_to_xyh(node, 0)?
370                                 found = node
371                                 return true
372                 if node is n
373                         state_before = false
374                 return false
375         if found?
376                 return [found, 0]
377         return null
378
379 find_prev_cursor_position = (tree, n, i) ->
380         if n? and n.type is TYPE_TEXT and i > 0
381                 orig_xyh = cursor_to_xyh n, i
382                 unless orig_xyh?
383                         console.log "ERROR: couldn't find xy for current cursor location"
384                         return
385                 for prev_i in [i-1 .. 0]
386                         prev_xyh = cursor_to_xyh n, prev_i
387                         if prev_xyh?
388                                 if prev_xyh.x < orig_xyh.x or prev_xyh.y < orig_xyh.y
389                                         return [n, prev_i]
390                 return [n, i - 1]
391         found_prev = n?
392         found = null
393         traverse_tree tree, (node) ->
394                 if node.type is TYPE_TEXT
395                         if node is n
396                                 if found_prev?
397                                         found = found_prev
398                                 return true
399                         found_prev = node
400                 return false
401         if found?
402                 if cursor_to_xyh found, found.text.length # text visible?
403                         return [found, found.text.length]
404                 return find_prev_cursor_position tree, ret[0], 0
405         return null
406
407 find_loc_cursor_position = (tree, loc) ->
408         for c in tree
409                 if c.type is TYPE_TAG or c.type is TYPE_TEXT
410                         bounds = get_el_bounds c.el
411                         continue if loc.x < bounds.x
412                         continue if loc.x > bounds.x + bounds.w
413                         continue if loc.y < bounds.y
414                         continue if loc.y > bounds.y + bounds.h
415                         if c.children.length
416                                 ret = find_loc_cursor_position c.children, loc
417                                 return ret if ret?
418                         if c.type is TYPE_TEXT
419                                 # click is within bounding box that contains all text.
420                                 return [c, 0] if c.text.length is 0
421                                 before_i = 0
422                                 before = cursor_to_xyh c, before_i
423                                 unless before?
424                                         console.log "error: failed to find cursor pixel location for start of", c
425                                         return
426                                 after_i = c.text.length
427                                 after = cursor_to_xyh c, after_i
428                                 unless after?
429                                         console.log "error: failed to find cursor pixel location for end of", c
430                                         return
431                                 if loc.y < before.y + before.h and loc.x < before.x
432                                         # console.log 'before first char on first line'
433                                         continue
434                                 if loc.y > after.y and loc.x > after.x
435                                         # console.log 'after last char on last line'
436                                         continue
437                                 if loc.y < before.y
438                                         console.log "Warning: click in bounding box but above first line"
439                                         continue # above first line (runaround?)
440                                 if loc.y > after.y + after.h
441                                         console.log "Warning: click in bounding box but below last line", loc.y, after.y, after.h
442                                         continue # below last line (shouldn't happen?)
443                                 while after_i - before_i > 1
444                                         cur_i = Math.round((before_i + after_i) / 2)
445                                         cur = cursor_to_xyh c, cur_i
446                                         unless loc?
447                                                 console.log "error: failed to find cursor pixel location for", c, cur_i
448                                                 return
449                                         if loc.y < cur.y or (loc.y <= cur.y + cur.h and loc.x < cur.x)
450                                                 after_i = cur_i
451                                                 after = cur
452                                         else
453                                                 before_i = cur_i
454                                                 before = cur
455                                 # which one is closest?
456                                 if Math.abs(before.x - loc.x) < Math.abs(after.x - loc.x)
457                                         return [c, before_i]
458                                 else
459                                         return [c, after_i]
460         return null
461
462 # browsers collapse these (html5 spec calls these "space characters")
463 is_space_code = (char_code) ->
464         switch char_code
465                 when 9, 10, 12, 13, 32
466                         return true
467         return false
468 is_space = (chr) ->
469         return is_space_code chr.charCodeAt 0
470
471 tree_remove_empty_text_nodes = (tree) ->
472         empties = []
473         traverse_tree tree, (n) ->
474                 if n.type is TYPE_TEXT
475                         if n.text.length is 0
476                                 empties.unshift n
477                 return false
478         console.log empties
479         for n in empties
480                 # don't completely empty the tree
481                 if tree.length is 1
482                         if tree[0].type is TYPE_TEXT
483                                 console.log "oop, leaving a blank node because it's the only thing"
484                                 return
485                 n.el.parentNode.removeChild n.el
486                 console.log 'removing'
487                 for c, i in n.parent.children
488                         if c is n
489                                 n.parent.children.splice i, 1
490                                 console.log 'removed'
491                                 break
492
493 # pass a array of nodes (from parser library, ie it should have .el and .text)
494 tree_dedup_space = (tree) ->
495         prev = cur = next = null
496         prev_i = cur_i = next_i = 0
497         prev_pos = pos = next_pos = null
498         prev_px = cur_px = next_px = null
499         first = true
500         removed_char = null
501
502         tree_remove_empty_text_nodes(tree)
503
504         iterate = (tree, cb) ->
505                 for n in tree
506                         if n.type is TYPE_TEXT
507                                 i = 0
508                                 while i < n.text.length # don't foreach, cb might remove chars
509                                         advance = cb n, i
510                                         if advance
511                                                 i += 1
512                         if n.type is TYPE_TAG
513                                 block = is_display_block n.el
514                                 if block
515                                         cb null
516                                 if n.children.length > 0
517                                         iterate n.children, cb
518                                 if block
519                                         cb null
520         # remove cur char
521         remove = ->
522                 removed_char = cur.text.charAt(cur_i)
523                 cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + (cur.text.substr cur_i + 1)
524                 if next is cur # in same text node
525                         if next_i is 0
526                                 throw "how is this possible?"
527                         next_i -= 1
528                 return true
529         # undo remove()
530         put_it_back = ->
531                 cur.el.textContent = cur.text = (cur.text.substr 0, cur_i) + removed_char + (cur.text.substr cur_i)
532                 if next is cur # in same text node
533                         next_i += 1
534                 return false
535         # return true if cur was removed from the dom (ie re-use same prev)
536         operate = ->
537                 # cur definitately set
538                 # prev and/or next might be null, indicating the start/end of a display:block
539                 return false unless is_space_code cur.text.charCodeAt cur_i
540                 bounds = text_range_bounds cur.el, cur_i, cur_i + 1
541                 # consistent cases:
542                 # 1. zero rects returned by getClientRects() means collapsed space
543                 if bounds is null
544                         return remove()
545                 # 2. width greater than zero means visible space
546                 if bounds.w > 0
547                         return false
548                 # now the weird edge cases...
549                 #
550                 # firefox and chromium both report zero width for characters at the end
551                 # of a line where the text wraps (automatically, due to word-wrap) to
552                 # the next line. These do not appear to be distinguishable from
553                 # collapsed spaces via the range/bounds api, so...
554                 #
555                 # remove it from the dom, and if prev or next moves, put it back.
556                 if prev? and not prev_px?
557                         prev_px = cursor_to_xyh prev, prev_i
558                 if next? and not next_px?
559                         next_px = cursor_to_xyh next, next_i
560                 #if prev is null and next is null
561                 #       parent_px = cur.parent.el.getBoundingClientRect()
562                 remove()
563                 if prev?
564                         if prev_px?
565                                 new_prev_px = cursor_to_xyh prev, prev_i
566                                 if new_prev_px.x isnt prev_px.x or new_prev_px.y isnt prev_px.y
567                                         return put_it_back()
568                         else
569                                 console.log "this shouldn't happen, we remove spaces that don't locate"
570                 if next?
571                         if next_px?
572                                 new_next_px = cursor_to_xyh next, next_i
573                                 if new_next_px.x isnt next_px.x or new_next_px.y isnt next_px.y
574                                         return put_it_back()
575                         #else
576                         #       console.log "removing space becase space after it is collapsed"
577                 return true
578         # pass null at start/end of display:block
579         queue = (n, i) ->
580                 next = n
581                 next_i = i
582                 next_px = null
583                 advance = true
584                 if cur?
585                         removed = operate()
586                         # don't advance (to the next character next time) if we removed a
587                         # character from the same text node as ``next``, because doing so
588                         # renumbers the indexes in that string
589                         if removed and cur is next
590                                 advance = false
591                 else
592                         removed = false
593                 unless removed
594                         prev = cur
595                         prev_i = cur_i
596                         prev_px = cur_px
597                 cur = next
598                 cur_i = next_i
599                 cur_px = next_px
600                 return advance
601         queue null
602         iterate tree, queue
603         queue null
604
605         tree_remove_empty_text_nodes(tree)
606
607 class PeachHTML5Editor
608         # Options: (all optional)
609         #   editor_id: "id" attribute for outer-most element created by/for editor
610         #   on_init: callback for when the editable content is in place
611         constructor: (in_el, options) ->
612                 @options = options ? {}
613                 @in_el = in_el
614                 @tree = []
615                 @inited = false # when iframes have loaded
616                 @outer_iframe # iframe to hold editor
617                 @outer_idoc # "document" object for @outer_iframe
618                 @iframe = null # iframe to hold editable content
619                 @idoc = null # "document" object for @iframe
620                 @cursor = null
621                 @cursor_el = null
622                 @cursor_visible = false
623                 opt_fragment = @options.fragment ? true
624                 @parser_opts = {}
625                 if opt_fragment
626                         @parser_opts.fragment = 'body'
627
628                 @outer_iframe = domify document, iframe: {}
629                 outer_iframe_style = 'border: none !important; margin: 0 !important; padding: 0 !important; height: 100% !important; width: 100% !important;'
630                 if @options.editor_id?
631                         @outer_iframe.setAttribute 'id', @options.editor_id
632                 @outer_iframe.onload = =>
633                         @outer_idoc = @outer_iframe.contentDocument
634                         icss = domify @outer_idoc, style: children: [
635                                 domify @outer_idoc, text: css
636                         ]
637                         @outer_idoc.head.appendChild icss
638                         @iframe = domify @outer_idoc, iframe: {}
639                         @iframe.onload = =>
640                                 @init()
641                         setTimeout (=> @init() unless @inited), 200 # firefox never fires this onload
642                         @outer_idoc.body.appendChild(
643                                 domify @outer_idoc, div: id: 'wrap1', children: [
644                                         domify @outer_idoc, div: id: 'wrap2', children: [
645                                                 domify @outer_idoc, div: id: 'wrap3', children: [
646                                                         @iframe
647                                                         @overlay = domify @outer_idoc, div: id: 'overlay'
648                                                 ]
649                                         ]
650                                 ]
651                         )
652                 outer_wrap = domify document, div: class: 'peach_html5_editor'
653                 @in_el.parentNode.appendChild outer_wrap
654                 outer_bounds = get_el_bounds outer_wrap
655                 if outer_bounds.w < 300
656                         outer_bounds.w = 300
657                 if outer_bounds.h < 300
658                         outer_bounds.h = 300
659                 outer_iframe_style += "width: #{outer_bounds.w}px; height: #{outer_bounds.h}px;"
660                 @outer_iframe.setAttribute 'style', outer_iframe_style
661                 css = outer_css w: outer_bounds.w, h: outer_bounds.h
662                 outer_wrap.appendChild @outer_iframe
663         init: -> # called by @iframe's onload (or timeout on firefox)
664                 @idoc = @iframe.contentDocument
665                 @overlay.onclick = (e) =>
666                         return event_return @onclick e
667                 @overlay.ondoubleclick = (e) =>
668                         return event_return @ondoubleclick e
669                 @outer_idoc.body.onkeyup = (e) =>
670                         return event_return @onkeyup e
671                 @outer_idoc.body.onkeydown = (e) =>
672                         return event_return @onkeydown e
673                 @outer_idoc.body.onkeypress = (e) =>
674                         return event_return @onkeypress e
675                 if @options.stylesheet
676                         # TODO test this
677                         @idoc.head.appendChild domify @idoc, style: src: @options.stylesheet
678                 @load_html @in_el.value
679                 @inited = true
680                 if @options.on_init?
681                         @options.on_init()
682         onclick: (e) ->
683                 x = (e.offsetX ? e.layerX) - overlay_padding
684                 y = (e.offsetY ? e.layerY) - overlay_padding
685                 new_cursor = find_loc_cursor_position @tree, x: x, y: y
686                 if new_cursor?
687                         @move_cursor new_cursor
688                 return false
689         ondoubleclick: (e) ->
690                 return false
691         onkeyup: (e) ->
692                 return if e.ctrlKey
693                 return false if ignore_key_codes[e.keyCode]?
694                 #return false if control_key_codes[e.keyCode]?
695         onkeydown: (e) ->
696                 return if e.ctrlKey
697                 return false if ignore_key_codes[e.keyCode]?
698                 #return false if control_key_codes[e.keyCode]?
699                 switch e.keyCode
700                         when KEY_LEFT
701                                 if @cursor?
702                                         new_cursor = find_prev_cursor_position @tree, @cursor...
703                                         if new_cursor?
704                                                 @move_cursor new_cursor
705                                 else
706                                         for c in @tree
707                                                 new_cursor = find_next_cursor_position @tree, c, -1
708                                                 if new_cursor?
709                                                         @move_cursor new_cursor
710                                                         break
711                                 return false
712                         when KEY_UP
713                                 return false
714                         when KEY_RIGHT
715                                 if @cursor?
716                                         new_cursor = find_next_cursor_position @tree, @cursor...
717                                         if new_cursor?
718                                                 @move_cursor new_cursor
719                                 else
720                                         for c in @tree
721                                                 new_cursor = find_prev_cursor_position @tree, c, -1
722                                                 if new_cursor?
723                                                         @move_cursor new_cursor
724                                                         break
725                                 return false
726                         when KEY_DOWN
727                                 return false
728                         when KEY_END
729                                 return false
730                         when KEY_BACKSPACE
731                                 return false unless @cursor?
732                                 return false unless @cursor[1] > 0
733                                 @cursor[0].text = @cursor[0].text.substr(0, @cursor[1] - 1) + @cursor[0].text.substr(@cursor[1])
734                                 @cursor[0].el.nodeValue = @cursor[0].text
735                                 @move_cursor [@cursor[0], @cursor[1] - 1]
736                                 return false
737                         when KEY_DELETE
738                                 return false unless @cursor?
739                                 return false unless @cursor[1] < @cursor[0].text.length
740                                 @cursor[0].text = @cursor[0].text.substr(0, @cursor[1]) + @cursor[0].text.substr(@cursor[1] + 1)
741                                 @cursor[0].el.nodeValue = @cursor[0].text
742                                 @move_cursor [@cursor[0], @cursor[1]]
743                                 return false
744                         when KEY_ENTER
745                                 return false
746                         when KEY_ESCAPE
747                                 return false
748                         when KEY_HOME
749                                 return false
750                         when KEY_INSERT
751                                 return false
752                         when KEY_PAGE_UP
753                                 return false
754                         when KEY_PAGE_DOWN
755                                 return false
756                         when KEY_TAB
757                                 return false
758         onkeypress: (e) ->
759                 return if e.ctrlKey
760                 return false if ignore_key_codes[e.keyCode]?
761                 return false if control_key_codes[e.keyCode]? # handled in keydown
762                 char = e.charCode ? e.keyCode
763                 if char and @cursor?
764                         char = String.fromCharCode char
765                         if @cursor[1] is 0
766                                 @cursor[0].text = char + @cursor[0].text
767                         else if @cursor[1] is @cursor[0].text.length - 1
768                                 @cursor[0].text += char
769                         else
770                                 @cursor[0].text =
771                                         @cursor[0].text.substr(0, @cursor[1]) +
772                                         char +
773                                         @cursor[0].text.substr(@cursor[1])
774                         @cursor[0].el.nodeValue = @cursor[0].text
775                         @move_cursor [@cursor[0], @cursor[1] + 1]
776                         @changed()
777                 return false
778         clear_dom: -> # remove all the editable content (and cursor, overlays, etc)
779                 while @idoc.body.childNodes.length
780                         @idoc.body.removeChild @idoc.body.childNodes[0]
781                 @kill_cursor()
782                 return
783         load_html: (html) ->
784                 @tree = peach_parser.parse html, @parser_opts
785                 @clear_dom()
786                 instantiate_tree @tree, @idoc.body
787                 tree_dedup_space @tree
788                 @changed()
789         changed: ->
790                 # FIXME don't export cursor placeholder (when cursor is between space characters)
791                 @in_el.onchange = null
792                 @in_el.value = dom_to_html @tree
793                 @in_el.onchange = =>
794                         @load_html @in_el.value
795                 @iframe.style.height = "0"
796                 @iframe.style.height = "#{@idoc.body.scrollHeight}px"
797         kill_cursor: -> # remove it, forget where it was
798                 if @cursor_visible
799                         @cursor_el.parentNode.removeChild @cursor_el
800                         @cursor_visible = false
801                 @cursor = null
802         move_cursor: (cursor) ->
803                 loc = cursor_to_xyh cursor[0], cursor[1]
804                 unless loc?
805                         console.log "error: tried to move cursor to position that has no pixel location", cursor[0], cursor[1]
806                         return
807                 @cursor = cursor
808                 # replace cursor, to reset blink animation
809                 if @cursor_visible
810                         @cursor_el.parentNode.removeChild @cursor_el
811                 @cursor_el = domify @outer_idoc, div: id: 'cursor'
812                 @overlay.appendChild @cursor_el
813                 @cursor_visible = true
814                 # TODO figure out x,y coords for cursor
815                 @cursor_el.style.left = "#{loc.x + overlay_padding - 1}px"
816                 @cursor_el.style.top = "#{loc.y + overlay_padding}px"
817
818 window.peach_html5_editor = (args...) ->
819         return new PeachHTML5Editor args...
820
821 # test in browser: peach_html5_editor(document.getElementsByTagName('textarea')[0])