Fossil SCM

chat: added drag/drop support for files. Images get previewed like those pasted from the clipboard.

stephan 2020-12-23 10:23 UTC chatroom-dev
Commit 4c0146f180ef15b10ddd5914d26c32240c353d4f5aa76bc054c70df8b34e599a
2 files changed +24 -4 +74 -17
+24 -4
--- src/chat.c
+++ src/chat.c
@@ -84,10 +84,14 @@
8484
@ #chat-input-file {
8585
@ display: flex;
8686
@ flex-direction: row;
8787
@ align-items: center;
8888
@ }
89
+ @ #chat-input-file > span,
90
+ @ #chat-input-file > input[type=file] {
91
+ @ align-self: flex-start;
92
+ @ }
8993
@ #chat-input-file > input {
9094
@ flex: 1 0 auto;
9195
@ }
9296
@ .chat-timestamp {
9397
@ font-family: monospace;
@@ -94,24 +98,40 @@
9498
@ font-size: 0.8em;
9599
@ white-space: pre;
96100
@ text-align: left;
97101
@ opacity: 0.8;
98102
@ }
103
+ @ #chat-drop-zone {
104
+ @ box-sizing: content-box;
105
+ @ background-color: #e0e0e0;
106
+ @ flex: 3 1 auto;
107
+ @ padding: 0.5em 1em;
108
+ @ border: 1px solid #808080;
109
+ @ border-radius: 0.25em;
110
+ @ }
111
+ @ #chat-drop-zone.dragover {
112
+ @ border: 1px dashed green;
113
+ @ }
114
+ @ #chat-drop-details {
115
+ @ white-space: pre;
116
+ @ font-family: monospace;
117
+ @ }
99118
@ </style>
100119
@ <form accept-encoding="utf-8" id="chat-form">
101120
@ <div id='chat-input-area'>
102121
@ <div id='chat-input-line'>
103
- @ <input type="text" name="msg" id="sbox"\
122
+ @ <input type="text" name="msg" id="sbox" \
104123
@ placeholder="Type message here.">
105124
@ <input type="submit" value="Send">
106125
@ </div>
107126
@ <div id='chat-input-file'>
108127
@ <span>File:</span>
109128
@ <input type="file" name="file">
110
- @ <div id='chat-pasted-image'>
111
- @ Or paste an image from the clipboard, if supported by your
112
- @ environment.<br><img>
129
+ @ <div id="chat-drop-zone">
130
+ @ Or drag/drop a file in this spot, or paste an image from
131
+ @ the clipboard if supported by your environment.
132
+ @ <div id="chat-drop-details"></div>
113133
@ </div>
114134
@ </div>
115135
@ </div>
116136
@ </form>
117137
@ <hr>
118138
--- src/chat.c
+++ src/chat.c
@@ -84,10 +84,14 @@
84 @ #chat-input-file {
85 @ display: flex;
86 @ flex-direction: row;
87 @ align-items: center;
88 @ }
 
 
 
 
89 @ #chat-input-file > input {
90 @ flex: 1 0 auto;
91 @ }
92 @ .chat-timestamp {
93 @ font-family: monospace;
@@ -94,24 +98,40 @@
94 @ font-size: 0.8em;
95 @ white-space: pre;
96 @ text-align: left;
97 @ opacity: 0.8;
98 @ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99 @ </style>
100 @ <form accept-encoding="utf-8" id="chat-form">
101 @ <div id='chat-input-area'>
102 @ <div id='chat-input-line'>
103 @ <input type="text" name="msg" id="sbox"\
104 @ placeholder="Type message here.">
105 @ <input type="submit" value="Send">
106 @ </div>
107 @ <div id='chat-input-file'>
108 @ <span>File:</span>
109 @ <input type="file" name="file">
110 @ <div id='chat-pasted-image'>
111 @ Or paste an image from the clipboard, if supported by your
112 @ environment.<br><img>
 
113 @ </div>
114 @ </div>
115 @ </div>
116 @ </form>
117 @ <hr>
118
--- src/chat.c
+++ src/chat.c
@@ -84,10 +84,14 @@
84 @ #chat-input-file {
85 @ display: flex;
86 @ flex-direction: row;
87 @ align-items: center;
88 @ }
89 @ #chat-input-file > span,
90 @ #chat-input-file > input[type=file] {
91 @ align-self: flex-start;
92 @ }
93 @ #chat-input-file > input {
94 @ flex: 1 0 auto;
95 @ }
96 @ .chat-timestamp {
97 @ font-family: monospace;
@@ -94,24 +98,40 @@
98 @ font-size: 0.8em;
99 @ white-space: pre;
100 @ text-align: left;
101 @ opacity: 0.8;
102 @ }
103 @ #chat-drop-zone {
104 @ box-sizing: content-box;
105 @ background-color: #e0e0e0;
106 @ flex: 3 1 auto;
107 @ padding: 0.5em 1em;
108 @ border: 1px solid #808080;
109 @ border-radius: 0.25em;
110 @ }
111 @ #chat-drop-zone.dragover {
112 @ border: 1px dashed green;
113 @ }
114 @ #chat-drop-details {
115 @ white-space: pre;
116 @ font-family: monospace;
117 @ }
118 @ </style>
119 @ <form accept-encoding="utf-8" id="chat-form">
120 @ <div id='chat-input-area'>
121 @ <div id='chat-input-line'>
122 @ <input type="text" name="msg" id="sbox" \
123 @ placeholder="Type message here.">
124 @ <input type="submit" value="Send">
125 @ </div>
126 @ <div id='chat-input-file'>
127 @ <span>File:</span>
128 @ <input type="file" name="file">
129 @ <div id="chat-drop-zone">
130 @ Or drag/drop a file in this spot, or paste an image from
131 @ the clipboard if supported by your environment.
132 @ <div id="chat-drop-details"></div>
133 @ </div>
134 @ </div>
135 @ </div>
136 @ </form>
137 @ <hr>
138
+74 -17
--- src/chat.js
+++ src/chat.js
@@ -1,29 +1,92 @@
11
(function(){
22
const form = document.querySelector('#chat-form');
33
let mxMsg = 0;
4
- const F = window.fossil;
4
+ const F = window.fossil, D = F.dom;
55
const _me = F.user.name;
6
- /* State for pasting an image from the clipboard */
7
- const ImagePasteState = {
8
- imgTag: document.querySelector('#chat-pasted-image img'),
6
+ /* State for paste and drag/drop */
7
+ const BlobXferState = {
8
+ dropZone: document.querySelector('#chat-drop-zone'),
9
+ dropDetails: document.querySelector('#chat-drop-details'),
10
+ imgTag: document.querySelector('#chat-drop-details img'),
911
blob: undefined
1012
};
13
+ /** Updates the paste/drop zone with details of the pasted/dropped
14
+ data. */
15
+ const updateDropZoneContent = function(blob){
16
+ const bx = BlobXferState, dd = bx.dropDetails;
17
+ bx.blob = blob;
18
+ D.clearElement(dd);
19
+ D.append(dd, D.br(), "Name: ", blob.name,
20
+ D.br(), "Size: ",blob.size);
21
+ if(blob.type && blob.type.startsWith("image/")){
22
+ const img = D.img();
23
+ D.append(dd, D.br(), img);
24
+ const reader = new FileReader();
25
+ reader.onload = (e)=>img.setAttribute('src', e.target.result);
26
+ reader.readAsDataURL(blob);
27
+ }
28
+ };
29
+ ////////////////////////////////////////////////////////////
30
+ // File drag/drop.
31
+ // Adapted from: https://stackoverflow.com/a/58677161
32
+ const dropHighlight = BlobXferState.dropZone /* target zone */;
33
+ const dropEvents = {
34
+ drop: function(ev){
35
+ ev.preventDefault();
36
+ D.removeClass(dropHighlight, 'dragover');
37
+ const file = ev.dataTransfer.files[0];
38
+ if(file) {
39
+ updateDropZoneContent(file);
40
+ }
41
+ },
42
+ dragenter: function(ev){
43
+ ev.preventDefault();
44
+ ev.dataTransfer.dropEffect = "copy";
45
+ D.addClass(dropHighlight, 'dragover');
46
+ },
47
+ dragover: function(ev){
48
+ ev.preventDefault();
49
+ },
50
+ dragend: function(ev){
51
+ ev.preventDefault();
52
+ },
53
+ dragleave: function(ev){
54
+ ev.preventDefault();
55
+ D.removeClass(dropHighlight, 'dragover');
56
+ }
57
+ };
58
+ /*
59
+ The idea here is to accept drops at multiple points or, ideally,
60
+ document.body, and apply them to P.e.taContent, but the precise
61
+ combination of event handling needed to pull this off is eluding
62
+ me.
63
+ */
64
+ [BlobXferState.dropZone
65
+ /* ideally we'd link only to document.body, but the events seem to
66
+ get out of whack, with dropleave being triggered at unexpected
67
+ points. */
68
+ ].forEach(function(e){
69
+ Object.keys(dropEvents).forEach(
70
+ (k)=>e.addEventListener(k, dropEvents[k], true)
71
+ );
72
+ });
73
+
1174
form.addEventListener('submit',(e)=>{
1275
e.preventDefault();
1376
const fd = new FormData(form);
14
- if(ImagePasteState.blob/*replace file content with this*/){
15
- fd.set("file", ImagePasteState.blob);
77
+ if(BlobXferState.blob/*replace file content with this*/){
78
+ fd.set("file", BlobXferState.blob);
1679
}
17
- if( form.msg.value.length>0 || form.file.value.length>0 || ImagePasteState.blob ){
80
+ if( form.msg.value.length>0 || form.file.value.length>0 || BlobXferState.blob ){
1881
fetch("chat-send",{
1982
method: 'POST',
2083
body: fd
2184
});
2285
}
23
- ImagePasteState.blob = undefined;
24
- ImagePasteState.imgTag.removeAttribute('src');
86
+ BlobXferState.blob = undefined;
87
+ D.clearElement(BlobXferState.dropDetails);
2588
form.msg.value = "";
2689
form.file.value = "";
2790
form.msg.focus();
2891
});
2992
/* Handle image paste from clipboard. TODO: figure out how we can
@@ -32,18 +95,12 @@
3295
document.onpaste = function(event){
3396
const items = event.clipboardData.items,
3497
item = items[0];
3598
if(!item || !item.type) return;
3699
//console.debug("pasted item =",item);
37
- if('file'===item.kind && item.type.startsWith("image/")){
38
- ImagePasteState.blob = items[0].getAsFile();
39
- //console.debug("pasted blob =",ImagePasteState.blob);
40
- const reader = new FileReader();
41
- reader.onload = function(event){
42
- ImagePasteState.imgTag.setAttribute('src', event.target.result);
43
- };
44
- reader.readAsDataURL(ImagePasteState.blob);
100
+ if('file'===item.kind){
101
+ updateDropZoneContent(items[0].getAsFile());
45102
}else if('string'===item.kind){
46103
item.getAsString((v)=>form.msg.value = v);
47104
}
48105
};
49106
/* Injects element e as a new row in the chat, at the top of the list */
50107
--- src/chat.js
+++ src/chat.js
@@ -1,29 +1,92 @@
1 (function(){
2 const form = document.querySelector('#chat-form');
3 let mxMsg = 0;
4 const F = window.fossil;
5 const _me = F.user.name;
6 /* State for pasting an image from the clipboard */
7 const ImagePasteState = {
8 imgTag: document.querySelector('#chat-pasted-image img'),
 
 
9 blob: undefined
10 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11 form.addEventListener('submit',(e)=>{
12 e.preventDefault();
13 const fd = new FormData(form);
14 if(ImagePasteState.blob/*replace file content with this*/){
15 fd.set("file", ImagePasteState.blob);
16 }
17 if( form.msg.value.length>0 || form.file.value.length>0 || ImagePasteState.blob ){
18 fetch("chat-send",{
19 method: 'POST',
20 body: fd
21 });
22 }
23 ImagePasteState.blob = undefined;
24 ImagePasteState.imgTag.removeAttribute('src');
25 form.msg.value = "";
26 form.file.value = "";
27 form.msg.focus();
28 });
29 /* Handle image paste from clipboard. TODO: figure out how we can
@@ -32,18 +95,12 @@
32 document.onpaste = function(event){
33 const items = event.clipboardData.items,
34 item = items[0];
35 if(!item || !item.type) return;
36 //console.debug("pasted item =",item);
37 if('file'===item.kind && item.type.startsWith("image/")){
38 ImagePasteState.blob = items[0].getAsFile();
39 //console.debug("pasted blob =",ImagePasteState.blob);
40 const reader = new FileReader();
41 reader.onload = function(event){
42 ImagePasteState.imgTag.setAttribute('src', event.target.result);
43 };
44 reader.readAsDataURL(ImagePasteState.blob);
45 }else if('string'===item.kind){
46 item.getAsString((v)=>form.msg.value = v);
47 }
48 };
49 /* Injects element e as a new row in the chat, at the top of the list */
50
--- src/chat.js
+++ src/chat.js
@@ -1,29 +1,92 @@
1 (function(){
2 const form = document.querySelector('#chat-form');
3 let mxMsg = 0;
4 const F = window.fossil, D = F.dom;
5 const _me = F.user.name;
6 /* State for paste and drag/drop */
7 const BlobXferState = {
8 dropZone: document.querySelector('#chat-drop-zone'),
9 dropDetails: document.querySelector('#chat-drop-details'),
10 imgTag: document.querySelector('#chat-drop-details img'),
11 blob: undefined
12 };
13 /** Updates the paste/drop zone with details of the pasted/dropped
14 data. */
15 const updateDropZoneContent = function(blob){
16 const bx = BlobXferState, dd = bx.dropDetails;
17 bx.blob = blob;
18 D.clearElement(dd);
19 D.append(dd, D.br(), "Name: ", blob.name,
20 D.br(), "Size: ",blob.size);
21 if(blob.type && blob.type.startsWith("image/")){
22 const img = D.img();
23 D.append(dd, D.br(), img);
24 const reader = new FileReader();
25 reader.onload = (e)=>img.setAttribute('src', e.target.result);
26 reader.readAsDataURL(blob);
27 }
28 };
29 ////////////////////////////////////////////////////////////
30 // File drag/drop.
31 // Adapted from: https://stackoverflow.com/a/58677161
32 const dropHighlight = BlobXferState.dropZone /* target zone */;
33 const dropEvents = {
34 drop: function(ev){
35 ev.preventDefault();
36 D.removeClass(dropHighlight, 'dragover');
37 const file = ev.dataTransfer.files[0];
38 if(file) {
39 updateDropZoneContent(file);
40 }
41 },
42 dragenter: function(ev){
43 ev.preventDefault();
44 ev.dataTransfer.dropEffect = "copy";
45 D.addClass(dropHighlight, 'dragover');
46 },
47 dragover: function(ev){
48 ev.preventDefault();
49 },
50 dragend: function(ev){
51 ev.preventDefault();
52 },
53 dragleave: function(ev){
54 ev.preventDefault();
55 D.removeClass(dropHighlight, 'dragover');
56 }
57 };
58 /*
59 The idea here is to accept drops at multiple points or, ideally,
60 document.body, and apply them to P.e.taContent, but the precise
61 combination of event handling needed to pull this off is eluding
62 me.
63 */
64 [BlobXferState.dropZone
65 /* ideally we'd link only to document.body, but the events seem to
66 get out of whack, with dropleave being triggered at unexpected
67 points. */
68 ].forEach(function(e){
69 Object.keys(dropEvents).forEach(
70 (k)=>e.addEventListener(k, dropEvents[k], true)
71 );
72 });
73
74 form.addEventListener('submit',(e)=>{
75 e.preventDefault();
76 const fd = new FormData(form);
77 if(BlobXferState.blob/*replace file content with this*/){
78 fd.set("file", BlobXferState.blob);
79 }
80 if( form.msg.value.length>0 || form.file.value.length>0 || BlobXferState.blob ){
81 fetch("chat-send",{
82 method: 'POST',
83 body: fd
84 });
85 }
86 BlobXferState.blob = undefined;
87 D.clearElement(BlobXferState.dropDetails);
88 form.msg.value = "";
89 form.file.value = "";
90 form.msg.focus();
91 });
92 /* Handle image paste from clipboard. TODO: figure out how we can
@@ -32,18 +95,12 @@
95 document.onpaste = function(event){
96 const items = event.clipboardData.items,
97 item = items[0];
98 if(!item || !item.type) return;
99 //console.debug("pasted item =",item);
100 if('file'===item.kind){
101 updateDropZoneContent(items[0].getAsFile());
 
 
 
 
 
 
102 }else if('string'===item.kind){
103 item.getAsString((v)=>form.msg.value = v);
104 }
105 };
106 /* Injects element e as a new row in the chat, at the top of the list */
107

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button