Fossil SCM
Implemented paste image into chat from clipboard. Fixed posted file download link but the files download with the same name as their message ID, which isn't very friendly. Not sure how to resolve that bit.
Commit
eb7845f33982b3f14836db15d149d84c40efe761f6e830a493a18fdcf306cd8c
Parent
0101325f9dcc55c…
2 files changed
+4
+28
-4
+4
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -105,10 +105,14 @@ | ||
| 105 | 105 | @ <input type="submit" value="Send"> |
| 106 | 106 | @ </div> |
| 107 | 107 | @ <div id='chat-input-file'> |
| 108 | 108 | @ <span>File:</span> |
| 109 | 109 | @ <input type="file" name="file"> |
| 110 | + @ <div id='chat-pasted-image'> | |
| 111 | + @ Or paste an image from clipboard, if supported by your | |
| 112 | + @ environment.<br><img> | |
| 113 | + @ </div> | |
| 110 | 114 | @ </div> |
| 111 | 115 | @ </div> |
| 112 | 116 | @ </form> |
| 113 | 117 | @ <hr> |
| 114 | 118 | |
| 115 | 119 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -105,10 +105,14 @@ | |
| 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> |
| 111 | @ </div> |
| 112 | @ </form> |
| 113 | @ <hr> |
| 114 | |
| 115 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -105,10 +105,14 @@ | |
| 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 clipboard, if supported by your |
| 112 | @ environment.<br><img> |
| 113 | @ </div> |
| 114 | @ </div> |
| 115 | @ </div> |
| 116 | @ </form> |
| 117 | @ <hr> |
| 118 | |
| 119 |
+28
-4
| --- src/chat.js | ||
| +++ src/chat.js | ||
| @@ -1,22 +1,45 @@ | ||
| 1 | 1 | (function(){ |
| 2 | 2 | const form = document.querySelector('#chat-form'); |
| 3 | 3 | let mxMsg = 0; |
| 4 | 4 | const F = window.fossil; |
| 5 | 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 | + }; | |
| 6 | 11 | form.addEventListener('submit',(e)=>{ |
| 7 | 12 | e.preventDefault(); |
| 8 | - if( form.msg.value.length>0 || form.file.value.length>0 ){ | |
| 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 ){ | |
| 9 | 18 | fetch("chat-send",{ |
| 10 | 19 | method: 'POST', |
| 11 | - body: new FormData(form) | |
| 20 | + body: fd | |
| 12 | 21 | }); |
| 13 | 22 | } |
| 23 | + ImagePasteState.blob = undefined; | |
| 24 | + ImagePasteState.imgTag.removeAttribute('src'); | |
| 14 | 25 | form.msg.value = ""; |
| 15 | 26 | form.file.value = ""; |
| 16 | 27 | form.msg.focus(); |
| 17 | 28 | }); |
| 29 | + /* Handle image paste from clipboard. TODO: confirm that we only | |
| 30 | + paste images here (silently ignore non-image data), or change the | |
| 31 | + related code to support non-image pasting/posting. */ | |
| 32 | + document.onpaste = function(event){ | |
| 33 | + const items = event.clipboardData.items; | |
| 34 | + ImagePasteState.blob = items[0].getAsFile(); | |
| 35 | + const reader = new FileReader(); | |
| 36 | + reader.onload = function(event){ | |
| 37 | + ImagePasteState.imgTag.setAttribute('src', event.target.result); | |
| 38 | + }; | |
| 39 | + reader.readAsDataURL(ImagePasteState.blob); | |
| 40 | + }; | |
| 18 | 41 | /* Injects element e as a new row in the chat, at the top of the list */ |
| 19 | 42 | const injectMessage = function f(e){ |
| 20 | 43 | if(!f.injectPoint){ |
| 21 | 44 | f.injectPoint = document.querySelector('#message-inject-point'); |
| 22 | 45 | } |
| @@ -24,10 +47,11 @@ | ||
| 24 | 47 | f.injectPoint.parentNode.insertBefore(e, f.injectPoint.nextSibling); |
| 25 | 48 | }else{ |
| 26 | 49 | f.injectPoint.parentNode.appendChild(e); |
| 27 | 50 | } |
| 28 | 51 | }; |
| 52 | + /* Returns a new TEXT node with the given text content. */ | |
| 29 | 53 | const textNode = (T)=>document.createTextNode(T); |
| 30 | 54 | /** Returns the local time string of Date object d, defaulting |
| 31 | 55 | to the current time. */ |
| 32 | 56 | const localTimeString = function ff(d){ |
| 33 | 57 | if(!ff.pad){ |
| @@ -126,12 +150,12 @@ | ||
| 126 | 150 | img.src = "chat-download/" + m.msgid; |
| 127 | 151 | span.appendChild(img); |
| 128 | 152 | }else{ |
| 129 | 153 | let a = document.createElement("a"); |
| 130 | 154 | let txt = "(" + m.fname + " " + m.fsize + " bytes)"; |
| 131 | - a.href = "%string($downloadurl)/" + m.msgid; | |
| 132 | - a.appendChild(document.createTextNode(txt)); | |
| 155 | + a.href = window.fossil.rootPath+'chat-download/' + m.msgid; | |
| 156 | + a.appendChild(textNode(txt)); | |
| 133 | 157 | span.appendChild(a); |
| 134 | 158 | } |
| 135 | 159 | let br = document.createElement("br"); |
| 136 | 160 | br.style.clear = "both"; |
| 137 | 161 | span.appendChild(br); |
| 138 | 162 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -1,22 +1,45 @@ | |
| 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 | form.addEventListener('submit',(e)=>{ |
| 7 | e.preventDefault(); |
| 8 | if( form.msg.value.length>0 || form.file.value.length>0 ){ |
| 9 | fetch("chat-send",{ |
| 10 | method: 'POST', |
| 11 | body: new FormData(form) |
| 12 | }); |
| 13 | } |
| 14 | form.msg.value = ""; |
| 15 | form.file.value = ""; |
| 16 | form.msg.focus(); |
| 17 | }); |
| 18 | /* Injects element e as a new row in the chat, at the top of the list */ |
| 19 | const injectMessage = function f(e){ |
| 20 | if(!f.injectPoint){ |
| 21 | f.injectPoint = document.querySelector('#message-inject-point'); |
| 22 | } |
| @@ -24,10 +47,11 @@ | |
| 24 | f.injectPoint.parentNode.insertBefore(e, f.injectPoint.nextSibling); |
| 25 | }else{ |
| 26 | f.injectPoint.parentNode.appendChild(e); |
| 27 | } |
| 28 | }; |
| 29 | const textNode = (T)=>document.createTextNode(T); |
| 30 | /** Returns the local time string of Date object d, defaulting |
| 31 | to the current time. */ |
| 32 | const localTimeString = function ff(d){ |
| 33 | if(!ff.pad){ |
| @@ -126,12 +150,12 @@ | |
| 126 | img.src = "chat-download/" + m.msgid; |
| 127 | span.appendChild(img); |
| 128 | }else{ |
| 129 | let a = document.createElement("a"); |
| 130 | let txt = "(" + m.fname + " " + m.fsize + " bytes)"; |
| 131 | a.href = "%string($downloadurl)/" + m.msgid; |
| 132 | a.appendChild(document.createTextNode(txt)); |
| 133 | span.appendChild(a); |
| 134 | } |
| 135 | let br = document.createElement("br"); |
| 136 | br.style.clear = "both"; |
| 137 | span.appendChild(br); |
| 138 |
| --- src/chat.js | |
| +++ src/chat.js | |
| @@ -1,22 +1,45 @@ | |
| 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: confirm that we only |
| 30 | paste images here (silently ignore non-image data), or change the |
| 31 | related code to support non-image pasting/posting. */ |
| 32 | document.onpaste = function(event){ |
| 33 | const items = event.clipboardData.items; |
| 34 | ImagePasteState.blob = items[0].getAsFile(); |
| 35 | const reader = new FileReader(); |
| 36 | reader.onload = function(event){ |
| 37 | ImagePasteState.imgTag.setAttribute('src', event.target.result); |
| 38 | }; |
| 39 | reader.readAsDataURL(ImagePasteState.blob); |
| 40 | }; |
| 41 | /* Injects element e as a new row in the chat, at the top of the list */ |
| 42 | const injectMessage = function f(e){ |
| 43 | if(!f.injectPoint){ |
| 44 | f.injectPoint = document.querySelector('#message-inject-point'); |
| 45 | } |
| @@ -24,10 +47,11 @@ | |
| 47 | f.injectPoint.parentNode.insertBefore(e, f.injectPoint.nextSibling); |
| 48 | }else{ |
| 49 | f.injectPoint.parentNode.appendChild(e); |
| 50 | } |
| 51 | }; |
| 52 | /* Returns a new TEXT node with the given text content. */ |
| 53 | const textNode = (T)=>document.createTextNode(T); |
| 54 | /** Returns the local time string of Date object d, defaulting |
| 55 | to the current time. */ |
| 56 | const localTimeString = function ff(d){ |
| 57 | if(!ff.pad){ |
| @@ -126,12 +150,12 @@ | |
| 150 | img.src = "chat-download/" + m.msgid; |
| 151 | span.appendChild(img); |
| 152 | }else{ |
| 153 | let a = document.createElement("a"); |
| 154 | let txt = "(" + m.fname + " " + m.fsize + " bytes)"; |
| 155 | a.href = window.fossil.rootPath+'chat-download/' + m.msgid; |
| 156 | a.appendChild(textNode(txt)); |
| 157 | span.appendChild(a); |
| 158 | } |
| 159 | let br = document.createElement("br"); |
| 160 | br.style.clear = "both"; |
| 161 | span.appendChild(br); |
| 162 |