Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Initial impl of buttons to load older chat messages. The UI code is a bit more involved than might seem necessary, but is so largely because it needs to avoid UI/ajax race conditions. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
6d676f6eb5d78f4dd1db55caf589d8ea |
| User & Date: | stephan 2020-12-24 20:18:49.278 |
Context
|
2020-12-24
| ||
| 22:07 | chat message deletion: admins now have both delete local and delete global options, in case they want to remove something from local view without deleting it for all users. ... (check-in: b12d69d9f4 user: stephan tags: trunk) | |
| 20:18 | Initial impl of buttons to load older chat messages. The UI code is a bit more involved than might seem necessary, but is so largely because it needs to avoid UI/ajax race conditions. ... (check-in: 6d676f6eb5 user: stephan tags: trunk) | |
| 19:28 | A valid /chat-ping request should set the Access-Control-Allow-Origin in the reply header, to avoid client-side errors. ... (check-in: ffb40fd894 user: drh tags: trunk) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
** If N is negative, then the return value is the N most recent messages.
** Hence a request like /chat-poll/-100 can be used to initialize a new
** chat session to just the most recent messages.
**
** Some webservers (althttpd) do not allow a term of the URL path to
** begin with "-". Then /chat-poll/-100 cannot be used. Instead you
** have to say "/chat-poll?name=-100".
**
** The reply from this webpage is JSON that describes the new content.
** Format of the json:
**
** | {
** | "msg":[
** | {
| > > > > > > > > > > | 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 |
** If N is negative, then the return value is the N most recent messages.
** Hence a request like /chat-poll/-100 can be used to initialize a new
** chat session to just the most recent messages.
**
** Some webservers (althttpd) do not allow a term of the URL path to
** begin with "-". Then /chat-poll/-100 cannot be used. Instead you
** have to say "/chat-poll?name=-100".
**
** If the integer parameter "before" is passed in, it is assumed that
** the client is requesting older messages, up to (but not including)
** that message ID, in which case the next-oldest "n" messages
** (default=chat-initial-history setting, equivalent to n=0) are
** returned (negative n fetches all older entries). The client then
** needs to take care to inject them at the end of the history rather
** than the same place new messages go.
**
** If "before" is provided, "name" is ignored.
**
** The reply from this webpage is JSON that describes the new content.
** Format of the json:
**
** | {
** | "msg":[
** | {
|
| ︙ | ︙ | |||
421 422 423 424 425 426 427 428 429 430 431 432 |
**
** The "fname" and "fmime" fields are only present if "fsize" is greater
** than zero. The "xmsg" field may be an empty string if "fsize" is zero.
**
** The "msgid" values will be in increasing order.
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
*/
void chat_poll_webpage(void){
Blob json; /* The json to be constructed and returned */
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
int iDelay = 1000; /* Delay until next poll (milliseconds) */
| > > > < > > > > > > > > | > > > > > > > > > > > | | | | | | < < < | | | | > > > | | | < > | 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 |
**
** The "fname" and "fmime" fields are only present if "fsize" is greater
** than zero. The "xmsg" field may be an empty string if "fsize" is zero.
**
** The "msgid" values will be in increasing order.
**
** The "mdel" will only exist if "xmsg" is an empty string and "fsize" is zero.
**
** The messages are ordered oldest first unless "before" is provided, in which
** case they are sorted newest first (to facilitate the client-side UI update).
*/
void chat_poll_webpage(void){
Blob json; /* The json to be constructed and returned */
sqlite3_int64 dataVersion; /* Data version. Used for polling. */
int iDelay = 1000; /* Delay until next poll (milliseconds) */
int msgid = atoi(PD("name","0"));
const int msgBefore = atoi(PD("before","0"));
int nLimit = msgBefore>0 ? atoi(PD("n","0")) : 0;
Blob sql = empty_blob;
Stmt q1;
login_check_credentials();
if( !g.perm.Chat ) return;
chat_create_tables();
cgi_set_content_type("text/json");
dataVersion = db_int64(0, "PRAGMA data_version");
blob_append_sql(&sql,
"SELECT msgid, datetime(mtime), xfrom, xmsg, length(file),"
" fname, fmime, %s"
" FROM chat ",
msgBefore>0 ? "0 as mdel" : "mdel");
if( msgid<=0 || msgBefore>0 ){
db_begin_write();
chat_purge();
db_commit_transaction();
}
if(msgBefore>0){
if(0==nLimit){
nLimit = db_get_int("chat-initial-history",50);
}
blob_append_sql(&sql,
" WHERE msgid<%d"
" ORDER BY msgid DESC "
"LIMIT %d",
msgBefore, nLimit>0 ? nLimit : -1
);
}else{
if( msgid<0 ){
msgid = db_int(0,
"SELECT msgid FROM chat WHERE mdel IS NOT true"
" ORDER BY msgid DESC LIMIT 1 OFFSET %d", -msgid);
}
blob_append_sql(&sql,
" WHERE msgid>%d"
" ORDER BY msgid",
msgid
);
}
db_prepare(&q1, "%s", blob_sql_text(&sql));
blob_reset(&sql);
blob_init(&json, "{\"msgs\":[\n", -1);
while(1){
int cnt = 0;
while( db_step(&q1)==SQLITE_ROW ){
int id = db_column_int(&q1, 0);
const char *zDate = db_column_text(&q1, 1);
const char *zFrom = db_column_text(&q1, 2);
const char *zRawMsg = db_column_text(&q1, 3);
int nByte = db_column_int(&q1, 4);
const char *zFName = db_column_text(&q1, 5);
const char *zFMime = db_column_text(&q1, 6);
int iToDel = db_column_int(&q1, 7);
char *zMsg;
if(cnt++){
blob_append(&json, ",\n", 2);
}
blob_appendf(&json, "{\"msgid\":%d,\"mtime\":%!j,", id, zDate);
blob_appendf(&json, "\"xfrom\":%!j,", zFrom);
blob_appendf(&json, "\"uclr\":%!j,", hash_color(zFrom));
zMsg = chat_format_to_html(zRawMsg ? zRawMsg : "");
blob_appendf(&json, "\"xmsg\":%!j,", zMsg);
fossil_free(zMsg);
|
| ︙ | ︙ | |||
489 490 491 492 493 494 495 |
if( iToDel ){
blob_appendf(&json, ",\"mdel\":%d}", iToDel);
}else{
blob_append(&json, "}", 1);
}
}
db_reset(&q1);
| | | 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 |
if( iToDel ){
blob_appendf(&json, ",\"mdel\":%d}", iToDel);
}else{
blob_append(&json, "}", 1);
}
}
db_reset(&q1);
if( cnt || msgBefore>0 ){
blob_append(&json, "\n]}", 3);
cgi_set_content(&json);
break;
}
sqlite3_sleep(iDelay);
while( 1 ){
sqlite3_int64 newDataVers = db_int64(0,"PRAGMA repository.data_version");
|
| ︙ | ︙ |
Changes to src/chat.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/**
This file contains the client-side implementation of fossil's /chat
application.
*/
(function(){
const form = document.querySelector('#chat-form');
const F = window.fossil, D = F.dom;
const Chat = (function(){
const cs = {
me: F.user.name,
mxMsg: F.config.chatInitSize ? -F.config.chatInitSize : -50,
pageIsActive: 'visible'===document.visibilityState,
changesSincePageHidden: 0,
notificationBubbleColor: 'white',
| > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 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 |
/**
This file contains the client-side implementation of fossil's /chat
application.
*/
(function(){
const form = document.querySelector('#chat-form');
const F = window.fossil, D = F.dom;
const Chat = (function(){
const cs = {
e:{/*map of certain DOM elements.*/
messageInjectPoint: document.querySelector('#message-inject-point'),
pageTitle: document.querySelector('head title'),
loadToolbar: undefined /* the load-posts toolbar (dynamically created) */
},
me: F.user.name,
mxMsg: F.config.chatInitSize ? -F.config.chatInitSize : -50,
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
pageIsActive: 'visible'===document.visibilityState,
changesSincePageHidden: 0,
notificationBubbleColor: 'white',
totalMessageCount: 0, // total # of inbound messages
//! Number of messages to load for the history buttons
loadMessageCount: Math.abs(F.config.chatInitSize || 20),
ajaxInflight: 0,
/** Enables (if yes is truthy) or disables all elements in
* this.disableDuringAjax. */
enableAjaxComponents: function(yes){
D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
return this;
},
/* Must be called before any API is used which starts ajax traffic.
If this call represents the currently only in-flight ajax request,
all DOM elements in this.disableDuringAjax are disabled.
We cannot do this via a central API because (1) window.fetch()'s
Promise-based API seemingly makes that impossible and (2) the polling
technique holds ajax requests open for as long as possible. A call
to this method obligates the caller to also call ajaxEnd().
This must NOT be called for the chat-polling API except, as a
special exception, the very first one which fetches the
initial message list.
*/
ajaxStart: function(){
if(1===++this.ajaxInflight){
this.enableAjaxComponents(false);
}
},
/* Must be called after any ajax-related call for which
ajaxStart() was called, regardless of success or failure. If
it was the last such call (as measured by calls to
ajaxStart() and ajaxEnd()), elements disabled by a prior call
to ajaxStart() will be re-enabled. */
ajaxEnd: function(){
if(0===--this.ajaxInflight){
this.enableAjaxComponents(true);
}
},
disableDuringAjax: [
/* List of DOM elements disable while ajax traffic is in
transit. Must be populated before ajax starts. We do this
to avoid various race conditions in the UI and long-running
network requests. */
],
/* Injects element e as a new row in the chat, at the top of the
list if atEnd is falsy, else at the end of the list, before
the load-history widget. */
injectMessageElem: function f(e, atEnd){
const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
if(atEnd){
mip.parentNode.insertBefore(e, mip);
}else{
if(mip.nextSibling){
mip.parentNode.insertBefore(e, mip.nextSibling);
}else{
mip.parentNode.appendChild(e);
}
}
}
};
cs.pageTitleOrig = cs.e.pageTitle.innerText;
const qs = (e)=>document.querySelector(e);
const argsToArray = function(args){
return Array.prototype.slice.call(args,0);
};
cs.reportError = function(/*msg args*/){
const args = argsToArray(arguments);
console.error("chat error:",args);
|
| ︙ | ︙ | |||
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
e = id;
id = e.dataset.msgid;
}else{
e = this.getMessageElemById(id);
}
if(!(e instanceof HTMLElement)) return;
if(this.userMayDelete(e)){
fetch("chat-delete?name=" + id)
.then(()=>this.deleteMessageElem(e))
.catch(err=>this.reportError(err))
}else{
this.deleteMessageElem(id);
}
};
document.addEventListener('visibilitychange', function(ev){
cs.pageIsActive = !document.hidden;
if(cs.pageIsActive){
| > > | | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
e = id;
id = e.dataset.msgid;
}else{
e = this.getMessageElemById(id);
}
if(!(e instanceof HTMLElement)) return;
if(this.userMayDelete(e)){
this.ajaxStart();
fetch("chat-delete?name=" + id)
.then(()=>this.deleteMessageElem(e))
.catch(err=>this.reportError(err))
.finally(()=>this.ajaxEnd());
}else{
this.deleteMessageElem(id);
}
};
document.addEventListener('visibilitychange', function(ev){
cs.pageIsActive = !document.hidden;
if(cs.pageIsActive){
cs.e.pageTitle.innerText = cs.pageTitleOrig;
}
}, true);
return cs;
})()/*Chat initialization*/;
/* State for paste and drag/drop */
const BlobXferState = {
dropDetails: document.querySelector('#chat-drop-details'),
|
| ︙ | ︙ | |||
193 194 195 196 197 198 199 |
D.removeClass(dropHighlight, 'dragover');
}
};
Object.keys(dropEvents).forEach(
(k)=>form.file.addEventListener(k, dropEvents[k], true)
);
| < < < < < < < < < < < | 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
D.removeClass(dropHighlight, 'dragover');
}
};
Object.keys(dropEvents).forEach(
(k)=>form.file.addEventListener(k, dropEvents[k], true)
);
/* Returns a new TEXT node with the given text content. */
/** Returns the local time string of Date object d, defaulting
to the current time. */
const localTimeString = function ff(d){
if(!ff.pad){
ff.pad = (x)=>(''+x).length>1 ? x : '0'+x;
}
|
| ︙ | ︙ | |||
264 265 266 267 268 269 270 |
});
f.popup.installClickToHide();
f.popup.hide = function(){
delete this._eMsg;
D.clearElement(this.e);
return this.show(false);
};
| | | | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > | | | | > > | > > | | | 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 |
});
f.popup.installClickToHide();
f.popup.hide = function(){
delete this._eMsg;
D.clearElement(this.e);
return this.show(false);
};
}/*end static init*/
const rect = ev.target.getBoundingClientRect();
const eMsg = ev.target.parentNode/*the owning fieldset element*/;
f.popup._eMsg = eMsg;
let x = rect.left, y = rect.top - 10;
f.popup.show(ev.target)/*so we can get its computed size*/;
if('right'===ev.target.getAttribute('align')){
// Shift popup to the left for right-aligned messages to avoid
// truncation off the right edge of the page.
const pRect = f.popup.e.getBoundingClientRect();
x -= pRect.width/3*2;
}
f.popup.show(x, y);
}/*handleLegendClicked()*/;
/** Callback for poll() to inject new content into the page. jx ==
the response from /chat-poll. If atEnd is true, the message is
appended to the end of the chat list, else the beginning (the
default). */
const newcontent = function f(jx,atEnd){
if(!f.processPost){
/** Processes chat message m, placing it either the start (if atEnd
is falsy) or end (if atEnd is truthy) of the chat history. atEnd
should only be true when loading older messages. */
f.processPost = function(m,atEnd){
++Chat.totalMessageCount;
if( m.msgid>Chat.mxMsg ) Chat.mxMsg = m.msgid;
if( !Chat.mnMsg || m.msgid<Chat.mnMsg) Chat.mnMsg = m.msgid;
if( m.mdel ){
/* A record deletion notice. */
Chat.deleteMessageElem(m.mdel);
return;
}
const eWho = D.create('legend'),
row = D.addClass(D.fieldset(eWho), 'message-row');
row.dataset.msgid = m.msgid;
row.dataset.xfrom = m.xfrom;
row.dataset.timestamp = m.mtime;
Chat.injectMessageElem(row,atEnd);
eWho.addEventListener('click', handleLegendClicked, false);
if( m.xfrom==Chat.me && window.outerWidth<1000 ){
eWho.setAttribute('align', 'right');
row.style.justifyContent = "flex-end";
}else{
eWho.setAttribute('align', 'left');
}
eWho.style.backgroundColor = m.uclr;
eWho.classList.add('message-user');
let whoName = m.xfrom;
var d = new Date(m.mtime + "Z");
if( d.getMinutes().toString()!="NaN" ){
/* Show local time when we can compute it */
eWho.append(D.text(whoName+' @ '+
d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3)
))
}else{
/* Show UTC on systems where Date() does not work */
eWho.append(D.text(whoName+' @ '+m.mtime.slice(11,16)))
}
let eContent = D.addClass(D.div(),'message-content','chat-message');
eContent.style.backgroundColor = m.uclr;
row.appendChild(eContent);
if( m.fsize>0 ){
if( m.fmime && m.fmime.startsWith("image/") ){
eContent.appendChild(D.img("chat-download/" + m.msgid));
}else{
eContent.appendChild(D.a(
window.fossil.rootPath+
'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
// ^^^ add m.fname to URL to cause downloaded file to have that name.
"(" + m.fname + " " + m.fsize + " bytes)"
));
}
const br = D.br();
br.style.clear = "both";
eContent.appendChild(br);
}
if(m.xmsg){
// The m.xmsg text comes from the same server as this script and
// is guaranteed by that server to be "safe" HTML - safe in the
// sense that it is not possible for a malefactor to inject HTML
// or javascript or CSS. The m.xmsg content might contain
// hyperlinks, but otherwise it will be markup-free. See the
// chat_format_to_html() routine in the server for details.
//
// Hence, even though innerHTML is normally frowned upon, it is
// perfectly safe to use in this context.
eContent.innerHTML += m.xmsg
}
}/*processPost()*/;
}/*end static init*/
jx.msgs.forEach((m)=>f.processPost(m,atEnd));
if('visible'===document.visibilityState){
if(Chat.changesSincePageHidden){
Chat.changesSincePageHidden = 0;
Chat.e.pageTitle.innerText = Chat.pageTitleOrig;
}
}else{
Chat.changesSincePageHidden += jx.msgs.length;
Chat.e.pageTitle.innerText = '('+Chat.changesSincePageHidden+') '+
Chat.pageTitleOrig;
}
if(jx.msgs.length && F.config.pingTcp){
fetch("http:/"+"/localhost:"+window.fossil.config.pingTcp+"/chat-ping");
}
}/*newcontent()*/;
if(true){
/** Add toolbar for loading older messages. We use a FIELDSET here
because a fieldset is the only parent element type which can
automatically enable/disable its children by
enabling/disabling the parent element. */
const loadLegend = D.legend("Load...");
const toolbar = Chat.e.loadToolbar = D.addClass(
D.fieldset(loadLegend), "load-msg-toolbar"
);
Chat.disableDuringAjax.push(toolbar);
/* Loads the next n oldest messages, or all previous history if n is negative. */
const loadOldMessages = function(n){
Chat.ajaxStart();
var gotMessages = false;
fetch("chat-poll?before="+Chat.mnMsg+"&n="+n)
.then(x=>x.json())
.then(function(x){
gotMessages = x.msgs.length;
newcontent(x,true);
})
.catch(e=>Chat.reportError(e))
.finally(function(){
if(n<0/*we asked for all history*/
|| 0===gotMessages/*we found no history*/
|| (n>0 && gotMessages<n /*we got fewer history entries than requested*/)
|| (false!==gotMessages && n<0 && gotMessages<Chat.loadMessageCount
/*we asked for default amount and got fewer than that.*/)){
/* We've loaded all history. Permanently disable the
history-load toolbar and keep it from being re-enabled
via the ajaxStart()/ajaxEnd() mechanism... */
const div = Chat.e.loadToolbar.querySelector('div');
D.append(D.clearElement(div), "All history has been loaded.");
D.addClass(Chat.e.loadToolbar, 'all-done');
const ndx = Chat.disableDuringAjax.indexOf(Chat.e.loadToolbar);
if(ndx>=0) Chat.disableDuringAjax.splice(ndx,1);
Chat.e.loadToolbar.disabled = true;
}
if(gotMessages > 0){
F.toast.message("Loaded "+gotMessages+" older messages.");
}
Chat.ajaxEnd();
});
};
const wrapper = D.div(); /* browsers don't all properly handle >1 child in a fieldset */;
D.append(toolbar, wrapper);
var btn = D.button("Previous "+Chat.loadMessageCount+" messages");
D.append(wrapper, btn);
btn.addEventListener('click',()=>loadOldMessages(Chat.loadMessageCount));
btn = D.button("All previous messages");
D.append(wrapper, btn);
btn.addEventListener('click',()=>loadOldMessages(-1));
D.append(document.querySelector('body.chat > div.content'), toolbar);
toolbar.disabled = true /*will be enabled when msg load finishes */;
}/*end history loading widget setup*/
async function poll(isFirstCall){
if(poll.running) return;
poll.running = true;
if(isFirstCall) Chat.ajaxStart();
var p = fetch("chat-poll?name=" + Chat.mxMsg);
p.then(x=>x.json())
.then(y=>newcontent(y))
.catch(e=>Chat.reportError(e))
.finally(function(x){
if(isFirstCall) Chat.ajaxEnd();
poll.running=false;
});
}
poll.running = false;
poll(true);
setInterval(poll, 1000);
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
})();
|
Changes to src/default.css.
| ︙ | ︙ | |||
1511 1512 1513 1514 1515 1516 1517 | font-size: 0.8em; text-align: left; opacity: 0.8; display: flex; flex-direction: column; align-items: stretch; } | | | > > > > > > > > > > > > > > > > > | 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 |
font-size: 0.8em;
text-align: left;
opacity: 0.8;
display: flex;
flex-direction: column;
align-items: stretch;
}
body.chat .chat-message-popup > span { white-space: nowrap; }
body.chat .chat-message-popup > .toolbar {
padding: 0.2em;
margin: 0;
border: 2px inset rgba(0,0,0,0.3);
border-radius: 0.25em;
}
body.chat .load-msg-toolbar {
border-radius: 0.25em;
padding: 0.1em 0.2em;
}
body.chat .load-msg-toolbar.all-done {
opacity: 0.5;
}
body.chat .load-msg-toolbar > div {
display: flex;
flex-direction: row;
justify-content: stretch;
flex-wrap: wrap;
}
body.chat .load-msg-toolbar > div > button {
flex: 1 1 auto;
}
|
Changes to src/fossil.dom.js.
| ︙ | ︙ | |||
244 245 246 247 248 249 250 |
dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
dom.tr = dom.createElemFactoryWithOptionalParent('tr');
dom.td = dom.createElemFactoryWithOptionalParent('td');
dom.th = dom.createElemFactoryWithOptionalParent('th');
/**
Creates and returns a FIELDSET element, optionaly with a LEGEND
| | | > | | > > > > > > > > > > | 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
dom.tr = dom.createElemFactoryWithOptionalParent('tr');
dom.td = dom.createElemFactoryWithOptionalParent('td');
dom.th = dom.createElemFactoryWithOptionalParent('th');
/**
Creates and returns a FIELDSET element, optionaly with a LEGEND
element added to it. If legendText is an HTMLElement then is is
assumed to be a LEGEND and is appended as-is, else it is assumed
(if truthy) to be a value suitable for passing to
dom.append(aLegendElement,...).
*/
dom.fieldset = function(legendText){
const fs = this.create('fieldset');
if(legendText){
this.append(
fs,
(legendText instanceof HTMLElement)
? legendText
: this.append(this.legend(legendText))
);
}
return fs;
};
/**
Returns a new LEGEND legend element. The given argument, if
not falsy, is append()ed to the element (so it may be a string
or DOM element.
*/
dom.legend = function(legendText){
const rc = this.create('legend');
if(legendText) this.append(rc, legendText);
return rc;
};
/**
Appends each argument after the first to the first argument
(a DOM node) and returns the first argument.
- If an argument is a string or number, it is transformed
into a text node.
|
| ︙ | ︙ |