# Copyright 2015 Jason Woofenden
# This file implements an WYSIWYG editor in the browser (no contenteditable)
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
# encode text so it can be safely placed inside an html attribute
enc_attr_regex = new RegExp '(&)|(")|(\u00A0)', 'g'
enc_attr = (txt) ->
return txt.replace enc_attr_regex, (match, amp, quote) ->
return '&' if (amp)
return '"' if (quote)
return ' '
void_elements = {
area: true
base: true
br: true
col: true
embed: true
hr: true
img: true
input: true
keygen: true
link: true
meta: true
param: true
source: true
track: true
wbr: true
}
dom_to_html = (dom) ->
ret = ''
for el in dom
switch el.type
when peach_parser.TYPE_TAG
ret += '<' + el.name
attr_keys = []
for k of el.attrs
attr_keys.unshift k
#attr_keys.sort()
for k in attr_keys
ret += " #{k}"
if el.attrs[k].length > 0
ret += "=\"#{enc_attr el.attrs[k]}\""
ret += '>'
unless void_elements[el.name]
if el.children.length
ret += dom_to_html el.children
ret += "#{el.name}>"
when peach_parser.TYPE_TEXT
ret += el.text
when peach_parser.TYPE_COMMENT
ret += ""
when peach_parser.TYPE_DOCTYPE
ret += " 0
ret += " \"#{el.public_identifier}\""
if el.system_identifier? and el.system_identifier.length > 0
ret += " \"#{el.system_identifier}\""
ret += ">\n"
return ret
domify = (h) ->
for tag, attrs of h
if tag is 'text'
return document.createTextNode attrs
el = document.createElement tag
for k, v of attrs
if k is 'children'
for child in v
el.appendChild child
else
el.setAttribute k, v
return el
css = ''
css += 'div#peach_editor_cursor {'
css += 'display: inline-block;'
css += 'height: 1em;'
css += 'width: 2px;'
css += 'margin-left: -1px;'
css += 'margin-right: -1px;'
css += 'background: #000;'
css += '-webkit-animation: 1s blink step-end infinite;'
css += 'animation: 1s blink step-end infinite;'
css += '}'
css += '@-webkit-keyframes "blink" {'
css += 'from, to { background: #000; }'
css += '50% { background: transparent; }'
css += '}'
css += '@keyframes "blink" {'
css += 'from, to { background: #000; }'
css += '50% { background: transparent; }'
css += '}'
# key codes:
KEY_LEFT = 37
KEY_UP = 38
KEY_RIGHT = 39
KEY_DOWN = 40
KEY_BACKSPACE = 8 # <--
KEY_DELETE = 46 # -->
KEY_END = 35
KEY_ENTER = 13
KEY_ESCAPE = 27
KEY_HOME = 36
KEY_INSERT = 45
KEY_PAGE_UP = 33
KEY_PAGE_DOWN = 34
KEY_TAB = 9
wysiwyg = (el, options = {}) ->
opt_fragment = options.fragment ? true
parser_opts = {}
if opt_fragment
parser_opts.fragment = 'body'
editor_instance = {
dom: []
iframe: document.createElement('iframe')
load_html: (html) ->
@dom = peach_parser.parse html, parser_opts
as_html = peach.dom_to_html @dom
as_html = as_html.substr(0, 5) + '' + as_html.substr(5)
@iframe.contentDocument.body.innerHTML = as_html
}
el.parentNode.appendChild editor_instance.iframe
idoc = editor_instance.iframe.contentDocument
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
'39': KEY_RIGHT
'40': KEY_DOWN
'35': KEY_END
'8': KEY_BACKSPACE
'46': KEY_DELETE
'13': KEY_ENTER
'27': KEY_ESCAPE
'36': KEY_HOME
'45': KEY_INSERT
'33': KEY_PAGE_UP
'34': KEY_PAGE_DOWN
'9': KEY_TAB
idoc.body.onkeyup = (e) ->
return false if ignore_key_codes[e.keyCode]?
return false if control_key_codes[e.keyCode]?
idoc.body.onkeydown = (e) ->
return false if ignore_key_codes[e.keyCode]?
return false if control_key_codes[e.keyCode]?
idoc.body.onkeypress = (e) ->
return if e.ctrlKey
return false if ignore_key_codes[e.keyCode]?
# in firefox, keyCode is only set for non-typing keys
if e.keyCode isnt KEY_BACKSPACE # so this is fine
return false if control_key_codes[e.keyCode]?
char = e.charCode ? e.keyCode
el.value += String.fromCharCode char
editor_instance.load_html el.value
return false
if options.stylesheet # TODO test this
istyle = idoc.createElement 'style'
istyle.setAttribute 'src', options.stylesheet
idoc.head.appendChild istyle
icss = idoc.createElement 'style'
icss.appendChild idoc.createTextNode css
idoc.head.appendChild icss
editor_instance.load_html el.value
return editor_instance
window.peach = {
wysiwyg: wysiwyg
dom_to_html: dom_to_html
}
# test in browser: peach.wysiwyg(document.getElementsByTagName('textarea')[0])