Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Added fossil.confirmer JS API to offer a non-intrusive click confirmation mechanism. Re-activated the /filepage content reload button with a confirmation click required. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | fileedit-ajaxify |
| Files: | files | file ages | folders |
| SHA3-256: |
e70ab3a368b78adfb76feb58e86e647c |
| User & Date: | stephan 2020-05-06 03:02:23.302 |
Context
|
2020-05-06
| ||
| 03:19 | Removed devious hard tabs which slipped in while porting. ... (check-in: 626c45609e user: stephan tags: fileedit-ajaxify) | |
| 03:02 | Added fossil.confirmer JS API to offer a non-intrusive click confirmation mechanism. Re-activated the /filepage content reload button with a confirmation click required. ... (check-in: e70ab3a368 user: stephan tags: fileedit-ajaxify) | |
| 01:58 | Removed the FORM element - it was superfluous and particularly stubborn in how it responded to every button. ... (check-in: 7635d9345d user: stephan tags: fileedit-ajaxify) | |
Changes
Changes to src/fileedit.c.
| ︙ | ︙ | |||
1562 1563 1564 1565 1566 1567 1568 |
/******* Content tab *******/
{
CX("<div id='fileedit-tab-content' "
"data-tab-parent='fileedit-tabs' "
"data-tab-label='File Content'"
">");
CX("<div class='fileedit-options flex-container row'>");
| | | > > > > | | 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 |
/******* Content tab *******/
{
CX("<div id='fileedit-tab-content' "
"data-tab-parent='fileedit-tabs' "
"data-tab-label='File Content'"
">");
CX("<div class='fileedit-options flex-container row'>");
if(1){
/* Discard/reload button. Leave this out until we have a
** nice way of offering confirmation, e.g. like the old
** jQuery.confirmer plugin which required a 2nd click of the
** button within X seconds to confirm. Right now it's simply
** to easy to tap by accident. */
CX("<button class='fileedit-content-reload confirmer' "
"title='Reload the file from the server, discarding "
"any local edits. To help avoid accidental loss of "
"edits, it requires confirmation (a second click) within "
"a few seconds or it will not reload.'"
">Discard & Reload</button>");
}
style_select_list_int("select-font-size",
"editor_font_size", "Editor Font Size",
NULL/*tooltip*/,
100,
"100%", 100, "125%", 125,
"150%", 150, "175%", 175,
|
| ︙ | ︙ | |||
1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 |
}
blob_reset(&err);
CheckinMiniInfo_cleanup(&cimi);
style_emit_script_fossil_bootstrap(0);
style_emit_script_fetch(0);
style_emit_script_tabs(0);
style_emit_script_builtin("fossil.page.fileedit.js",0);
if(blob_size(&endScript)>0){
style_emit_script_tag(0,0);
CX("(function(){\n");
CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
&endScript);
CX("})();");
style_emit_script_tag(1,0);
}
db_end_transaction(0);
style_footer();
}
| > | 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 |
}
blob_reset(&err);
CheckinMiniInfo_cleanup(&cimi);
style_emit_script_fossil_bootstrap(0);
style_emit_script_fetch(0);
style_emit_script_tabs(0);
style_emit_script_builtin("fossil.page.fileedit.js",0);
style_emit_script_confirmer(0);
if(blob_size(&endScript)>0){
style_emit_script_tag(0,0);
CX("(function(){\n");
CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
&endScript);
CX("})();");
style_emit_script_tag(1,0);
}
db_end_transaction(0);
style_footer();
}
|
Changes to src/fossil.bootstrap.js.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
"use strict";
(function(global){
/* Bootstrapping bits for the global.fossil object. Must be
loaded after style.c:style_emit_script_tag() has initialized
that object.
*/
/**
Returns the current time in something approximating
ISO-8601 format.
*/
const timestring = function f(){
if(!f.rx1){
f.rx1 = /\.\d+Z$/;
| > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"use strict";
(function(global){
/* Bootstrapping bits for the global.fossil object. Must be
loaded after style.c:style_emit_script_tag() has initialized
that object.
*/
const F = global.fossil;
/**
Returns the current time in something approximating
ISO-8601 format.
*/
const timestring = function f(){
if(!f.rx1){
f.rx1 = /\.\d+Z$/;
|
| ︙ | ︙ | |||
23 24 25 26 27 28 29 | ** element, its innerText gets assigned to the concatenation of all ** arguments (with a space between each), and the CSS 'error' class is ** removed from the object. Pass it a falsy value to clear the target ** element. ** ** Returns this object. */ | | | | | | 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 |
** element, its innerText gets assigned to the concatenation of all
** arguments (with a space between each), and the CSS 'error' class is
** removed from the object. Pass it a falsy value to clear the target
** element.
**
** Returns this object.
*/
F.message = function f(msg){
const args = Array.prototype.slice.call(arguments,0);
const tgt = f.targetElement;
args.unshift(timestring(),'UTC:');
if(tgt){
tgt.classList.remove('error');
tgt.innerText = args.join(' ');
}
else{
args.unshift('Fossil status:');
console.debug.apply(console,args);
}
return this;
};
/*
** Set default message.targetElement to #fossil-status-bar, if found.
*/
F.message.targetElement =
document.querySelector('#fossil-status-bar');
/*
** By default fossil.error() sends its first argument to
** console.error(). If fossil.message.targetElement (yes,
** fossil.message) is set, it adds the 'error' CSS class to
** that element and sets its content as defined for message().
**
** Returns this object.
*/
F.error = function f(msg){
const args = Array.prototype.slice.call(arguments,0);
const tgt = F.message.targetElement;
args.unshift(timestring(),'UTC:');
if(tgt){
tgt.classList.add('error');
tgt.innerText = args.join(' ');
}
else{
args.unshift('Fossil error:');
|
| ︙ | ︙ | |||
76 77 78 79 80 81 82 |
If the 2nd argument is an array, each encoded element is appended
to that array and tgtArray is returned. The above object would be
appended as ['a','=','1','&','b','=','2']. This form is used for
building up parameter lists before join('')ing the array to create
the result string.
*/
| | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 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 140 141 142 143 144 145 |
If the 2nd argument is an array, each encoded element is appended
to that array and tgtArray is returned. The above object would be
appended as ['a','=','1','&','b','=','2']. This form is used for
building up parameter lists before join('')ing the array to create
the result string.
*/
F.encodeUrlArgs = function(obj,tgtArray){
if(!obj) return '';
const a = (tgtArray instanceof Array) ? tgtArray : [];
let k, i = 0;
for( k in obj ){
if(i++) a.push('&');
a.push(encodeURIComponent(k),
'=',encodeURIComponent(obj[k]));
}
return a===tgtArray ? a : a.join('');
};
/**
repoUrl( repoRelativePath [,urlParams] )
Creates a URL by prepending this.rootPath to the given path
(which must be relative from the top of the site, without a
leading slash). If urlParams is a string, it must be
paramters encoded in the form "key=val&key2=val2...", WITHOUT
a leading '?'. If it's an object, all of its properties get
appended to the URL in that form.
*/
F.repoUrl = function(path,urlParams){
if(!urlParams) return this.rootPath+path;
const url=[this.rootPath,path];
url.push('?');
if('string'===typeof urlParams) url.push(urlParams);
else if('object'===typeof urlParams){
this.encodeUrlArgs(urlParams, url);
}
return url.join('');
};
/**
Returns true if v appears to be a plain object.
*/
F.isObject = function(v){
return v &&
(v instanceof Object) &&
('[object Object]' === Object.prototype.toString.apply(v) );
};
/**
For each object argument, this function combines their properties,
using a last-one-wins policy, and returns a new object with the
combined properties. If passed a single object, it effectively
shallowly clones that object.
*/
F.mergeLastWins = function(){
var k, o, i;
const n = arguments.length, rc={};
for(i = 0; i < n; ++i){
if(!F.isObject(o = arguments[i])) continue;
for( k in o ){
if(o.hasOwnProperty(k)) rc[k] = o[k];
}
}
return rc;
};
})(window);
|
Added src/fossil.confirmer.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 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 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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
/**************************************************************
Confirmer is a utility which provides an alternative to confirmation
dialog boxes and "check this checkbox to confirm action" widgets. It
acts by modifying a button to require two clicks within a certain
time, with the second click acting as a confirmation of the first. If
the second click does not come within a specified timeout then the
action is not confirmed.
Usage:
fossil.confirmer(domElement, options);
Usually:
fossil.confirmer(element, {
onconfirm: function(){
// this === the element.
// Do whatever the element would normally do when
// clicked.
}
});
Options:
.initialText = initial text of the element. Defaults
to the result of the element's .value (for INPUT tags) or
innerHTML (for everything else).
.confirmText = text to show when in "confirm mode".
Default=("Confirm: "+initialText), or something similar.
.timeout = Number of milliseconds to wait for confirmation.
Default=3000.
.onconfirm = function to call when clicked in confirm mode. Default
= undefined. The function's "this" is the the DOM element to which the
countdown applies.
.ontimeout = function to call when confirm is not issued. Default =
undefined. The function's "this" is the DOM element to which the
countdown applies.
.onactivate = function to call when item is clicked, but only if the
item is not currently in countdown mode. This is called (and must
return) before the countdown starts. The function's "this" is the
DOM element to which the countdown applies. This can be used, e.g.,
to change the element's text or CSS classes.
.classInitial = optional CSS class string (default='') which
is added to the element during its "initial" state (the state
it is in when it is not waiting on a timeout). When the target
is activated (waiting on a timeout) this class is removed.
In the case of a timeout, this class is added *before* the
.ontimeout handler is called.
.classActivated = optional CSS class string (default='') which
is added to the target when it is waiting on a timeout. When
the target leaves timeout-wait mode, this class is removed.
When timeout-wait mode is entered, this class is added *before*
the .onactivate handler is called.
.debug = boolean. If truthy, it sends some debug output
to the dev console to track what it's doing.
Due to the nature of multi-threaded code, it is potentially possible
that confirmation and timeout actions BOTH happen if the user triggers
the associated action at "just the right millisecond" before the
timeout is triggered.
To change the default option values, modify the
fossil.confirmer.defaultOpts object.
Terse Change history:
20200506:
- Ported from jQuery to plain JS.
- 20181112:
- extended to support certain INPUT elements.
- made default opts configurable.
- 20070717: initial jQuery-based impl.
*/
(function(F/*the fossil object*/){
"use strict";
F.confirmer = function f(elem,opt){
const dbg = opt.debug
? function(){console.debug.apply(console,arguments)}
: function(){};
dbg("confirmer opt =",opt);
if(!f.Holder){
f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
f.Holder = function(target,opt){
const self = this;
self.target = target;
self.opt = opt;
self.timerID = undefined;
self.state = this.states.initial;
const isInput = f.isInput(target);
const updateText = function(msg){
if(isInput) target.value = msg;
else target.innerHTML = msg;
}
updateText(self.opt.initialText);
this.setClasses(false);
this.doTimeout = function() {
this.timerID = undefined;
if( this.state != this.states.waiting ) {
// it was already confirmed
return;
}
this.setClasses( false );
this.state = this.states.initial;
dbg("Timeout triggered.");
updateText(this.opt.initialText);
if( this.opt.ontimeout ) {
this.opt.ontimeout.call(this.target);
}
};
target.addEventListener(
'click', function(){
switch( self.state ) {
case( self.states.waiting ):
if( undefined !== self.timerID ) clearTimeout( self.timerID );
self.state = self.states.initial;
self.setClasses( false );
dbg("Confirmed");
updateText(self.opt.initialText);
if( self.opt.onconfirm ) self.opt.onconfirm.call(self.target);
break;
case( self.states.initial ):
self.setClasses( true );
if( self.opt.onactivate ) self.opt.onactivate.call( self.target );
self.state = self.states.waiting;
dbg("Waiting "+self.opt.timeout+"ms on confirmation...");
updateText( self.opt.confirmText );
self.timerID = setTimeout(function(){self.doTimeout();},self.opt.timeout );
break;
default: // can't happen.
break;
}
}, false
);
};
f.Holder.prototype = {
states:{
initial: 0, waiting: 1
},
setClasses: function(activated) {
if( activated ) {
if( this.opt.classActivated ) {
this.target.addClass( this.opt.classActivated );
}
if( this.opt.classInitial ) {
this.target.removeClass( this.opt.classInitial );
}
} else {
if( this.opt.classInitial ) {
this.target.addClass( this.opt.classInitial );
}
if( this.opt.classActivated ) {
this.target.removeClass( this.opt.classActivated );
}
}
}
};
}/*static init*/
opt = F.mergeLastWins(f.defaultOpts,{
initialText: (
f.isInput(elem) ? elem.value : elem.innerHTML
) || "PLEASE SET .initialText"
},opt);
if(!opt.confirmText){
opt.confirmText = "Confirm: "+opt.initialText;
}
new f.Holder(elem,opt);
return this;
};
/**
The default options for initConfirmer(). Tweak them to set the
defaults. A couple of them (initialText and confirmText) are
dynamically-generated, and can't reasonably be set in the
defaults.
*/
F.confirmer.defaultOpts = {
timeout:3000,
onconfirm:undefined,
ontimeout:undefined,
onactivate:undefined,
classInitial:'',
classActivated:'',
debug:true
};
})(window.fossil);
|
Changes to src/fossil.page.fileedit.js.
| ︙ | ︙ | |||
52 53 54 55 56 57 58 |
diffButtons.querySelector('button.unified').addEventListener(
"click",(e)=>P.diff(false), false
);
P.e.btnCommit.addEventListener(
"click",(e)=>P.commit(), false
);
if(P.e.btnReload){
| | > | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
diffButtons.querySelector('button.unified').addEventListener(
"click",(e)=>P.diff(false), false
);
P.e.btnCommit.addEventListener(
"click",(e)=>P.commit(), false
);
if(P.e.btnReload){
F.confirmer(P.e.btnReload, {
confirmText: "Really reload, losing edits?",
onconfirm: (e)=>P.loadFile()
});
}
/**
Cosmetic: jump through some hoops to enable/disable
certain preview options depending on the current
preview mode...
*/
const selectPreviewMode =
|
| ︙ | ︙ |
Changes to src/main.mk.
| ︙ | ︙ | |||
217 218 219 220 221 222 223 224 225 226 227 228 229 230 | $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.dom.js \ $(SRCDIR)/fossil.fetch.js \ $(SRCDIR)/fossil.page.fileedit.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ | > | 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.confirmer.js \ $(SRCDIR)/fossil.dom.js \ $(SRCDIR)/fossil.fetch.js \ $(SRCDIR)/fossil.page.fileedit.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ |
| ︙ | ︙ |
Changes to src/style.c.
| ︙ | ︙ | |||
1572 1573 1574 1575 1576 1577 1578 |
void style_emit_script_tabs(int asInline){
static int once = 0;
if(0==once++){
style_emit_script_dom(asInline);
style_emit_script_builtin("fossil.tabs.js",asInline);
}
}
| > > > > > > > > > > > > > > > > > | 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 |
void style_emit_script_tabs(int asInline){
static int once = 0;
if(0==once++){
style_emit_script_dom(asInline);
style_emit_script_builtin("fossil.tabs.js",asInline);
}
}
/*
** The first time this is called, it calls style_emit_script_dom(),
** passing it the given asInline value, and emits the JS code from the
** built-in file fossil.confirmer.js. Subsequent calls are no-ops.
**
** If passed a true value, it emits the contents directly
** to the page output, else it emits a script tag with a
** src=builtin/... to load the script.
*/
void style_emit_script_confirmer(int asInline){
static int once = 0;
if(0==once++){
style_emit_script_dom(asInline);
style_emit_script_builtin("fossil.confirmer.js",asInline);
}
}
|
Changes to win/Makefile.mingw.
| ︙ | ︙ | |||
639 640 641 642 643 644 645 646 647 648 649 650 651 652 | $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.dom.js \ $(SRCDIR)/fossil.fetch.js \ $(SRCDIR)/fossil.page.fileedit.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ | > | 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 | $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/fossil.bootstrap.js \ $(SRCDIR)/fossil.confirmer.js \ $(SRCDIR)/fossil.dom.js \ $(SRCDIR)/fossil.fetch.js \ $(SRCDIR)/fossil.page.fileedit.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ |
| ︙ | ︙ |
Changes to win/Makefile.msc.
| ︙ | ︙ | |||
546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
$(SRCDIR)\..\skins\xekri\header.txt \
$(SRCDIR)\accordion.js \
$(SRCDIR)\ci_edit.js \
$(SRCDIR)\copybtn.js \
$(SRCDIR)\diff.tcl \
$(SRCDIR)\forum.js \
$(SRCDIR)\fossil.bootstrap.js \
$(SRCDIR)\fossil.dom.js \
$(SRCDIR)\fossil.fetch.js \
$(SRCDIR)\fossil.page.fileedit.js \
$(SRCDIR)\fossil.tabs.js \
$(SRCDIR)\graph.js \
$(SRCDIR)\href.js \
$(SRCDIR)\login.js \
| > | 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 |
$(SRCDIR)\..\skins\xekri\header.txt \
$(SRCDIR)\accordion.js \
$(SRCDIR)\ci_edit.js \
$(SRCDIR)\copybtn.js \
$(SRCDIR)\diff.tcl \
$(SRCDIR)\forum.js \
$(SRCDIR)\fossil.bootstrap.js \
$(SRCDIR)\fossil.confirmer.js \
$(SRCDIR)\fossil.dom.js \
$(SRCDIR)\fossil.fetch.js \
$(SRCDIR)\fossil.page.fileedit.js \
$(SRCDIR)\fossil.tabs.js \
$(SRCDIR)\graph.js \
$(SRCDIR)\href.js \
$(SRCDIR)\login.js \
|
| ︙ | ︙ |