Fossil SCM

Added UI to delete chat posts (tap on the message header). Made a change to the semantics of when fossil.PopupWidget's refresh() callback is triggered to account for the common case of having to show() the popup twice in a row without a hide() in between.

stephan 2020-12-24 05:58 trunk
Commit b7f106da8a6e31b05dd930172bdfe4b72e45b493521fd37e5a68aa88ecde5672
-7
--- src/chat.c
+++ src/chat.c
@@ -126,17 +126,10 @@
126126
@ padding: 0.25em;
127127
@ }
128128
@ #chat-input-file > input {
129129
@ flex: 1 0 auto;
130130
@ }
131
- @ .chat-timestamp {
132
- @ font-family: monospace;
133
- @ font-size: 0.8em;
134
- @ white-space: pre;
135
- @ text-align: left;
136
- @ opacity: 0.8;
137
- @ }
138131
@ #chat-input-file.dragover {
139132
@ border: 1px dashed green;
140133
@ }
141134
@ #chat-drop-details {
142135
@ flex: 0 1 auto;
143136
--- src/chat.c
+++ src/chat.c
@@ -126,17 +126,10 @@
126 @ padding: 0.25em;
127 @ }
128 @ #chat-input-file > input {
129 @ flex: 1 0 auto;
130 @ }
131 @ .chat-timestamp {
132 @ font-family: monospace;
133 @ font-size: 0.8em;
134 @ white-space: pre;
135 @ text-align: left;
136 @ opacity: 0.8;
137 @ }
138 @ #chat-input-file.dragover {
139 @ border: 1px dashed green;
140 @ }
141 @ #chat-drop-details {
142 @ flex: 0 1 auto;
143
--- src/chat.c
+++ src/chat.c
@@ -126,17 +126,10 @@
126 @ padding: 0.25em;
127 @ }
128 @ #chat-input-file > input {
129 @ flex: 1 0 auto;
130 @ }
 
 
 
 
 
 
 
131 @ #chat-input-file.dragover {
132 @ border: 1px dashed green;
133 @ }
134 @ #chat-drop-details {
135 @ flex: 0 1 auto;
136
+72 -22
--- src/chat.js
+++ src/chat.js
@@ -26,34 +26,65 @@
2626
};
2727
2828
cs.getMessageElemById = function(id){
2929
return qs('[data-msgid="'+id+'"]');
3030
};
31
- cs.deleteMessageElemById = function(id){
32
- const e = this.getMessageElemById(id);
33
- if(e) D.remove(e);
31
+ /**
32
+ LOCALLY deletes a message element by the message ID or passing
33
+ the .message-row element. Returns true if it removes an element,
34
+ else false.
35
+ */
36
+ cs.deleteMessageElem = function(id){
37
+ var e;
38
+ if(id instanceof HTMLElement){
39
+ e = id;
40
+ id = e.dataset.msgid;
41
+ }else{
42
+ e = this.getMessageElemById(id);
43
+ }
44
+ console.debug("e && id ===",e&&id, e, id);
45
+ if(e && id){
46
+ D.remove(e);
47
+ F.toast.message("Deleted message "+id+".");
48
+ }
3449
return !!e;
3550
};
51
+
52
+ /** Given a .message-row element, this function returns whethe the
53
+ current user may, at least hypothetically, delete the message
54
+ globally. A user may always delete a local copy of a
55
+ post. The server may trump this, e.g. if the login has been
56
+ cancelled after this page was loaded.
57
+ */
58
+ cs.userMayDelete = function(eMsg){
59
+ return this.me === eMsg.dataset.xfrom
60
+ || F.user.isAdmin/*will be confirmed server-side*/;
61
+ };
3662
3763
/**
3864
Removes the given message ID from the local chat record and, if
3965
the message was posted by this user OR this user in an
4066
admin/setup, also submits it for removal on the remote.
67
+
68
+ id may optionally be a DOM element, in which case it must be a
69
+ .message-row element.
4170
*/
42
- cs.deleteMessageById = function(id){
43
- const e = this.getMessageElemById(id);
44
- if(!e) return;
45
- if(this.me === e.dataset.xfrom
46
- || F.user.isAdmin/*will be confirmed server-side*/
47
- ){
71
+ cs.deleteMessage = function(id){
72
+ var e;
73
+ if(id instanceof HTMLElement){
74
+ e = id;
75
+ id = e.dataset.msgid;
76
+ }else{
77
+ e = this.getMessageElemById(id);
78
+ }
79
+ if(!(e instanceof HTMLElement)) return;
80
+ if(this.userMayDelete(e)){
4881
fetch("chat-delete?name=" + id)
49
- .then(()=>D.remove(e))
50
- .then(()=>F.toast.message("Deleted message "+id+"."))
82
+ .then(()=>this.deleteMessageElem(e))
5183
.catch(err=>this.reportError(err))
5284
}else{
53
- D.remove(e);
54
- F.toast.message("Locally removed message "+id+".");
85
+ this.deleteMessageElem(id);
5586
}
5687
};
5788
5889
return cs;
5990
})();
@@ -198,29 +229,48 @@
198229
timestamps. */
199230
const handleLegendClicked = function f(ev){
200231
if(!f.popup){
201232
/* Timestamp popup widget */
202233
f.popup = new F.PopupWidget({
203
- cssClass: ['fossil-tooltip', 'chat-timestamp'],
234
+ cssClass: ['fossil-tooltip', 'chat-message-popup'],
204235
refresh:function(){
205
- const D = F.dom;
236
+ const eMsg = this._eMsg;
237
+ if(!eMsg) return;
206238
D.clearElement(this.e);
207
- const d = new Date(this._timestamp+"Z");
239
+ const d = new Date(eMsg.dataset.timestamp+"Z");
208240
if(d.getMinutes().toString()!=="NaN"){
209241
// Date works, render informative timestamps
210
- D.append(this.e, localTimeString(d)," client-local", D.br(),
211
- iso8601ish(d));
242
+ D.append(this.e,
243
+ D.append(D.span(), localTimeString(d)," client-local"),
244
+ D.append(D.span(), iso8601ish(d)));
212245
}else{
213246
// Date doesn't work, so dumb it down...
214
- D.append(this.e, this._timestamp," GMT");
247
+ D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," GMT"));
215248
}
249
+ const toolbar = D.addClass(D.div(), 'toolbar');
250
+ const btnDelete = D.button("Delete "+
251
+ (Chat.userMayDelete(eMsg)
252
+ ? "globally" : "locally"));
253
+ const self = this;
254
+ btnDelete.addEventListener('click', function(){
255
+ self.hide();
256
+ Chat.deleteMessage(eMsg);
257
+ });
258
+ D.append(this.e, toolbar);
259
+ D.append(toolbar, btnDelete);
216260
}
217261
});
218262
f.popup.installClickToHide();
263
+ f.popup.hide = function(){
264
+ delete this._eMsg;
265
+ D.clearElement(this.e);
266
+ return this.show(false);
267
+ };
219268
}
220269
const rect = ev.target.getBoundingClientRect();
221
- f.popup._timestamp = ev.target.dataset.timestamp;
270
+ const eMsg = ev.target.parentNode/*the owning fieldset element*/;
271
+ f.popup._eMsg = eMsg;
222272
let x = rect.left, y = rect.top - 10;
223273
f.popup.show(ev.target)/*so we can get its computed size*/;
224274
if('right'===ev.target.getAttribute('align')){
225275
// Shift popup to the left for right-aligned messages to avoid
226276
// truncation off the right edge of the page.
@@ -235,19 +285,19 @@
235285
for(i=0; i<jx.msgs.length; ++i){
236286
const m = jx.msgs[i];
237287
if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
238288
if( m.mdel ){
239289
/* A record deletion notice. */
240
- Chat.deleteMessageElemById(m.mdel);
290
+ Chat.deleteMessageElem(m.mdel);
241291
continue;
242292
}
243293
const eWho = D.create('legend'),
244294
row = D.addClass(D.fieldset(eWho), 'message-row');
245295
row.dataset.msgid = m.msgid;
246296
row.dataset.xfrom = m.xfrom;
297
+ row.dataset.timestamp = m.mtime;
247298
injectMessage(row);
248
- eWho.dataset.timestamp = m.mtime;
249299
eWho.addEventListener('click', handleLegendClicked, false);
250300
if( m.xfrom==Chat.me && window.outerWidth<1000 ){
251301
eWho.setAttribute('align', 'right');
252302
row.style.justifyContent = "flex-end";
253303
}else{
254304
--- src/chat.js
+++ src/chat.js
@@ -26,34 +26,65 @@
26 };
27
28 cs.getMessageElemById = function(id){
29 return qs('[data-msgid="'+id+'"]');
30 };
31 cs.deleteMessageElemById = function(id){
32 const e = this.getMessageElemById(id);
33 if(e) D.remove(e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34 return !!e;
35 };
 
 
 
 
 
 
 
 
 
 
 
36
37 /**
38 Removes the given message ID from the local chat record and, if
39 the message was posted by this user OR this user in an
40 admin/setup, also submits it for removal on the remote.
 
 
 
41 */
42 cs.deleteMessageById = function(id){
43 const e = this.getMessageElemById(id);
44 if(!e) return;
45 if(this.me === e.dataset.xfrom
46 || F.user.isAdmin/*will be confirmed server-side*/
47 ){
 
 
 
 
48 fetch("chat-delete?name=" + id)
49 .then(()=>D.remove(e))
50 .then(()=>F.toast.message("Deleted message "+id+"."))
51 .catch(err=>this.reportError(err))
52 }else{
53 D.remove(e);
54 F.toast.message("Locally removed message "+id+".");
55 }
56 };
57
58 return cs;
59 })();
@@ -198,29 +229,48 @@
198 timestamps. */
199 const handleLegendClicked = function f(ev){
200 if(!f.popup){
201 /* Timestamp popup widget */
202 f.popup = new F.PopupWidget({
203 cssClass: ['fossil-tooltip', 'chat-timestamp'],
204 refresh:function(){
205 const D = F.dom;
 
206 D.clearElement(this.e);
207 const d = new Date(this._timestamp+"Z");
208 if(d.getMinutes().toString()!=="NaN"){
209 // Date works, render informative timestamps
210 D.append(this.e, localTimeString(d)," client-local", D.br(),
211 iso8601ish(d));
 
212 }else{
213 // Date doesn't work, so dumb it down...
214 D.append(this.e, this._timestamp," GMT");
215 }
 
 
 
 
 
 
 
 
 
 
 
216 }
217 });
218 f.popup.installClickToHide();
 
 
 
 
 
219 }
220 const rect = ev.target.getBoundingClientRect();
221 f.popup._timestamp = ev.target.dataset.timestamp;
 
222 let x = rect.left, y = rect.top - 10;
223 f.popup.show(ev.target)/*so we can get its computed size*/;
224 if('right'===ev.target.getAttribute('align')){
225 // Shift popup to the left for right-aligned messages to avoid
226 // truncation off the right edge of the page.
@@ -235,19 +285,19 @@
235 for(i=0; i<jx.msgs.length; ++i){
236 const m = jx.msgs[i];
237 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
238 if( m.mdel ){
239 /* A record deletion notice. */
240 Chat.deleteMessageElemById(m.mdel);
241 continue;
242 }
243 const eWho = D.create('legend'),
244 row = D.addClass(D.fieldset(eWho), 'message-row');
245 row.dataset.msgid = m.msgid;
246 row.dataset.xfrom = m.xfrom;
 
247 injectMessage(row);
248 eWho.dataset.timestamp = m.mtime;
249 eWho.addEventListener('click', handleLegendClicked, false);
250 if( m.xfrom==Chat.me && window.outerWidth<1000 ){
251 eWho.setAttribute('align', 'right');
252 row.style.justifyContent = "flex-end";
253 }else{
254
--- src/chat.js
+++ src/chat.js
@@ -26,34 +26,65 @@
26 };
27
28 cs.getMessageElemById = function(id){
29 return qs('[data-msgid="'+id+'"]');
30 };
31 /**
32 LOCALLY deletes a message element by the message ID or passing
33 the .message-row element. Returns true if it removes an element,
34 else false.
35 */
36 cs.deleteMessageElem = function(id){
37 var e;
38 if(id instanceof HTMLElement){
39 e = id;
40 id = e.dataset.msgid;
41 }else{
42 e = this.getMessageElemById(id);
43 }
44 console.debug("e && id ===",e&&id, e, id);
45 if(e && id){
46 D.remove(e);
47 F.toast.message("Deleted message "+id+".");
48 }
49 return !!e;
50 };
51
52 /** Given a .message-row element, this function returns whethe the
53 current user may, at least hypothetically, delete the message
54 globally. A user may always delete a local copy of a
55 post. The server may trump this, e.g. if the login has been
56 cancelled after this page was loaded.
57 */
58 cs.userMayDelete = function(eMsg){
59 return this.me === eMsg.dataset.xfrom
60 || F.user.isAdmin/*will be confirmed server-side*/;
61 };
62
63 /**
64 Removes the given message ID from the local chat record and, if
65 the message was posted by this user OR this user in an
66 admin/setup, also submits it for removal on the remote.
67
68 id may optionally be a DOM element, in which case it must be a
69 .message-row element.
70 */
71 cs.deleteMessage = function(id){
72 var e;
73 if(id instanceof HTMLElement){
74 e = id;
75 id = e.dataset.msgid;
76 }else{
77 e = this.getMessageElemById(id);
78 }
79 if(!(e instanceof HTMLElement)) return;
80 if(this.userMayDelete(e)){
81 fetch("chat-delete?name=" + id)
82 .then(()=>this.deleteMessageElem(e))
 
83 .catch(err=>this.reportError(err))
84 }else{
85 this.deleteMessageElem(id);
 
86 }
87 };
88
89 return cs;
90 })();
@@ -198,29 +229,48 @@
229 timestamps. */
230 const handleLegendClicked = function f(ev){
231 if(!f.popup){
232 /* Timestamp popup widget */
233 f.popup = new F.PopupWidget({
234 cssClass: ['fossil-tooltip', 'chat-message-popup'],
235 refresh:function(){
236 const eMsg = this._eMsg;
237 if(!eMsg) return;
238 D.clearElement(this.e);
239 const d = new Date(eMsg.dataset.timestamp+"Z");
240 if(d.getMinutes().toString()!=="NaN"){
241 // Date works, render informative timestamps
242 D.append(this.e,
243 D.append(D.span(), localTimeString(d)," client-local"),
244 D.append(D.span(), iso8601ish(d)));
245 }else{
246 // Date doesn't work, so dumb it down...
247 D.append(this.e, D.append(D.span(), eMsg.dataset.timestamp," GMT"));
248 }
249 const toolbar = D.addClass(D.div(), 'toolbar');
250 const btnDelete = D.button("Delete "+
251 (Chat.userMayDelete(eMsg)
252 ? "globally" : "locally"));
253 const self = this;
254 btnDelete.addEventListener('click', function(){
255 self.hide();
256 Chat.deleteMessage(eMsg);
257 });
258 D.append(this.e, toolbar);
259 D.append(toolbar, btnDelete);
260 }
261 });
262 f.popup.installClickToHide();
263 f.popup.hide = function(){
264 delete this._eMsg;
265 D.clearElement(this.e);
266 return this.show(false);
267 };
268 }
269 const rect = ev.target.getBoundingClientRect();
270 const eMsg = ev.target.parentNode/*the owning fieldset element*/;
271 f.popup._eMsg = eMsg;
272 let x = rect.left, y = rect.top - 10;
273 f.popup.show(ev.target)/*so we can get its computed size*/;
274 if('right'===ev.target.getAttribute('align')){
275 // Shift popup to the left for right-aligned messages to avoid
276 // truncation off the right edge of the page.
@@ -235,19 +285,19 @@
285 for(i=0; i<jx.msgs.length; ++i){
286 const m = jx.msgs[i];
287 if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
288 if( m.mdel ){
289 /* A record deletion notice. */
290 Chat.deleteMessageElem(m.mdel);
291 continue;
292 }
293 const eWho = D.create('legend'),
294 row = D.addClass(D.fieldset(eWho), 'message-row');
295 row.dataset.msgid = m.msgid;
296 row.dataset.xfrom = m.xfrom;
297 row.dataset.timestamp = m.mtime;
298 injectMessage(row);
 
299 eWho.addEventListener('click', handleLegendClicked, false);
300 if( m.xfrom==Chat.me && window.outerWidth<1000 ){
301 eWho.setAttribute('align', 'right');
302 row.style.justifyContent = "flex-end";
303 }else{
304
--- src/default.css
+++ src/default.css
@@ -1503,5 +1503,22 @@
15031503
}
15041504
15051505
body.chat .fossil-tooltip.help-buttonlet-content {
15061506
font-size: 80%;
15071507
}
1508
+
1509
+body.chat .chat-message-popup {
1510
+ font-family: monospace;
1511
+ font-size: 0.8em;
1512
+ text-align: left;
1513
+ opacity: 0.8;
1514
+ display: flex;
1515
+ flex-direction: column;
1516
+ align-items: stretch;
1517
+}
1518
+.chat-message-popup > span { white-space: nowrap; }
1519
+.chat-message-popup > .toolbar {
1520
+ padding: 0.2em;
1521
+ margin: 0;
1522
+ border: 2px inset rgba(0,0,0,0.3);
1523
+ border-radius: 0.25em;
1524
+}
15081525
--- src/default.css
+++ src/default.css
@@ -1503,5 +1503,22 @@
1503 }
1504
1505 body.chat .fossil-tooltip.help-buttonlet-content {
1506 font-size: 80%;
1507 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1508
--- src/default.css
+++ src/default.css
@@ -1503,5 +1503,22 @@
1503 }
1504
1505 body.chat .fossil-tooltip.help-buttonlet-content {
1506 font-size: 80%;
1507 }
1508
1509 body.chat .chat-message-popup {
1510 font-family: monospace;
1511 font-size: 0.8em;
1512 text-align: left;
1513 opacity: 0.8;
1514 display: flex;
1515 flex-direction: column;
1516 align-items: stretch;
1517 }
1518 .chat-message-popup > span { white-space: nowrap; }
1519 .chat-message-popup > .toolbar {
1520 padding: 0.2em;
1521 margin: 0;
1522 border: 2px inset rgba(0,0,0,0.3);
1523 border-radius: 0.25em;
1524 }
1525
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -12,15 +12,23 @@
1212
Creates a new tooltip-like widget using the given options object.
1313
1414
Options:
1515
1616
.refresh: callback which is called just before the tooltip is
17
- revealed or moved. It must refresh the contents of the tooltip,
18
- if needed, by applying the content to/within this.e, which is the
19
- base DOM element for the tooltip (and is a child of
20
- document.body). If the contents are static and set up via the
21
- .init option then this callback is not needed.
17
+ revealed. It must refresh the contents of the tooltip, if needed,
18
+ by applying the content to/within this.e, which is the base DOM
19
+ element for the tooltip (and is a child of document.body). If the
20
+ contents are static and set up via the .init option then this
21
+ callback is not needed. When moving an already-shown tooltip,
22
+ this is *not* called. It arguably should be, but the fact is that
23
+ we often have to show() a popup twice in a row without hiding it
24
+ between those calls: once to get its computed size and another to
25
+ move it by some amount relative to that size. If the state of the
26
+ popup depends on its position and a "double-show()" is needed
27
+ then the client must hide() the popup between the two calls to
28
+ show() in order to force a call to refresh() on the second
29
+ show().
2230
2331
.adjustX: an optional callback which is called when the tooltip
2432
is to be displayed at a given position and passed the X
2533
viewport-relative coordinate. This routine must either return its
2634
argument as-is or return an adjusted value. The intent is to
@@ -140,16 +148,21 @@
140148
For the latter two, this.options.adjustX() and adjustY() will
141149
be called to adjust it further.
142150
143151
Returns this object.
144152
153
+ If this call will reveal the element then it calls
154
+ this.refresh() to update the UI state. If the element was
155
+ already revealed, the call to refresh() is skipped.
156
+
145157
Sidebar: showing/hiding the widget is, as is conventional for
146158
this framework, done by removing/adding the 'hidden' CSS class
147159
to it, so that class must be defined appropriately.
148160
*/
149161
show: function(){
150
- var x = undefined, y = undefined, showIt;
162
+ var x = undefined, y = undefined, showIt,
163
+ wasShown = !this.e.classList.contains('hidden');
151164
if(2===arguments.length){
152165
x = arguments[0];
153166
y = arguments[1];
154167
showIt = true;
155168
}else if(1===arguments.length){
@@ -162,11 +175,11 @@
162175
}else{
163176
showIt = !!arguments[0];
164177
}
165178
}
166179
if(showIt){
167
- this.refresh();
180
+ if(!wasShown) this.refresh();
168181
x = this.options.adjustX.call(this,x);
169182
y = this.options.adjustY.call(this,y);
170183
x += window.pageXOffset;
171184
y += window.pageYOffset;
172185
}
@@ -182,24 +195,35 @@
182195
this.e.style.removeProperty('top');
183196
}
184197
return this;
185198
},
186199
200
+ /**
201
+ Equivalent to show(false), but may be overridden by instances,
202
+ so long as they also call this.show(false) to perform the
203
+ actual hiding. Overriding can be used to clean up any state so
204
+ that the next call to refresh() (before the popup is show()n
205
+ again) can recognize whether it needs to do something, noting
206
+ that it's legal, and sometimes necessary, to call show()
207
+ multiple times without needing/wanting to completely refresh
208
+ the popup between each call (e.g. when moving the popup after
209
+ it's been show()n).
210
+ */
187211
hide: function(){return this.show(false)},
188212
189213
/**
190214
A convenience method which adds click handlers to this popup's
191
- main element and document.body to hide the popup when either
192
- element is clicked or the ESC key is pressed. Only call this
193
- once per instance, if at all. Returns this;
215
+ main element and document.body to hide (via hide()) the popup
216
+ when either element is clicked or the ESC key is pressed. Only
217
+ call this once per instance, if at all. Returns this;
194218
*/
195219
installClickToHide: function f(){
196
- this.e.addEventListener('click', ()=>this.show(false), false);
197
- document.body.addEventListener('click', ()=>this.show(false), true);
220
+ this.e.addEventListener('click', ()=>this.hide(), false);
221
+ document.body.addEventListener('click', ()=>this.hide(), true);
198222
const self = this;
199223
document.body.addEventListener('keydown', function(ev){
200
- if(self.isShown() && 27===ev.which) self.show(false);
224
+ if(self.isShown() && 27===ev.which) self.hide();
201225
}, true);
202226
return this;
203227
}
204228
}/*F.PopupWidget.prototype*/;
205229
206230
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -12,15 +12,23 @@
12 Creates a new tooltip-like widget using the given options object.
13
14 Options:
15
16 .refresh: callback which is called just before the tooltip is
17 revealed or moved. It must refresh the contents of the tooltip,
18 if needed, by applying the content to/within this.e, which is the
19 base DOM element for the tooltip (and is a child of
20 document.body). If the contents are static and set up via the
21 .init option then this callback is not needed.
 
 
 
 
 
 
 
 
22
23 .adjustX: an optional callback which is called when the tooltip
24 is to be displayed at a given position and passed the X
25 viewport-relative coordinate. This routine must either return its
26 argument as-is or return an adjusted value. The intent is to
@@ -140,16 +148,21 @@
140 For the latter two, this.options.adjustX() and adjustY() will
141 be called to adjust it further.
142
143 Returns this object.
144
 
 
 
 
145 Sidebar: showing/hiding the widget is, as is conventional for
146 this framework, done by removing/adding the 'hidden' CSS class
147 to it, so that class must be defined appropriately.
148 */
149 show: function(){
150 var x = undefined, y = undefined, showIt;
 
151 if(2===arguments.length){
152 x = arguments[0];
153 y = arguments[1];
154 showIt = true;
155 }else if(1===arguments.length){
@@ -162,11 +175,11 @@
162 }else{
163 showIt = !!arguments[0];
164 }
165 }
166 if(showIt){
167 this.refresh();
168 x = this.options.adjustX.call(this,x);
169 y = this.options.adjustY.call(this,y);
170 x += window.pageXOffset;
171 y += window.pageYOffset;
172 }
@@ -182,24 +195,35 @@
182 this.e.style.removeProperty('top');
183 }
184 return this;
185 },
186
 
 
 
 
 
 
 
 
 
 
 
187 hide: function(){return this.show(false)},
188
189 /**
190 A convenience method which adds click handlers to this popup's
191 main element and document.body to hide the popup when either
192 element is clicked or the ESC key is pressed. Only call this
193 once per instance, if at all. Returns this;
194 */
195 installClickToHide: function f(){
196 this.e.addEventListener('click', ()=>this.show(false), false);
197 document.body.addEventListener('click', ()=>this.show(false), true);
198 const self = this;
199 document.body.addEventListener('keydown', function(ev){
200 if(self.isShown() && 27===ev.which) self.show(false);
201 }, true);
202 return this;
203 }
204 }/*F.PopupWidget.prototype*/;
205
206
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -12,15 +12,23 @@
12 Creates a new tooltip-like widget using the given options object.
13
14 Options:
15
16 .refresh: callback which is called just before the tooltip is
17 revealed. It must refresh the contents of the tooltip, if needed,
18 by applying the content to/within this.e, which is the base DOM
19 element for the tooltip (and is a child of document.body). If the
20 contents are static and set up via the .init option then this
21 callback is not needed. When moving an already-shown tooltip,
22 this is *not* called. It arguably should be, but the fact is that
23 we often have to show() a popup twice in a row without hiding it
24 between those calls: once to get its computed size and another to
25 move it by some amount relative to that size. If the state of the
26 popup depends on its position and a "double-show()" is needed
27 then the client must hide() the popup between the two calls to
28 show() in order to force a call to refresh() on the second
29 show().
30
31 .adjustX: an optional callback which is called when the tooltip
32 is to be displayed at a given position and passed the X
33 viewport-relative coordinate. This routine must either return its
34 argument as-is or return an adjusted value. The intent is to
@@ -140,16 +148,21 @@
148 For the latter two, this.options.adjustX() and adjustY() will
149 be called to adjust it further.
150
151 Returns this object.
152
153 If this call will reveal the element then it calls
154 this.refresh() to update the UI state. If the element was
155 already revealed, the call to refresh() is skipped.
156
157 Sidebar: showing/hiding the widget is, as is conventional for
158 this framework, done by removing/adding the 'hidden' CSS class
159 to it, so that class must be defined appropriately.
160 */
161 show: function(){
162 var x = undefined, y = undefined, showIt,
163 wasShown = !this.e.classList.contains('hidden');
164 if(2===arguments.length){
165 x = arguments[0];
166 y = arguments[1];
167 showIt = true;
168 }else if(1===arguments.length){
@@ -162,11 +175,11 @@
175 }else{
176 showIt = !!arguments[0];
177 }
178 }
179 if(showIt){
180 if(!wasShown) this.refresh();
181 x = this.options.adjustX.call(this,x);
182 y = this.options.adjustY.call(this,y);
183 x += window.pageXOffset;
184 y += window.pageYOffset;
185 }
@@ -182,24 +195,35 @@
195 this.e.style.removeProperty('top');
196 }
197 return this;
198 },
199
200 /**
201 Equivalent to show(false), but may be overridden by instances,
202 so long as they also call this.show(false) to perform the
203 actual hiding. Overriding can be used to clean up any state so
204 that the next call to refresh() (before the popup is show()n
205 again) can recognize whether it needs to do something, noting
206 that it's legal, and sometimes necessary, to call show()
207 multiple times without needing/wanting to completely refresh
208 the popup between each call (e.g. when moving the popup after
209 it's been show()n).
210 */
211 hide: function(){return this.show(false)},
212
213 /**
214 A convenience method which adds click handlers to this popup's
215 main element and document.body to hide (via hide()) the popup
216 when either element is clicked or the ESC key is pressed. Only
217 call this once per instance, if at all. Returns this;
218 */
219 installClickToHide: function f(){
220 this.e.addEventListener('click', ()=>this.hide(), false);
221 document.body.addEventListener('click', ()=>this.hide(), true);
222 const self = this;
223 document.body.addEventListener('keydown', function(ev){
224 if(self.isShown() && 27===ev.which) self.hide();
225 }, true);
226 return this;
227 }
228 }/*F.PopupWidget.prototype*/;
229
230

Keyboard Shortcuts

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