Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Minor code style improvements, fixed a couple jquery-isms in the confirmer port, added the ability to select tabs by number and pre-select a specific tab, removed a superfluous C function. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | fileedit-ajaxify |
| Files: | files | file ages | folders |
| SHA3-256: |
9085ec2351d42966153cb6b13ed15f0c |
| User & Date: | stephan 2020-05-06 04:52:29.446 |
Context
|
2020-05-06
| ||
| 05:21 | cosmetic internal tweaks. ... (check-in: bb71f9ecd2 user: stephan tags: fileedit-ajaxify) | |
| 04:52 | Minor code style improvements, fixed a couple jquery-isms in the confirmer port, added the ability to select tabs by number and pre-select a specific tab, removed a superfluous C function. ... (check-in: 9085ec2351 user: stephan tags: fileedit-ajaxify) | |
| 03:30 | Removed a bogus, but harmless, JS dep and corrected some copy/pasted docs. ... (check-in: 2abc1ff42a user: stephan tags: fileedit-ajaxify) | |
Changes
Changes to src/default_css.txt.
| ︙ | ︙ | |||
1002 1003 1004 1005 1006 1007 1008 |
////////////////////////////////////////////////////////////////////
// Styles developed for /fileedit but which have wider
// applicability:
.flex-container {
display: flex;
}
.flex-container.row {
| | | | | | | | | | | | | | | | 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 |
////////////////////////////////////////////////////////////////////
// Styles developed for /fileedit but which have wider
// applicability:
.flex-container {
display: flex;
}
.flex-container.row {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.flex-container.row.stretch {
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
margin: 0;
}
.flex-container.column {
flex-direction: column;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.flex-container.column.stretch {
align-items: stretch;
margin: 0;
}
.font-size-100 {
font-size: 100%;
}
.font-size-125 {
font-size: 125%;
}
|
| ︙ | ︙ | |||
1053 1054 1055 1056 1057 1058 1059 |
display: inline-block;
cursor: pointer;
}
.input-with-label > * {
vertical-align: middle;
}
.input-with-label > input {
| | | | | | | 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 |
display: inline-block;
cursor: pointer;
}
.input-with-label > * {
vertical-align: middle;
}
.input-with-label > input {
margin: 0;
}
.input-with-label > select {
margin: 0;
}
.input-with-label > input[type=text] {
margin: 0;
}
.input-with-label > textarea {
margin: 0;
}
.input-with-label > input[type=checkbox] {
vertical-align: sub;
}
.input-with-label > input[type=radio] {
vertical-align: sub;
}
.input-with-label > span {
margin: 0 0.25em 0 0.25em;
vertical-align: middle;
}
|
Changes to src/fileedit.c.
| ︙ | ︙ | |||
26 27 28 29 30 31 32 | ** State for the "mini-checkin" infrastructure, which enables the ** ability to commit changes to a single file without a checkout ** db, e.g. for use via an HTTP request. ** ** Use CheckinMiniInfo_init() to cleanly initialize one to a known ** valid/empty default state. ** | | > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
** State for the "mini-checkin" infrastructure, which enables the
** ability to commit changes to a single file without a checkout
** db, e.g. for use via an HTTP request.
**
** Use CheckinMiniInfo_init() to cleanly initialize one to a known
** valid/empty default state.
**
** Memory for all non-const pointer members is owned by the
** CheckinMiniInfo instance and is freed by CheckinMiniInfo_cleanup().
** Similarly, each instance owns any memory for its Blob members.
*/
struct CheckinMiniInfo {
Manifest * pParent; /* parent checkin. Memory is owned by this
object. */
char *zParentUuid; /* Full UUID of pParent */
char *zFilename; /* Name of single file to commit. Must be
relative to the top of the repo. */
|
| ︙ | ︙ | |||
166 167 168 169 170 171 172 | fossil_free(p->zCommentMimetype); fossil_free(p->zUser); CheckinMiniInfo_init(p); } /* ** Internal helper which returns an F-card perms string suitable for | | > < < < < | > | > | 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 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
fossil_free(p->zCommentMimetype);
fossil_free(p->zUser);
CheckinMiniInfo_init(p);
}
/*
** Internal helper which returns an F-card perms string suitable for
** writing as-is into a manifest. If it's not empty, it includes a
** leading space to separate it from the F-card's hash field.
*/
static const char * mfile_permint_mstring(int perm){
switch(perm){
case PERM_EXE: return " x";
case PERM_LNK: return " l";
default: return "";
}
}
/*
** Given a ManifestFile permission string (or NULL), it returns one of
** PERM_REG, PERM_EXE, or PERM_LNK.
*/
static int mfile_permstr_int(const char *zPerm){
if(!zPerm || !*zPerm) return PERM_REG;
else if(strstr(zPerm,"x")) return PERM_EXE;
else if(strstr(zPerm,"l")) return PERM_LNK;
else return PERM_REG/*???*/;
}
/*
** Internal helper for checkin_mini() and friends. Appends an F-card
** for p to pOut.
*/
static void checkin_mini_append_fcard(Blob *pOut,
const ManifestFile *p){
if(p->zUuid){
assert(*p->zUuid);
blob_appendf(pOut, "F %F %s%s", p->zName,
p->zUuid,
mfile_permint_mstring(manifest_file_mperm(p)));
if(p->zPrior){
assert(*p->zPrior);
blob_appendf(pOut, " %F\n", p->zPrior);
}else{
blob_append(pOut, "\n", 1);
}
}else{
|
| ︙ | ︙ | |||
1674 1675 1676 1677 1678 1679 1680 |
"</div>");
CX("<div id='fileedit-tab-diff-wrapper'>"
"Diffs will be shown here."
"</div>");
CX("</div>"/*#fileedit-tab-diff*/);
}
| < > | | 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 |
"</div>");
CX("<div id='fileedit-tab-diff-wrapper'>"
"Diffs will be shown here."
"</div>");
CX("</div>"/*#fileedit-tab-diff*/);
}
/****** Commit ******/
CX("<div id='fileedit-tab-commit' "
"data-tab-parent='fileedit-tabs' "
"data-tab-select='1' "
"data-tab-label='Commit'"
">");
{
/******* Commit flags/options *******/
CX("<div class='fileedit-options flex-container row'>");
style_labeled_checkbox("cb-dry-run",
"dry_run", "Dry-run?", "1",
"In dry-run mode, the Save button performs "
"all work needed for saving but then rolls "
"back the transaction, and thus does not "
"really save.",
|
| ︙ | ︙ | |||
1733 1734 1735 1736 1737 1738 1739 |
"Inherit", 0,
"Unix", 1,
"Windows", 2,
NULL);
CX("</div>"/*checkboxes*/);
}
| | | 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 |
"Inherit", 0,
"Unix", 1,
"Windows", 2,
NULL);
CX("</div>"/*checkboxes*/);
}
{ /******* Commit comment, button, and result manifest *******/
CX("<fieldset class='fileedit-options'>"
"<legend>Message (required)</legend><div>");
CX("<input type='text' name='comment' "
"id='fileedit-comment'>");
/* ^^^ adding the 'required' attribute means we cannot even
submit for PREVIEW mode if it's empty :/. */
if(blob_size(&cimi.comment)){
|
| ︙ | ︙ | |||
1780 1781 1782 1783 1784 1785 1786 |
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");
| > > > | > | 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 |
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){"
"fossil.error(e);\n"
"console.error('Exception:',e);\n"
"}\n",
&endScript);
CX("})();");
style_emit_script_tag(1,0);
}
db_end_transaction(0);
style_footer();
}
|
Changes to src/fossil.confirmer.js.
1 2 3 4 5 6 7 | /************************************************************** 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. | > | 1 2 3 4 5 6 7 8 | "use strict"; /************************************************************** 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. |
| ︙ | ︙ | |||
49 50 51 52 53 54 55 | .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. | | | < < < | 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 |
.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.
.classWaiting = 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*/){
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){
|
| ︙ | ︙ | |||
119 120 121 122 123 124 125 |
this.opt.ontimeout.call(this.target);
}
};
target.addEventListener(
'click', function(){
switch( self.state ) {
case( self.states.waiting ):
| | > > > | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
this.opt.ontimeout.call(this.target);
}
};
target.addEventListener(
'click', function(){
switch( self.state ) {
case( self.states.waiting ):
if( undefined !== self.timerID ){
clearTimeout( self.timerID );
delete 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 ):
|
| ︙ | ︙ | |||
141 142 143 144 145 146 147 |
default: // can't happen.
break;
}
}, false
);
};
f.Holder.prototype = {
| < | < | | | | | | | | < | 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 |
default: // can't happen.
break;
}
}, false
);
};
f.Holder.prototype = {
states:{initial: 0, waiting: 1},
setClasses: function(activated) {
if(activated) {
if( this.opt.classWaiting ) {
this.target.classList.add( this.opt.classWaiting );
}
if( this.opt.classInitial ) {
this.target.classList.remove( this.opt.classInitial );
}
}else{
if( this.opt.classInitial ) {
this.target.classList.add( this.opt.classInitial );
}
if( this.opt.classWaiting ) {
this.target.classList.remove( this.opt.classWaiting );
}
}
}
};
}/*static init*/
opt = F.mergeLastWins(f.defaultOpts,{
initialText: (
f.isInput(elem) ? elem.value : elem.innerHTML
) || "PLEASE SET .initialText"
},opt);
|
| ︙ | ︙ | |||
183 184 185 186 187 188 189 |
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,
| | | | | | | | 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
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: '',
classWaiting: '',
debug: false
};
})(window.fossil);
|
Changes to src/fossil.fetch.js.
| ︙ | ︙ | |||
21 22 23 24 25 26 27 | - onload: callback(responseData) (default = output response to the console). - onerror: callback(XHR onload event | exception) (default = event or exception to the console). | | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | - onload: callback(responseData) (default = output response to the console). - onerror: callback(XHR onload event | exception) (default = event or exception to the console). - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! - payload: anything acceptable by XHR2.send(ARG) (DOMString, Document, FormData, Blob, File, ArrayBuffer), or a plain object or array, either of which gets JSON.stringify()'d. If payload is set then the method is automatically set to 'POST'. If an object/array is converted to JSON, the contentType option is automatically set to 'application/json'. By default XHR2 will set the content type |
| ︙ | ︙ | |||
58 59 60 61 62 63 64 65 66 67 68 |
overwrite those members to provide default implementations suitable
for the page's use.
Returns this object, noting that the XHR request is asynchronous,
and still in transit (or has yet to be sent) when that happens.
*/
window.fossil.fetch = function f(uri,opt){
if(!f.onerror){
f.onerror = function(e/*event or exception*/){
console.error("Ajax error:",e);
if(e instanceof Error){
| > | | | | | | 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 |
overwrite those members to provide default implementations suitable
for the page's use.
Returns this object, noting that the XHR request is asynchronous,
and still in transit (or has yet to be sent) when that happens.
*/
window.fossil.fetch = function f(uri,opt){
const F = fossil;
if(!f.onerror){
f.onerror = function(e/*event or exception*/){
console.error("Ajax error:",e);
if(e instanceof Error){
F.error('Exception:',e);
}
else if(e.originalTarget && e.originalTarget.responseType==='text'){
const txt = e.originalTarget.responseText;
try{
/* The convention from the /filepage_xyz routes is to
return error responses in JSON form if possible:
{error: "..."}
*/
const j = JSON.parse(txt);
console.error("Error JSON:",j);
if(j.error){ F.error(j.error) };
}catch(e){/* Try harder */
F.error(txt)
}
}
};
f.onload = (r)=>console.debug('ajax response:',r);
}
if('/'===uri[0]) uri = uri.substr(1);
if(!opt) opt = {};
else if('function'===typeof opt) opt={onload:opt};
if(!opt.onload) opt.onload = f.onload;
if(!opt.onerror) opt.onerror = f.onerror;
let payload = opt.payload, jsonResponse = false;
if(undefined!==payload){
opt.method = 'POST';
if(!(payload instanceof FormData)
&& !(payload instanceof Document)
&& !(payload instanceof Blob)
&& !(payload instanceof File)
&& !(payload instanceof ArrayBuffer)
&& ('object'===typeof payload
|| payload instanceof Array)){
payload = JSON.stringify(payload);
opt.contentType = 'application/json';
}
}
const url=[F.repoUrl(uri,opt.urlParams)],
x=new XMLHttpRequest();
if('POST'===opt.method && 'string'===typeof opt.contentType){
x.setRequestHeader('Content-Type',opt.contentType);
}
x.open(opt.method||'GET', url.join(''), true);
if('json'===opt.responseType){
/* 'json' is an extension to the supported XHR.responseType
|
| ︙ | ︙ | |||
129 130 131 132 133 134 135 |
opt.onload((jsonResponse && this.response)
? JSON.parse(this.response) : this.response);
}catch(e){
if(opt.onerror) opt.onerror(e);
}
}
}
| | | 130 131 132 133 134 135 136 137 138 139 140 |
opt.onload((jsonResponse && this.response)
? JSON.parse(this.response) : this.response);
}catch(e){
if(opt.onerror) opt.onerror(e);
}
}
}
if(undefined!==payload) x.send(payload);
else x.send();
return this;
};
|
Changes to src/fossil.tabs.js.
1 | "use strict"; | | < < < < < > > > > | > > > > > > | > > | > > > > > > > > > > > > > > > > > > > > | > | > | | | | > | | > > > > > | | > | < < < | | | | | 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 |
"use strict";
(function(F/*fossil object*/){
const E = (s)=>document.querySelector(s),
EA = (s)=>document.querySelectorAll(s),
D = F.dom;
/**
Creates a TabManager. If passed an argument, it is
passed to init().
*/
const TabManager = function(domElem){
this.e = {};
if(domElem) this.init(domElem);
};
/**
Internal helper to normalize a method argument
to a tab element.
*/
const tabArg = function(arg,tabMgr){
if('string'===typeof arg) arg = E(arg);
else if(tabMgr && 'number'===typeof arg && arg>=0){
arg = tabMgr.e.tabs.childNodes[arg];
}
return arg;
};
const setVisible = function(e,yes){
D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
};
TabManager.prototype = {
/**
Initializes the tabs associated with the given tab container
(DOM element or selector for a single element).
The tab container must have an 'id' attribute. This function
looks through the DOM for all elements which have
data-tab-parent=thatId. For each one it creates a button to
switch to that tab and moves the element into this.e.tabs.
The label for each tab is set by the data-tab-label attribute
of each element, defaulting to something not terribly useful.
When it's done, it auto-selects the first tab unless a tab has
a truthy numeric value in its data-tab-select attribute, in
which case the last tab to have such a property is selected.
This method must only be called once per instance. TabManagers
may be nested but may not share any tabs instances.
Returns this object.
DOM elements of potential interest to users:
this.e.container = the outermost container element.
this.e.tabBar = the button bar
this.e.tabs = the parent for all of the tab elements.
It is legal, within reason, to manipulate these a bit, in
particular this.e.container, e.g. by adding more children to
it. Do not remove elements from the tabs or tabBar, however, or
the tab state may get sorely out of sync.
CSS classes: the container element has whatever class(es) the
client sets on. this.e.tabBar gets the 'tab-bar' class and
this.e.tabs gets the 'tabs' class. It's hypothetically possible
to move the tabs to either side or the bottom using only CSS,
but it's never been tested.
*/
init: function(container){
container = tabArg(container);
const cID = container.getAttribute('id');
if(!cID){
throw new Error("Tab container element is missing 'id' attribute.");
}
const c = this.e.container = container;
this.e.tabBar = D.addClass(D.div(),'tab-bar');
this.e.tabs = D.addClass(D.div(),'tabs');
D.append(c, this.e.tabBar, this.e.tabs);
let selectIndex = 0;
EA('[data-tab-parent='+cID+']').forEach((c,n)=>{
if(+c.dataset.tabSelect) selectIndex=n;
this.addTab(c);
});
return this.switchToTab(selectIndex);
},
/**
For the given tab element, unique selector string, or integer
(0-based tab number), returns the button associated with that
tab, or undefined if the argument does not match any current
tab.
*/
getButtonForTab: function(tab){
tab = tabArg(tab,this);
var i = -1;
this.e.tabs.childNodes.forEach(function(e,n){
if(e===tab) i = n;
});
return i>=0 ? this.e.tabBar.childNodes[i] : undefined;
},
/**
Adds the given DOM element or unique selector as the next
tab in the tab container, adding a button to switch to
the tab. Returns this object.
*/
addTab: function f(tab){
if(!f.click){
f.click = function(e){
e.target.$manager.switchToTab(e.target.$tab);
};
}
tab = tabArg(tab);
tab.remove();
D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
const lbl = tab.dataset.tabLabel || 'Tab #'+(this.e.tabs.childNodes.length-1);
const btn = D.button(lbl);
D.append(this.e.tabBar,btn);
btn.$manager = this;
btn.$tab = tab;
btn.addEventListener('click', f.click, false);
return this;
},
/**
If the given DOM element, unique selector, or integer (0-based
tab number) is one of this object's tabs, the UI makes that tab
the currently-visible one. Returns this object.
*/
switchToTab: function(tab){
tab = tabArg(tab,this);
const self = this;
this.e.tabs.childNodes.forEach((e,ndx)=>{
const btn = this.e.tabBar.childNodes[ndx];
if(e===tab){
setVisible(e, true);
D.addClass(btn,'selected');
}else{
|
| ︙ | ︙ |