JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
parser fix: set .parent on text nodes
[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 # encode text so it can be safely placed inside an html attribute
18 enc_attr_regex = new RegExp '(&)|(")|(\u00A0)', 'g'
19 enc_attr = (txt) ->
20         return txt.replace enc_attr_regex, (match, amp, quote) ->
21                 return '&amp;' if (amp)
22                 return '&quot;' if (quote)
23                 return '&nbsp;'
24
25 void_elements = {
26         area: true
27         base: true
28         br: true
29         col: true
30         embed: true
31         hr: true
32         img: true
33         input: true
34         keygen: true
35         link: true
36         meta: true
37         param: true
38         source: true
39         track: true
40         wbr: true
41 }
42 dom_to_html = (dom) ->
43         ret = ''
44         for el in dom
45                 switch el.type
46                         when peach_parser.TYPE_TAG
47                                 ret += '<' + el.name
48                                 attr_keys = []
49                                 for k of el.attrs
50                                         attr_keys.unshift k
51                                 #attr_keys.sort()
52                                 for k in attr_keys
53                                         ret += " #{k}"
54                                         if el.attrs[k].length > 0
55                                                 ret += "=\"#{enc_attr el.attrs[k]}\""
56                                 ret += '>'
57                                 unless void_elements[el.name]
58                                         if el.children.length
59                                                 ret += dom_to_html el.children
60                                         ret += "</#{el.name}>"
61                         when peach_parser.TYPE_TEXT
62                                 ret += el.text
63                         when peach_parser.TYPE_COMMENT
64                                 ret += "<!--#{el.text}-->"
65                         when peach_parser.TYPE_DOCTYPE
66                                 ret += "<!DOCTYPE #{el.name}"
67                                 if el.public_identifier? and el.public_identifier.length > 0
68                                         ret += " \"#{el.public_identifier}\""
69                                 if el.system_identifier? and el.system_identifier.length > 0
70                                         ret += " \"#{el.system_identifier}\""
71                                 ret += ">\n"
72         return ret
73
74 domify = (h) ->
75         for tag, attrs of h
76                 if tag is 'text'
77                         return document.createTextNode attrs
78                 el = document.createElement tag
79                 for k, v of attrs
80                         if k is 'children'
81                                 for child in v
82                                         el.appendChild child
83                         else
84                                 el.setAttribute k, v
85         return el
86
87 css = ''
88 css += 'div#peach_editor_cursor {'
89 css +=     'display: inline-block;'
90 css +=     'height: 1em;'
91 css +=     'width: 2px;'
92 css +=     'margin-left: -1px;'
93 css +=     'margin-right: -1px;'
94 css +=     'background: #000;'
95 css +=     '-webkit-animation: 1s blink step-end infinite;'
96 css +=     'animation: 1s blink step-end infinite;'
97 css += '}'
98 css += '@-webkit-keyframes "blink" {'
99 css +=     'from, to { background: #000; }'
100 css +=     '50% { background: transparent; }'
101 css += '}'
102 css += '@keyframes "blink" {'
103 css +=     'from, to { background: #000; }'
104 css +=     '50% { background: transparent; }'
105 css += '}'
106
107 # key codes:
108 KEY_LEFT = 37
109 KEY_UP = 38
110 KEY_RIGHT = 39
111 KEY_DOWN = 40
112 KEY_BACKSPACE = 8 # <--
113 KEY_DELETE = 46 # -->
114 KEY_END = 35
115 KEY_ENTER = 13
116 KEY_ESCAPE = 27
117 KEY_HOME = 36
118 KEY_INSERT = 45
119 KEY_PAGE_UP = 33
120 KEY_PAGE_DOWN = 34
121 KEY_TAB = 9
122
123 wysiwyg = (el, options = {}) ->
124         opt_fragment = options.fragment ? true
125         parser_opts = {}
126         if opt_fragment
127                 parser_opts.fragment = 'body'
128         editor_instance = {
129                 dom: []
130                 iframe: document.createElement('iframe')
131                 load_html: (html) ->
132                         @dom = peach_parser.parse html, parser_opts
133                         as_html = peach.dom_to_html @dom
134                         as_html = as_html.substr(0, 5) + '<span class="peach_editor_cursor"></span>' + as_html.substr(5)
135                         @iframe.contentDocument.body.innerHTML = as_html
136         }
137         el.parentNode.appendChild editor_instance.iframe
138         idoc = editor_instance.iframe.contentDocument
139         ignore_key_codes =
140                 '18': true # alt
141                 '20': true # capslock
142                 '17': true # ctrl
143                 '144': true # numlock
144                 '16': true # shift
145                 '91': true # windows "start" key
146         control_key_codes = # we react to these, but they aren't typing
147                 '37': KEY_LEFT
148                 '38': KEY_UP
149                 '39': KEY_RIGHT
150                 '40': KEY_DOWN
151                 '35': KEY_END
152                 '8':  KEY_BACKSPACE
153                 '46': KEY_DELETE
154                 '13': KEY_ENTER
155                 '27': KEY_ESCAPE
156                 '36': KEY_HOME
157                 '45': KEY_INSERT
158                 '33': KEY_PAGE_UP
159                 '34': KEY_PAGE_DOWN
160                 '9':  KEY_TAB
161
162         idoc.body.onkeyup = (e) ->
163                 return false if ignore_key_codes[e.keyCode]?
164                 return false if control_key_codes[e.keyCode]?
165         idoc.body.onkeydown = (e) ->
166                 return false if ignore_key_codes[e.keyCode]?
167                 return false if control_key_codes[e.keyCode]?
168         idoc.body.onkeypress = (e) ->
169                 return if e.ctrlKey
170                 return false if ignore_key_codes[e.keyCode]?
171                 # in firefox, keyCode is only set for non-typing keys
172                 if e.keyCode isnt KEY_BACKSPACE # so this is fine
173                         return false if control_key_codes[e.keyCode]?
174                 char = e.charCode ? e.keyCode
175                 el.value += String.fromCharCode char
176                 editor_instance.load_html el.value
177                 return false
178         if options.stylesheet # TODO test this
179                 istyle = idoc.createElement 'style'
180                 istyle.setAttribute 'src', options.stylesheet
181                 idoc.head.appendChild istyle
182         icss = idoc.createElement 'style'
183         icss.appendChild idoc.createTextNode css
184         idoc.head.appendChild icss
185         editor_instance.load_html el.value
186         return editor_instance
187
188 window.peach = {
189         wysiwyg: wysiwyg
190         dom_to_html: dom_to_html
191 }
192
193 # test in browser: peach.wysiwyg(document.getElementsByTagName('textarea')[0])