Fossil

Check-in [4c0146f180]
Login

Check-in [4c0146f180]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:chat: added drag/drop support for files. Images get previewed like those pasted from the clipboard.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | chatroom-dev
Files: files | file ages | folders
SHA3-256: 4c0146f180ef15b10ddd5914d26c32240c353d4f5aa76bc054c70df8b34e599a
User & Date: stephan 2020-12-23 10:23:29.382
Context
2020-12-23
10:28
Chat style tweaks. ... (check-in: 3e956a2354 user: stephan tags: chatroom-dev)
10:23
chat: added drag/drop support for files. Images get previewed like those pasted from the clipboard. ... (check-in: 4c0146f180 user: stephan tags: chatroom-dev)
08:43
Removed some dead code. Updated some docs. ... (check-in: cf789fa7e1 user: stephan tags: chatroom-dev)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.c.
82
83
84
85
86
87
88




89
90
91
92
93
94
95
96
97
98















99
100
101
102
103
104
105
106
107
108
109
110

111
112
113
114
115
116
117
118
119
  @   flex: 5 1 auto;
  @ }
  @ #chat-input-file {
  @   display: flex;
  @   flex-direction: row;
  @   align-items: center;
  @ }




  @ #chat-input-file > input {
  @   flex: 1 0 auto;
  @ }
  @ .chat-timestamp {
  @    font-family: monospace;
  @    font-size: 0.8em;
  @    white-space: pre;
  @    text-align: left;
  @    opacity: 0.8;
  @ }















  @ </style>
  @ <form accept-encoding="utf-8" id="chat-form">
  @ <div id='chat-input-area'>
  @   <div id='chat-input-line'>
  @     <input type="text" name="msg" id="sbox"\
  @      placeholder="Type message here.">
  @     <input type="submit" value="Send">
  @   </div>
  @   <div id='chat-input-file'>
  @     <span>File:</span>
  @     <input type="file" name="file">
  @     <div id='chat-pasted-image'>

  @        Or paste an image from the clipboard, if supported by your
  @        environment.<br><img>
  @      </div>
  @   </div>
  @ </div>
  @ </form>
  @ <hr>

  /* New chat messages get inserted immediately after this element */







>
>
>
>










>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|






|
>
|
|







82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
  @   flex: 5 1 auto;
  @ }
  @ #chat-input-file {
  @   display: flex;
  @   flex-direction: row;
  @   align-items: center;
  @ }
  @ #chat-input-file > span,
  @ #chat-input-file > input[type=file] {
  @   align-self: flex-start;
  @ }
  @ #chat-input-file > input {
  @   flex: 1 0 auto;
  @ }
  @ .chat-timestamp {
  @    font-family: monospace;
  @    font-size: 0.8em;
  @    white-space: pre;
  @    text-align: left;
  @    opacity: 0.8;
  @ }
  @ #chat-drop-zone {
  @   box-sizing: content-box;
  @   background-color: #e0e0e0;
  @   flex: 3 1 auto;
  @   padding: 0.5em 1em;
  @   border: 1px solid #808080;
  @   border-radius: 0.25em;
  @ }
  @ #chat-drop-zone.dragover {
  @   border: 1px dashed green;
  @ }
  @ #chat-drop-details {
  @   white-space: pre;
  @   font-family: monospace;
  @ }
  @ </style>
  @ <form accept-encoding="utf-8" id="chat-form">
  @ <div id='chat-input-area'>
  @   <div id='chat-input-line'>
  @     <input type="text" name="msg" id="sbox" \
  @      placeholder="Type message here.">
  @     <input type="submit" value="Send">
  @   </div>
  @   <div id='chat-input-file'>
  @     <span>File:</span>
  @     <input type="file" name="file">
  @     <div id="chat-drop-zone">
  @        Or drag/drop a file in this spot, or paste an image from
  @        the clipboard if supported by your environment.
  @        <div id="chat-drop-details"></div>
  @      </div>
  @   </div>
  @ </div>
  @ </form>
  @ <hr>

  /* New chat messages get inserted immediately after this element */
Changes to src/chat.js.
1
2
3
4
5
6
7


8
9
10





























































11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(function(){
  const form = document.querySelector('#chat-form');
  let mxMsg = 0;
  const F = window.fossil;
  const _me = F.user.name;
  /* State for pasting an image from the clipboard */
  const ImagePasteState = {


    imgTag: document.querySelector('#chat-pasted-image img'),
    blob: undefined
  };





























































  form.addEventListener('submit',(e)=>{
    e.preventDefault();
    const fd = new FormData(form);
    if(ImagePasteState.blob/*replace file content with this*/){
      fd.set("file", ImagePasteState.blob);
    }
    if( form.msg.value.length>0 || form.file.value.length>0 || ImagePasteState.blob ){
      fetch("chat-send",{
        method: 'POST',
        body: fd
      });
    }
    ImagePasteState.blob = undefined;
    ImagePasteState.imgTag.removeAttribute('src');
    form.msg.value = "";
    form.file.value = "";
    form.msg.focus();
  });
  /* Handle image paste from clipboard. TODO: figure out how we can
     paste non-image binary data as if it had been selected via the
     file selection element. */
  document.onpaste = function(event){
    const items = event.clipboardData.items,
          item = items[0];
    if(!item || !item.type) return;
    //console.debug("pasted item =",item);
    if('file'===item.kind && item.type.startsWith("image/")){
      ImagePasteState.blob = items[0].getAsFile();
      //console.debug("pasted blob =",ImagePasteState.blob);
      const reader = new FileReader();
      reader.onload = function(event){
        ImagePasteState.imgTag.setAttribute('src', event.target.result);
      };
      reader.readAsDataURL(ImagePasteState.blob);
    }else if('string'===item.kind){
      item.getAsString((v)=>form.msg.value = v);
    }
  };
  /* Injects element e as a new row in the chat, at the top of the list */
  const injectMessage = function f(e){
    if(!f.injectPoint){



|

|
|
>
>
|


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



|
|

|





|
|












|
|
<
<
<
<
<
<







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101






102
103
104
105
106
107
108
(function(){
  const form = document.querySelector('#chat-form');
  let mxMsg = 0;
  const F = window.fossil, D = F.dom;
  const _me = F.user.name;
  /* State for paste and drag/drop */
  const BlobXferState = {
    dropZone: document.querySelector('#chat-drop-zone'),
    dropDetails: document.querySelector('#chat-drop-details'),
    imgTag: document.querySelector('#chat-drop-details img'),
    blob: undefined
  };
  /** Updates the paste/drop zone with details of the pasted/dropped
      data. */
  const updateDropZoneContent = function(blob){
    const bx = BlobXferState, dd = bx.dropDetails;
    bx.blob = blob;
    D.clearElement(dd);
    D.append(dd, D.br(), "Name: ", blob.name,
             D.br(), "Size: ",blob.size);
    if(blob.type && blob.type.startsWith("image/")){
      const img = D.img();
      D.append(dd, D.br(), img);
      const reader = new FileReader();
      reader.onload = (e)=>img.setAttribute('src', e.target.result);
      reader.readAsDataURL(blob);
    }
  };
  ////////////////////////////////////////////////////////////
  // File drag/drop.
  // Adapted from: https://stackoverflow.com/a/58677161
  const dropHighlight = BlobXferState.dropZone /* target zone */;
  const dropEvents = {
    drop: function(ev){
      ev.preventDefault();
      D.removeClass(dropHighlight, 'dragover');
      const file = ev.dataTransfer.files[0];
      if(file) {
        updateDropZoneContent(file);
      }
    },
    dragenter: function(ev){
      ev.preventDefault();
      ev.dataTransfer.dropEffect = "copy";
      D.addClass(dropHighlight, 'dragover');
    },
    dragover: function(ev){
      ev.preventDefault();
    },
    dragend: function(ev){
      ev.preventDefault();
    },
    dragleave: function(ev){
      ev.preventDefault();
      D.removeClass(dropHighlight, 'dragover');
    }
  };
  /*
    The idea here is to accept drops at multiple points or, ideally,
    document.body, and apply them to P.e.taContent, but the precise
    combination of event handling needed to pull this off is eluding
    me.
  */
  [BlobXferState.dropZone
   /* ideally we'd link only to document.body, but the events seem to
      get out of whack, with dropleave being triggered at unexpected
      points. */
  ].forEach(function(e){
    Object.keys(dropEvents).forEach(
      (k)=>e.addEventListener(k, dropEvents[k], true)
    );
  });

  form.addEventListener('submit',(e)=>{
    e.preventDefault();
    const fd = new FormData(form);
    if(BlobXferState.blob/*replace file content with this*/){
      fd.set("file", BlobXferState.blob);
    }
    if( form.msg.value.length>0 || form.file.value.length>0 || BlobXferState.blob ){
      fetch("chat-send",{
        method: 'POST',
        body: fd
      });
    }
    BlobXferState.blob = undefined;
    D.clearElement(BlobXferState.dropDetails);
    form.msg.value = "";
    form.file.value = "";
    form.msg.focus();
  });
  /* Handle image paste from clipboard. TODO: figure out how we can
     paste non-image binary data as if it had been selected via the
     file selection element. */
  document.onpaste = function(event){
    const items = event.clipboardData.items,
          item = items[0];
    if(!item || !item.type) return;
    //console.debug("pasted item =",item);
    if('file'===item.kind){
      updateDropZoneContent(items[0].getAsFile());






    }else if('string'===item.kind){
      item.getAsString((v)=>form.msg.value = v);
    }
  };
  /* Injects element e as a new row in the chat, at the top of the list */
  const injectMessage = function f(e){
    if(!f.injectPoint){