; Copyright (C) 2020 Thomas Elam
;
; This file is part of web-call.cc.
;
; web-call.cc is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation, either version 3 of the License, or
; (at your option) any later version.
;
; web-call.cc 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 General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with web-call.cc. If not, see .
(load "scheme/mini-framework.scm")
;;;; Console and messages
(define (console-log-fmt str obj)
(console-log (format #f str obj)))
(define (console-dir obj)
(js-call% "console.dir" obj))
(define (console-group str)
(js-call% "console.group" str))
(define (console-group-end)
(js-call% "console.groupEnd"))
;;;; EVENTS
;; originalEvent is referenced by a jQuery event. It is what it says: the original event,
;; unmassaged by jQuery.
(define (get-original-event jquery-event)
(js-ref jquery-event "originalEvent"))
;; dataTransfer is part of a dragstart event, but not part of a generic jQuery event.
(define (get-data-transfer-obj jquery-event)
;; FIXME: Is it really necessary to dereference twice?
(js-ref (get-original-event jquery-event) "dataTransfer"))
(define (prevent-default jq-event)
(js-invoke jq-event "preventDefault"))
(define (stop-propagation jq-event)
(js-invoke jq-event "stopPropagation"))
(define (src-element jq-event)
(js-ref jq-event "srcElement"))
(define (target jq-event)
(js-ref jq-event "target"))
(define (page-x jq-event)
(js-ref (js-ref jq-event "originalEvent") "pageX"))
(define (page-y jq-event)
(js-ref (js-ref jq-event "originalEvent") "pageY"))
;;;; AJAX
(js-eval "define_libfunc('http-post-text', 2, 2, function(ar){
var path = ar[0];
assert_string(path);
var data = ar[1];
assert_string(data);
return new BiwaScheme.Pause(function(pause){
$.ajax({
'type': 'POST',
'url': path,
'contentType': 'text/plain',
'data': data,
'dataType': 'text/plain',
'success': pause.resume(data)
});
});
})")
;;;; DOM navigation
;; Get the first element matching the selector.
(define (getelem1 selector)
(js-ref (getelem selector) "0"))
(define (js-childNodes node)
(js-ref node "childNodes"))
(define (js-child-nodes node)
(js-array->list (js-childNodes node)))
(define (js-children element)
(js-ref element "children"))
(define (js-children-list element)
(js-array->list (js-children element)))
;;;; DOM serialization
(define (node-type node)
(js-ref node "nodeType"))
(define (node-name node)
(string->symbol (js-ref node "nodeName")))
(define (element-attributes element)
(let* ((attrs (js-ref element "attributes"))
(attr-list (js-array->list attrs)))
(map (lambda (attr-node)
(list (string->symbol (js-ref attr-node "name"))
(js-ref attr-node "value")))
attr-list)))
(define (children->sexp nodes)
(map dom->sexp nodes))
(define (element->sexp element)
(cons (node-name element) (element-attributes element)))
(define (dom->string node)
(format "~s" (dom->sexp node)))
;; FIXME:
;; This just tests for ELEMENT_NODE and TEXT_NODE, but there are many
;; other types of node that should be handled. See https://mzl.la/2zcB5om.
(define (dom->sexp node)
(case (node-type node)
((1) ;; Node.ELEMENT_NODE
(cons
(element->sexp node)
(children->sexp (js-child-nodes node))))
((3) ;; Node.TEXT_NODE
(js-ref node "data"))
(else (node-name node))))
;; Example HTTP request using the output of dom->string:
;; (http-post "/doms" '(("dom" . "((DIV (id \"junk\") (style \"background-color:yellow;\")) \" \" ((OL) \"
;; \" ((LI) \"One\") \" \" ((LI) \"Two\") \" \" ((LI) \"Three\") \"
;; \") \" \")")))
;;;; DOM deserialization
;; FIXME: This doesn't handle input like "\n ".
(define (text-node-new node)
(display (string-append "text-node-new: " node))
(display (string-length node))
(js-eval (string-append "document.createTextNode('" node "')")))
(define (string->sexp str)
(read (open-input-string str)))
(define (symbol->element part)
(display part)
(cond
((list? part)
(display "symbol->element: got a list")
(element-new (car part)))
(else
(display "symbol->element: got a non-list")
;; This doesn't work, probably because it's not quoted for JavaScript:
(text-node-new part))))
(define (sexp->dom sexp)
(map symbol->element sexp))
;;;; DOM operations cognisant of custom elements: find target, clone, move, append, insert
;;
;; The HTML API's cloneNode method cannot be used for copying DOMs because:
;; * Some custom elements, such as Dojo widgets, are not wholly draggable,
;; so that when the user clicks on one of them, the target of the
;; dragstart event does not contain the whole widget.
;; * The id attribute of an element should not be used twice.
(define (clone-dom target)
(js-call% "cloneDOM" target))
;;;; Input
;; get-sym/datum - get a Scheme symbol or datum via keyboard or by dragging an HTML element representing a symbol
;;
;; FIXME: The logic in this function is rather complex. Check it.
;;
;; We will not try to support fully standard Scheme datum syntax, but here is a reference to the symbol syntax:
;;
;; The formal grammar of Scheme variables as defined in TSPL4:
;; --> *
;; --> | ! | $ | % | & | * | / | : | < | = | > | ? | ~ | _ | ^
;; |
;; | \x
;; --> | | . | + | - | @ |
;; --> a | b | ... | z | A | B | ... | Z
(define (get-sym/datum whence prompt)
(let ((textarea (element-new '(textarea id "word" rows "1" cols "42"))))
(element-append-child! whence textarea)
(sleep 0.1) ;; FIXME
(js-set! textarea "value" prompt)
(js-invoke textarea "focus")
(js-set! textarea "selectionEnd" 0)
(with-handlers ((keyup-handler "#word")
(dragstart-handler ".draggables > dojo-button"))
(let ((input #f)
(char #f)
(finished #f)
(aborted #f)
(str #f)
(real-num #f)
(first-char #t)
(got-sym #f))
;; Get and examine the first character or dragged HTML element
(set! input (get-input))
(console-log (format #f "Got event => ~a" (first input)))
;;(if (string=? "key")
;; (begin
(set! char (event->charcode (second input)))
(console-log (format #f "char => ~a" char))
(set! str (extract-text textarea))
(case char
((43 189 46 48 49 50 51 52 53 54 55 56 57) ; First character is +, -, ., or digit.
;; Getting number
(begin
;; Upon getting the first key, clear the prompt.
(clear-prompt! textarea)
(set! str (extract-text textarea))
(while (not finished)
(begin
(set! char (get-char))
(set! str (extract-text textarea))
(case char
((8) ; FIXME: This is just to test to see if a BACKSPACE can be input.
(console-log "BACKSPACE"))
((13 32) ; RETURN, SPACE. Considered terminator characters.
(set! finished #t)
(str-rm-last-char! str))
((27) ; ESCAPE. Considered an abort character.
(set! finihed #t)
(set! aborted #t)
(set-rm-last-char! str))
((46) ; Period
(if real-num
(str-rm-last-char! str) ; Remove extraneous '.'.
(set! real-num #t)))
((43 45) ; +, -
(if first-char
(set! signed #t)
(str-rm-last-char! str))) ; Remove extraneous sign.
((48 49 50 51 52 53 54 55 56 57) ;; Digit.
(console-log "digit"))
(else
(str-rm-last-char! str)))
(set! first-char #f))))) ; Remove extraneous character.
;; Testing for symbol
(else
(case char
((33 36 37 38 42 47 58 60 61 62 63 126 95 94) ; !, $, %, &, *, /, :, <, =, >, ?, ~, _, ^
(console-log "symbol name initial character, not letter")
(set! got-sym #t))
(else
(if (letter? char)
(set! got-sym #t)
(begin
(console-log "initial character not of number or symbol")
(clear-prompt! textarea)
(set! str "")))))
(if (and got-sym (not finished))
;; Getting symbol
(begin
;; Upon getting the first key, clear the prompt.
(clear-prompt! textarea)
(set! str (extract-text textarea))
(while (not finished)
(begin
(set! char (get-char))
(set! str (extract-text textarea))
;; FIXME: Use cond instead of case?
(case char
((48 49 50 51 52 53 54 55 56 57) ; Digit
(console-log "digit"))
((13 32) ; RETURN, SPACE. Considered terminator characters.
(set! finished #t)
(str-rm-last-char! str))
((27) ; ESCAPE. Considered a cancel character.
(set! finihed #t)
(set! aborted #t)
(set-rm-last-char! str))
(else
(if (letter? char)
(console-log "letter")
(str-rm-last-char! str))))
(set! first-char #f)))))))
(element-remove! textarea)
str))))
(define (letter? char)
(or
(and (> char 64) (< char 91)) ;; [A-Z]
(and (> char 96) (< char 123)))) ;; [a-z]
(define (get-char)
(event->charcode (second (get-input))))
(define (event->charcode jq-event)
(js-ref jq-event (if (js-undefined? (js-ref jq-event "which"))
"keyCode" ; For IE8
"which")))
(define (extract-text textarea)
(js-ref textarea "value"))
(define (clear-prompt! textarea)
(js-set! textarea "value" (substring (js-ref textarea "value") 0 1)))
;;;; DOM mutation
(define (append-html! elem str)
(js-call% "appendHTML" elem str))
(define (get-inner-html elem)
(js-ref elem "innerHTML"))
(define (set-inner-html! elem str)
(js-set! elem "innerHTML" str))
(define (append-to-inner-html! elem str)
(js-set! elem "innerHTML" (string-append (js-ref elem "innerHTML") str " ")))
(define (append-child child to)
(js-invoke to "appendChild" child))