/src/ajax-render.lisp

http://github.com/mtravers/wuwei · Lisp · 575 lines · 340 code · 83 blank · 152 comment · 2 complexity · a33fb9e89a1d49f83110ae7ecabf6df3 MD5 · raw file

  1. (in-package :wu)
  2. ;;; +=========================================================================+
  3. ;;; | Copyright (c) 2009, 2010 Mike Travers and CollabRx, Inc |
  4. ;;; | |
  5. ;;; | Released under the MIT Open Source License |
  6. ;;; | http://www.opensource.org/licenses/mit-license.php |
  7. ;;; | |
  8. ;;; | Permission is hereby granted, free of charge, to any person obtaining |
  9. ;;; | a copy of this software and associated documentation files (the |
  10. ;;; | "Software"), to deal in the Software without restriction, including |
  11. ;;; | without limitation the rights to use, copy, modify, merge, publish, |
  12. ;;; | distribute, sublicense, and/or sell copies of the Software, and to |
  13. ;;; | permit persons to whom the Software is furnished to do so, subject to |
  14. ;;; | the following conditions: |
  15. ;;; | |
  16. ;;; | The above copyright notice and this permission notice shall be included |
  17. ;;; | in all copies or substantial portions of the Software. |
  18. ;;; | |
  19. ;;; | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
  20. ;;; | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
  21. ;;; | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
  22. ;;; | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
  23. ;;; | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
  24. ;;; | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
  25. ;;; | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
  26. ;;; +=========================================================================+
  27. ;;; Author: Mike Travers
  28. #|
  29. Tools for rendering Ajax updates, based on the similar functionality found in Rails see:
  30. http://api.rubyonrails.org/classes/ActionView/Helpers/PrototypeHelper/JavaScriptGenerator/GeneratorMethods.html
  31. Note: to use these, you need to have prototype and (for some operations) scriptaculous.
  32. Here's an easy way to do include them:
  33. (javascript-includes "prototype.js" "effects.js" "dragdrop.js")
  34. For examples, see the test and examples directories.
  35. Available render-update operations, hopefully mostly self-explanatory. See the Rails and Prototype documentation for details.
  36. (:insert <position> <html>)
  37. (:update <id> <html>)
  38. (:replace <id> <html>)
  39. (:remove <id>)
  40. (:hide <id>)
  41. (:show <id>)
  42. (:toggle <id>)
  43. ;; +++ :add-class :remove-class, addClassName(<elt>, <name>)...
  44. (:draggable <id> <options>)
  45. (:drop-target <id> <options>)
  46. (:js <javascript>)
  47. (:redirect <url>)
  48. (:navigate <url>)
  49. (:reload)
  50. (:delay <seconds> <other-forms>)
  51. (:alert <msg>)
  52. (:visual_effect <effect-name> <id> <options>)
  53. Here's an example of combining render-update operations:
  54. (defun render-animated-delete (id &optional (factor 1))
  55. (render-update
  56. (:visual-effect :blind-up id :duration (* factor 0.25))
  57. (:visual-effect :fade id :duration (* factor 0.5))
  58. (:delay (* factor .5)
  59. (:remove id))))
  60. |#
  61. ;;; Define an update method handler
  62. (defmacro define-render-update (type args &body body)
  63. `(eval-when (:compile-toplevel :load-toplevel :execute)
  64. (setf (get ,type :renderer)
  65. (named-lambda ,type ,args
  66. ,@body))))
  67. (define-render-update :update (elt htmlspec)
  68. `(let ((html
  69. (with-output-to-string (s)
  70. (let ((*html-stream* s))
  71. (html ,htmlspec)))))
  72. (format *html-stream* "~%Element.update('~A', ~A);" ,elt (json:encode-json-to-string html))))
  73. (define-render-update :replace (elt htmlspec)
  74. `(let ((html
  75. (with-output-to-string (s)
  76. (let ((*html-stream* s))
  77. (html ,htmlspec)))))
  78. ;; This works better with FireFox, not sure why...
  79. ;; actually it doesn't, it leaves an extra DIV, so this is not right...
  80. ; (format *html-stream* "~%document.getElementById('~a').innerHTML=~A;" ,elt (json:encode-json-to-string html))
  81. (format *html-stream* "~%Element.replace('~A', ~A);" ,elt (json:encode-json-to-string html))
  82. ;;; another alternate form
  83. ; (format *html-stream* "~%$('~A').replace(~A);" ,elt (json:encode-json-to-string html))
  84. ))
  85. (define-render-update :insert (position elt htmlspec)
  86. `(let ((html
  87. (with-output-to-string (s)
  88. (let ((*html-stream* s))
  89. (html ,htmlspec)))))
  90. (format *html-stream* "~%Element.insert('~A',{~A: ~A});" ,elt (string-downcase (string ,position)) (json:encode-json-to-string html))))
  91. (defmacro define-render-element-operation (keyword &optional (func (string-downcase (string keyword))))
  92. `(define-render-update ,keyword (elt)
  93. `(format *html-stream* "~%Element.~A('~A');" ,,func ,elt)))
  94. (define-render-element-operation :hide)
  95. (define-render-element-operation :show)
  96. (define-render-element-operation :toggle)
  97. (define-render-element-operation :remove)
  98. (defun escape-single-quotes (string)
  99. (mt:string-replace string "'" "\\'"))
  100. ;;; pretty simple!
  101. (define-render-update :js (string)
  102. `(progn (terpri *html-stream*)
  103. (awhen ,string (write-string it *html-stream*))))
  104. ;;; A script that gets inserted after the normal updates
  105. (define-render-update :post-js (string)
  106. (render-script-later string))
  107. (define-render-update :redirect (url)
  108. `(format *html-stream* "~%window.location.replace('~A');" ,url))
  109. ;;; "redirect" and "navigate to new page" have different behaviors re browser history
  110. (define-render-update :navigate (url)
  111. `(format *html-stream* "~%window.location.href = '~A';" ,url))
  112. (define-render-update :popup (url &optional (name "_popup") (width 300) (height 300))
  113. `(format *html-stream* "~%window.open('~A', '~A', 'width=~A,height=~A');" ,url ,name ,width ,height))
  114. (define-render-update :reload ()
  115. `(format *html-stream* "~%window.location.reload();"))
  116. (define-render-update :delay (seconds &rest other-forms)
  117. `(progn
  118. (format *html-stream* "setTimeout(function() {")
  119. ,@(mapcar #'(lambda (clause)
  120. (apply (or (get (car clause) :renderer)
  121. (error "Don't know how to do operation ~A" (car clause)))
  122. (cdr clause)))
  123. other-forms)
  124. (format *html-stream* "}, ~A)" (* ,seconds 1000))))
  125. (define-render-update :visual-effect (effect elt &rest options)
  126. `(format *html-stream* "~%Effect.~A('~A', ~A);" (camel-case (string ,effect)) ,elt (json-options (list ,@options))))
  127. (define-render-update :alert (message)
  128. `(format *html-stream* "~%alert('~A');" (escape-single-quotes ,message)))
  129. ;;; dynamically bound to allow some things to change their behaviors.
  130. (defvar *within-render-update* nil)
  131. ;;; Mechanism for including js in HTML that might be an Ajax update or not.
  132. (defvar *render-update-scripts* nil)
  133. (defvar *render-debugging* nil)
  134. (defmacro render-debug (msg)
  135. `(when *render-debugging*
  136. (format t "~%render-debug: ~A" ,msg)))
  137. ;;; Wrap this around anything that does javascript updating.
  138. ;;; Saner version (duplicates body, might want to fix that with a closure or something +++)
  139. (defmacro with-render-update (&body body)
  140. `(if *within-render-update*
  141. (progn ,@body)
  142. (let ((*render-update-scripts* nil)
  143. (*within-render-update* t))
  144. ,@body
  145. (render-update-scripts))))
  146. (defun render-script-later (script)
  147. (push-end script *render-update-scripts*))
  148. ;;; Render
  149. (defun render-update-scripts ()
  150. (dolist (script *render-update-scripts*)
  151. (render-debug (list 'script-out script))
  152. (write-string script *html-stream*)))
  153. (defmacro render-update (&body clauses)
  154. `(with-render-update
  155. ,@(mapcar #'(lambda (clause)
  156. (apply (or (get (car clause) :renderer)
  157. (error "Don't know how to do operation ~A" (car clause)))
  158. (cdr clause)))
  159. clauses)))
  160. ;;; Like render-update, but for use within HTML blocks.
  161. ;;; Will either render scripts in script element as part of a page, or (if done inside an render-update) collect them for
  162. ;;; appending to the update.
  163. (defmacro render-scripts (&body clauses)
  164. `(if *within-render-update*
  165. (render-script-later (html-string
  166. (render-update ,@clauses)))
  167. (html ((:script :type "text/javascript")
  168. (render-debug "rendering <script> elt")
  169. (render-update ,@clauses)))
  170. ))
  171. ;;; Version of above that takes raw script rather than render-update clauses
  172. (defmacro render-script (script)
  173. `(if *within-render-update*
  174. (render-script-later ,script)
  175. (html ((:script :type "text/javascript")
  176. (render-debug "rendering <script> elt")
  177. :newline
  178. (:princ ,script))
  179. )))
  180. (defun html-escape-string (string)
  181. (with-output-to-string (stream)
  182. (net.html.generator::emit-safe stream string)))
  183. ;; Amazingly, FireFox will generate close tags for lisp objects
  184. ;; printed in pointy brackets, but then we *are* lying about the
  185. ;; content type.
  186. ;; This is not real JSON escaping....also not very efficient
  187. (defun clean-upload-js-string (string)
  188. (string-replace
  189. (string-replace string ">" "&gt;")
  190. "<" "&lt;")
  191. )
  192. ;;; Not very efficient
  193. (defun clean-js-string (string)
  194. (string-replace
  195. (string-replace
  196. (string-replace string (string #\Newline) "\\n")
  197. "\"" "\\\"")
  198. "'" "\\'"))
  199. (defvar *multipart-request*)
  200. (defvar *ajax-request* nil)
  201. ;; If the client performs a file upload, an HTML form is used and a page of type text/html must be returned
  202. ;;; +++ why is this a macro?
  203. (defmacro multipart? (req)
  204. `(let ((header (header-slot-value ,req :content-type)))
  205. (and header
  206. (string= (subseq header 0 (min (length header) (length "multipart/form-data")))
  207. "multipart/form-data"))))
  208. ;;;; :::::::::::::::::::::::::::::::: Ajax-continuation and friends ::::::::::::::::::::::::::::::::
  209. #|
  210. publish-ajax-update, publish-ajax-func, and ajax-continuation all do similar things: that is,
  211. they publish a temporary URL that, upon receipt at server, executes BODY.
  212. - publish-ajax-update is the lowest level
  213. - publish-ajax-func wraps publish-ajax-update, but also implements argument processing (that is, it
  214. will convert request parameters into variables bound around BODY).
  215. - ajax-continuation wraps publish-ajax-func, and in adddition handles automatically generating the
  216. URL and decomissioning it when done.
  217. Options
  218. :SESSION : NIL if no session management, T for session management, or the name of login handling
  219. procedure if login is required. The procedure takes req and ent and is responsible for
  220. redirecting to a login page.
  221. :PRE-RESPONSE is a list of forms executed before the response (so things that affect cookie session
  222. state should go there)
  223. :KEEP (ajax-continuation) T if continuation should be kept around after use (that is, it is something
  224. that may be called more than once).
  225. :ARGS (ajax-continuation) a list of arguments to be bound around BODY, and should be supplied in the POST.
  226. :CONTENT-TYPE
  227. :TIMEOUT (specifiy a timeout, looks like this is not actually implemented)
  228. |#
  229. (defmacro publish-ajax-update (path-or-options &body body)
  230. (let* ((path (if (listp path-or-options)
  231. (findprop :path path-or-options)
  232. path-or-options))
  233. (options (if (listp path-or-options) path-or-options))
  234. (content-type (and (listp path-or-options) (findprop :content-type path-or-options)))
  235. (session (aif (and (listp path-or-options) (member :session path-or-options))
  236. (cadr it)
  237. t)) ;defaults to t
  238. (pre-response (and (listp path-or-options) (findprop :pre-response path-or-options)))
  239. (login-handler (aif (and (listp path-or-options) (member :login-handler path-or-options))
  240. (cadr it))))
  241. (setf options (delete-keyword-args '(:path :session :pre-response) options))
  242. `(publish-temporarily ,path
  243. :function #'(lambda (req ent)
  244. (let* ((*multipart-request* (multipart? req))
  245. (*ajax-request* req)
  246. ;; +++ not sure what this condition on *multipart-request* was for, seems wrong
  247. (content-type ,(or content-type `(if *multipart-request* "text/html" "text/javascript"))))
  248. (,@(if session
  249. `(with-session (req ent ,@(if login-handler `(:login-handler ,login-handler))))
  250. '(progn))
  251. (with-http-response-and-body (req ent :content-type content-type)
  252. (with-ajax-error-handler (,path)
  253. (with-render-update
  254. ,@body
  255. )))))
  256. )
  257. ,@options
  258. )))
  259. (defmacro publish-ajax-func (path-or-options args &rest body)
  260. `(publish-ajax-update ,path-or-options
  261. (let (,@(mapcar #'(lambda (arg)
  262. `(,arg (request-query-value ',(smart-string (string arg)) req)))
  263. args))
  264. ,@body)))
  265. (defvar *ajax-counter* 0)
  266. (defmacro ajax-continuation ((&key args keep (content-type "text/javascript") (session nil session-spec?) name login-handler timeout) &body body)
  267. `(let ((fname (string+ "/ajax/" ,(or name "g") "/" (fast-string (incf *ajax-counter*)))))
  268. (publish-ajax-func (:path fname
  269. ,@(if content-type `(:content-type ,content-type))
  270. ,@(if session-spec? `(:session ,session))
  271. ,@(if login-handler `(:login-handler ,login-handler))
  272. ,@(if timeout `(:timeout ,timeout)))
  273. ,args
  274. ,@body
  275. ,(unless keep
  276. '(unpublish-path fname)))
  277. fname))
  278. ;;; Inexplicably not in aserve
  279. ;;; Later versions of aserve have a non-functional function called unpublish, so we give this a different name
  280. (defun unpublish-path (path)
  281. (net.aserve::unpublish-entity (net.aserve::find-locator :exact *wserver*) path nil nil))
  282. (defun publish-temporarily (path &rest args)
  283. (apply 'publish :path path args)
  284. (set-responder-timeout path))
  285. ;; could keep this sorted I supposed
  286. (defvar *responder-timeouts* nil)
  287. (defun set-responder-timeout (path &optional (time (+ (now) *default-responder-timeout*)))
  288. (push (list time path) *responder-timeouts*))
  289. (publish-prefix :prefix "/ajax/"
  290. :function 'ajax-timeout)
  291. (defun ajax-timeout (req ent)
  292. (with-http-response-and-body (req ent :content-type "text/javascript")
  293. (render-update
  294. (:alert "Command expired. Try reloading the page"))))
  295. (defun do-responder-timeouts ()
  296. (let* ((now (get-universal-time))
  297. (expired (filter #'(lambda (item)
  298. (< (car item) now))
  299. *responder-timeouts*)))
  300. (dolist (item expired)
  301. (unpublish-path (cadr item)))
  302. (setf *responder-timeouts* (nset-difference *responder-timeouts* expired))))
  303. #-:SBCL
  304. (eval-when (:load-toplevel :execute)
  305. (in-background "Responder timeout"
  306. (loop
  307. (sleep (floor *default-responder-timeout* 2))
  308. (do-responder-timeouts))))
  309. ;;; Drag/drop
  310. ;;; See here for description of options: http://wiki.github.com/madrobby/scriptaculous/draggable
  311. ;;; to make this useful, need patched cl-json that can do :raw strings.
  312. (define-render-update :draggable (elt &rest options)
  313. `(format *html-stream* "~%new Draggable('~A', ~A);"
  314. ,elt
  315. (json-options (list ,@options))))
  316. ;;; Define an entire CSS class as draggable.
  317. ;;; uses a local extension to scriptaculous. Does not apply to elements or classes added after the fact, a serious limitation.
  318. (define-render-update :draggable-class (class &rest options)
  319. `(format *html-stream* "~%Draggable.addClass('~A', null, ~A);"
  320. ,class
  321. (json-options (list ,@options))))
  322. ;;; options are as specified by scriptaculous, see http://wiki.github.com/madrobby/scriptaculous/droppables
  323. ;;; Particularly useful:
  324. ;;; :accept <css class> or list of classes -- specifies what can be dropped here.
  325. ;;; :|onDrop| `(:raw "function (elt) {...}") -- call the function when a drop occurs
  326. (define-render-update :drop-target (elt &rest options)
  327. `(format *html-stream* "~%Droppables.add('~A', ~A);"
  328. ,elt
  329. (json-options (list ,@options))))
  330. ;;; turn keywords (:k1 v1 :k2 v2 ...) into CL-JSON
  331. (defun json-options-transform (options)
  332. (do ((rest options (cddr rest))
  333. (result nil))
  334. ((null rest)
  335. (nreverse result))
  336. (push (cons (smart-string (car rest)) (cadr rest))
  337. result)))
  338. ;; as above but produce JSON string
  339. (defun json-options (options)
  340. (json:encode-json-to-string
  341. (json-options-transform options)))
  342. ;;; UPCASE turned to downcase, mixed case is left alone. Needs a better name
  343. (defun smart-string (k)
  344. (let ((s (string k)))
  345. (if (string-upcase? s)
  346. (string-downcase s)
  347. s)))
  348. (defun string-upcase? (s)
  349. (every #'(lambda (c)
  350. (or (not (alpha-char-p c))
  351. (upper-case-p c)))
  352. s))
  353. ;;; Equivalent of link_to_remote etc. Could take more options.
  354. ;;; We can now deal with arbitrary html-options, so regularize the calling sequence of these...
  355. ;;; default is :princ rather than :princ-safe to allow image tags in text.
  356. ;;; Should be rethought, maybe this should be a macro that wraps arbitrary html gen.
  357. (defun link-to-function (text js &key html-options safe?)
  358. (html
  359. ((:a :href "#" :onclick js :do* html-options)
  360. (if safe?
  361. (html (:princ-safe text))
  362. (html (:princ text))))) )
  363. (defun button-to-function (text js &key html-options)
  364. (html
  365. ((:input :type "button" :value text :onclick js :do* html-options))))
  366. (defun link-to-remote (text url &rest remote-function-options &key html-options &allow-other-keys)
  367. (link-to-function text (apply #'remote-function url (delete-keyword-args '(:html-options) remote-function-options))
  368. :html-options html-options))
  369. (defun button-to-remote (text url &rest remote-function-options &key html-options &allow-other-keys)
  370. (button-to-function text (apply #'remote-function url (delete-keyword-args '(:html-options) remote-function-options))
  371. :html-options html-options))
  372. (defun checkbox-to-function (text js &key html-options)
  373. (html
  374. ((:input :type "checkbox" :onclick js :do* html-options))
  375. (:princ "&nbsp;")
  376. (:princ-safe (or text ""))
  377. ))
  378. ;;; +++ SBCL sniffs at having &optional and &key in the same arglist, and maybe it should be changed
  379. ;;; +++ copy params, class keyword functionality to link-to-remote, button-to-remote, etc
  380. (defun checkbox-to-remote (text url &optional checked? &rest remote-function-options &key params (id (string (gensym "check"))) class html-options &allow-other-keys)
  381. (checkbox-to-function
  382. text
  383. (apply #'remote-function url :in-function? nil :params `(:checked (:raw ,(format nil "$('~A').checked" id)) ,@params) (delete-keyword-args '(:html-options :id :class) remote-function-options))
  384. :html-options
  385. `(:id ,id ,@(if class `(:class ,class)) ,@(if checked? '(:checked "true")) ,@html-options)))
  386. (defun radio-to-remote (text url &optional checked? &rest remote-function-options &key html-options &allow-other-keys)
  387. (html
  388. ((:input :type :radio :if* checked? :checked "true" :onclick (apply #'remote-function url (delete-keyword-args '(:html-options) remote-function-options))
  389. :do* html-options)
  390. (:princ "&nbsp;")
  391. (:princ-safe (or text ""))
  392. )))
  393. (defun goto-url-function (url)
  394. (format nil "~%window.location.href = '~A';" url))
  395. (defvar *uploader-html*
  396. (concatenate 'string
  397. "<div id='~a'></div>"
  398. "<script TYPE='text/javascript'>make_uploader('~a', '~a', '~a', ~a);</script>"
  399. ))
  400. (defparameter *file-field-name* "Data")
  401. (defun uploader (id url &optional isDrugrank)
  402. (format nil *uploader-html* id id url *file-field-name* (if isDrugrank "true" "false"))
  403. )
  404. ;;; Note: response content type of text/javascript determines whether response is evaled or not
  405. (defun remote-function (url &key form params (in-function? t) confirm before after spinner
  406. success failure complete eval-scripts? stop-propagation?
  407. updater? periodic?)
  408. #.(doc
  409. "Generate a remote function (javascript Ajax call)"
  410. " ex: (remote-function \"/new-chunk\" :params `(:user ,user :type (:raw ,(format nil \"$(~A).value\" selector-id))))"
  411. " returns:"
  412. " new Ajax.Request('/new-chunk', {\"asynchronous\":true,\"parameters\":{\"user\":\"mt\",\"type\":$(selector23).value}}); return false;"
  413. ":form If t, serialize the surrounding form; if a string serialise the form with that name; else use params"
  414. ":params List of (:key1 value1 ...), ignored if :form is t"
  415. ":confirm Ask user for confirmation first (value is the message)"
  416. ":complete Javascript to execute when action completes"
  417. ":success as :complete, but on success only"
  418. ":failure as :complete, but on failure only"
  419. ":before Javascript to run before the Ajax request"
  420. ":after Javascript to run after the Ajax request"
  421. ":spinner The ID of an elt, a spinner will be inserted after the elt before the Ajax request and removed when completed"
  422. ":in-function? "
  423. ":eval-scripts? " ;;; +++ only valid for Ajax.Update object?
  424. ":stop-propagation? Stop propagation of events to parents. Forces :in-function? to be nil"
  425. ":updater? Make an Ajax.Updater object rather than an Ajax.Request; value is dom id of item to be updated"
  426. ":periodic? Make an Ajax.PeriodicalUpdater, updater? must be non-nil"
  427. )
  428. (when stop-propagation?
  429. (setq in-function? nil)) ;incompatible, at least for now.
  430. (when spinner
  431. (let ((spin-js (format nil "add_spinner('~A');" spinner))
  432. (nospin-js (format nil "remove_spinner('~A');" spinner)))
  433. (setf before (if before
  434. (string+ before spin-js)
  435. spin-js))
  436. (setf complete (if complete
  437. (string+ nospin-js complete)
  438. nospin-js))))
  439. (let* ((options
  440. `(:asynchronous t
  441. :parameters ,(if form
  442. `(:raw ,(format nil "Form.serialize(~A)"
  443. (if (stringp form)
  444. (format nil "document.getElementById('~A')" form)
  445. "this")))
  446. (json-options-transform params))
  447. ,@(if complete `("onComplete" (:raw ,(format nil "function(request){~A}" complete))))
  448. ,@(if success `("onSuccess" (:raw ,(format nil "function(request){~A}" success))))
  449. ,@(if failure `("onFailure" (:raw ,(format nil "function(request){~A}" failure))))
  450. ,@(if eval-scripts? `("evalScripts" t))
  451. ))
  452. (result
  453. (cond (periodic?
  454. (assert updater?)
  455. (setf options (append `(:frequency ,periodic?) options))
  456. (format nil "new Ajax.PeriodicalUpdater('~A', '~A', ~A);" updater? url (json-options options)))
  457. (updater?
  458. (format nil "new Ajax.Updater('~A', '~A', ~A);" updater? url (json-options options)))
  459. (t
  460. (format nil "new Ajax.Request('~A', ~A);" url (json-options options))))))
  461. (when before (setf result (string+ before result)))
  462. (when after (setf result (string+ result after)))
  463. (when confirm (setf result (format nil "if (confirm('~A')) { ~A };" confirm result)))
  464. (when stop-propagation?
  465. (setf result (format nil "~A Event.stop(event);" result)))
  466. (when in-function?
  467. (setf result (string+ result "return false;")))
  468. result))