Fossil SCM

fossil-scm / src / fossil.page.fileedit.js
Blame History Raw 1414 lines
1
(function(F/*the fossil object*/){
2
"use strict";
3
/**
4
Client-side implementation of the /filepage app. Requires that
5
the fossil JS bootstrapping is complete and that several fossil
6
JS APIs have been installed: fossil.fetch, fossil.dom,
7
fossil.tabs, fossil.storage, fossil.confirmer.
8
9
Custom events which can be listened for via
10
fossil.page.addEventListener():
11
12
- Event 'fileedit-file-loaded': passes on information when it
13
loads a file (whether from the network or its internal local-edit
14
cache), in the form of an "finfo" object:
15
16
{
17
filename: string,
18
checkin: UUID string,
19
branch: branch name of UUID,
20
isExe: bool, true only for executable files
21
mimetype: mimetype string, as determined by the fossil server.
22
}
23
24
The internal docs and code frequently use the term "finfo", and such
25
references refer to an object with that form.
26
27
The fossil.page.fileContent() method gets or sets the current file
28
content for the page.
29
30
- Event 'fileedit-committed': is fired when a commit completes,
31
passing on the same info as fileedit-file-loaded.
32
33
- Event 'fileedit-content-replaced': when the editor's content is
34
replaced, as opposed to it being edited via user
35
interaction. This normally happens via selecting a file to
36
load. The event detail is the fossil.page object, not the current
37
file content.
38
39
- Event 'fileedit-preview-updated': when the preview is refreshed
40
from the server, this event passes on information about the preview
41
change in the form of an object:
42
43
{
44
element: the DOM element which contains the content preview.
45
46
mimetype: the fossil-reported content mimetype.
47
48
previewMode: a string describing the preview mode: see
49
the fossil.page.previewModes map for the values. This can
50
be used to determine whether, e.g., the content is suitable
51
for applying a 3rd-party code highlighting API to.
52
}
53
54
Here's an example which can be used with the highlightjs code
55
highlighter to update the highlighting when the preview is
56
refreshed in "wiki" mode (which includes fossil-native wiki and
57
markdown):
58
59
fossil.page.addEventListener(
60
'fileedit-preview-updated',
61
(ev)=>{
62
if(ev.detail.previewMode==='wiki'){
63
ev.detail.element.querySelectorAll(
64
'code[class^=language-]'
65
).forEach((e)=>hljs.highlightBlock(e));
66
}
67
}
68
);
69
*/
70
const E = (s)=>document.querySelector(s),
71
D = F.dom,
72
P = F.page;
73
74
P.config = {
75
defaultMaxStashSize: 7,
76
/**
77
See notes for this setting in fossil.page.wikiedit.js. Both
78
/wikiedit and /fileedit share this persistent config option
79
under the same storage key.
80
*/
81
shiftEnterPreview: F.storage.getBool('edit-shift-enter-preview', true)
82
};
83
84
/**
85
$stash is an internal-use-only object for managing "stashed"
86
local edits, to help avoid that users accidentally lose content
87
by switching tabs or following links or some such. The basic
88
theory of operation is...
89
90
All "stashed" state is stored using fossil.storage.
91
92
- When the current file content is modified by the user, the
93
current stathe of the current P.finfo and its the content
94
is stashed. For the built-in editor widget, "changes" is
95
notified via a 'change' event. For a client-side custom
96
widget, the client needs to call P.stashContentChange() when
97
their widget triggers the equivalent of a 'change' event.
98
99
- For certain non-content updates (as of this writing, only the
100
is-executable checkbox), only the P.finfo stash entry is
101
updated, not the content (unless the content has not yet been
102
stashed, in which case it is also stashed so that the stash
103
always has matching pairs of finfo/content).
104
105
- When saving, the stashed entry for the previous version is removed
106
from the stash.
107
108
- When "loading", we use any stashed state for the given
109
checkin/file combination. When forcing a re-load of content,
110
any stashed entry for that combination is removed from the
111
stash.
112
113
- Every time P.stashContentChange() updates the stash, it is
114
pruned to $stash.prune.defaultMaxCount most-recently-updated
115
entries.
116
117
- This API often refers to "finfo objects." Those are objects
118
with a minimum of {checkin,filename} properties (which must be
119
valid), and a combination of those two properties is used as
120
basis for the stash keys for any given checkin/filename
121
combination.
122
123
The structure of the stash is a bit convoluted for efficiency's
124
sake: we store a map of file info (finfo) objects separately from
125
those files' contents because otherwise we would be required to
126
JSONize/de-JSONize the file content when stashing/restoring it,
127
and that would be horribly inefficient (meaning "battery-consuming"
128
on mobile devices).
129
*/
130
const $stash = {
131
keys: {
132
index: F.page.name+'.index'
133
},
134
/**
135
index: {
136
"CHECKIN_HASH:FILENAME": {file info w/o content}
137
...
138
}
139
140
In F.storage we...
141
142
- Store this.index under the key this.keys.index.
143
144
- Store each file's content under the key
145
(P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
146
from the index entries to avoid having to JSONize/de-JSONize
147
the content. The assumption/hope is that the browser can store
148
those records "directly," without any intermediary
149
encoding/decoding going on.
150
*/
151
indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
152
/** Returns the key for storing content for the given key suffix,
153
by prepending P.name to suffix. */
154
contentKey: function(suffix){return P.name+'/'+suffix},
155
/** Returns the index object, fetching it from the stash or creating
156
it anew on the first call. */
157
getIndex: function(){
158
if(!this.index){
159
this.index = F.storage.getJSON(
160
this.keys.index, {}
161
);
162
}
163
return this.index;
164
},
165
_fireStashEvent: function(){
166
if(this._disableNextEvent) delete this._disableNextEvent;
167
else F.page.dispatchEvent('fileedit-stash-updated', this);
168
},
169
/**
170
Returns the stashed version, if any, for the given finfo object.
171
*/
172
getFinfo: function(finfo){
173
const ndx = this.getIndex();
174
return ndx[this.indexKey(finfo)];
175
},
176
/** Serializes this object's index to F.storage. Returns this. */
177
storeIndex: function(){
178
if(this.index) F.storage.setJSON(this.keys.index,this.index);
179
return this;
180
},
181
/** Updates the stash record for the given finfo
182
and (optionally) content. If passed 1 arg, only
183
the finfo stash is updated, else both the finfo
184
and its contents are (re-)stashed. Returns this.
185
*/
186
updateFile: function(finfo,content){
187
const ndx = this.getIndex(),
188
key = this.indexKey(finfo),
189
old = ndx[key];
190
const record = old || (ndx[key]={
191
checkin: finfo.checkin,
192
filename: finfo.filename,
193
mimetype: finfo.mimetype
194
});
195
record.isExe = !!finfo.isExe;
196
record.stashTime = new Date().getTime();
197
if(!record.branch) record.branch=finfo.branch;
198
this.storeIndex();
199
if(arguments.length>1){
200
F.storage.set(this.contentKey(key), content);
201
}
202
this._fireStashEvent();
203
return this;
204
},
205
/**
206
Returns the stashed content, if any, for the given finfo
207
object.
208
*/
209
stashedContent: function(finfo){
210
return F.storage.get(this.contentKey(this.indexKey(finfo)));
211
},
212
/** Returns true if we have stashed content for the given finfo
213
record. */
214
hasStashedContent: function(finfo){
215
return F.storage.contains(this.contentKey(this.indexKey(finfo)));
216
},
217
/** Unstashes the given finfo record and its content.
218
Returns this. */
219
unstash: function(finfo){
220
const ndx = this.getIndex(),
221
key = this.indexKey(finfo);
222
delete finfo.stashTime;
223
delete ndx[key];
224
F.storage.remove(this.contentKey(key));
225
this.storeIndex();
226
this._fireStashEvent();
227
return this;
228
},
229
/**
230
Clears all $stash entries from F.storage. Returns this.
231
*/
232
clear: function(){
233
const ndx = this.getIndex(),
234
self = this;
235
let count = 0;
236
Object.keys(ndx).forEach(function(k){
237
++count;
238
const e = ndx[k];
239
delete ndx[k];
240
F.storage.remove(self.contentKey(k));
241
});
242
F.storage.remove(this.keys.index);
243
delete this.index;
244
if(count) this._fireStashEvent();
245
return this;
246
},
247
/**
248
Removes all but the maxCount most-recently-updated stash
249
entries, where maxCount defaults to this.prune.defaultMaxCount.
250
*/
251
prune: function f(maxCount){
252
const ndx = this.getIndex();
253
const li = [];
254
if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
255
Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
256
li.sort((l,r)=>l.stashTime - r.stashTime);
257
let n = 0;
258
while(li.length>maxCount){
259
++n;
260
const e = li.shift();
261
this._disableNextEvent = true;
262
this.unstash(e);
263
console.warn("Pruned oldest local file edit entry:",e);
264
}
265
if(n) this._fireStashEvent();
266
}
267
};
268
$stash.prune.defaultMaxCount = P.config.defaultMaxStashSize;
269
270
/**
271
Widget for the checkin/file selection list.
272
*/
273
P.fileSelectWidget = {
274
e:{
275
container: E('#fileedit-file-selector')
276
},
277
finfo: {},
278
cache: {
279
checkins: undefined,
280
files:{},
281
branchKey: 'fileedit/uuid-branches',
282
branchNames: {}
283
},
284
/**
285
Fetches the list of leaf checkins from the server and updates
286
the UI with that list.
287
*/
288
loadLeaves: function(){
289
D.append(D.clearElement(
290
this.e.ciListLabel,
291
this.e.selectCi,
292
this.e.selectFiles
293
),"Loading leaves...");
294
D.disable(this.e.btnLoadFile, this.e.selectFiles, this.e.selectCi);
295
const self = this;
296
const onload = function(list){
297
D.append(D.clearElement(self.e.ciListLabel),
298
"Open leaves (newest first):");
299
self.cache.checkins = list;
300
D.clearElement(D.enable(self.e.selectCi));
301
let loadThisOne = P.initialFiles/*possibly injected at page-load time*/;
302
if(loadThisOne){
303
self.cache.files[loadThisOne.checkin] = loadThisOne;
304
delete P.initialFiles;
305
}
306
list.forEach(function(o,n){
307
if(!n && !loadThisOne) loadThisOne = o;
308
self.cache.branchNames[F.hashDigits(o.checkin,true)] = o.branch;
309
D.option(self.e.selectCi, o.checkin,
310
o.timestamp+' ['+o.branch+']: '
311
+F.hashDigits(o.checkin));
312
});
313
F.storage.setJSON(self.cache.branchKey, self.cache.branchNames);
314
if(loadThisOne){
315
self.e.selectCi.value = loadThisOne.checkin;
316
}
317
self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
318
};
319
if(P.initialLeaves/*injected at page-load time.*/){
320
const lv = P.initialLeaves;
321
delete P.initialLeaves;
322
onload(lv);
323
}else{
324
F.fetch('fileedit/filelist',{
325
urlParams:'leaves',
326
responseType: 'json',
327
onload: onload
328
});
329
}
330
},
331
/**
332
Loads the file list for the given checkin UUID. It uses a
333
cached copy on subsequent calls for the same UUID. If passed a
334
falsy value, it instead clears and disables the file selection
335
list.
336
*/
337
loadFiles: function(ciUuid){
338
delete this.finfo.filename;
339
this.finfo.checkin = ciUuid;
340
const selFiles = this.e.selectFiles;
341
if(!ciUuid){
342
D.clearElement(D.disable(selFiles, this.e.btnLoadFile));
343
return this;
344
}
345
const onload = (response)=>{
346
D.clearElement(selFiles);
347
D.append(
348
D.clearElement(this.e.fileListLabel),
349
"Editable files for ",
350
D.append(
351
D.code(), "[",
352
D.a(F.repoUrl('timeline',{
353
c: ciUuid
354
}), F.hashDigits(ciUuid)),"]"
355
), ":"
356
);
357
this.cache.files[response.checkin] = response;
358
response.editableFiles.forEach(function(fn,n){
359
D.option(selFiles, fn);
360
});
361
if(selFiles.options.length){
362
D.enable(selFiles, this.e.btnLoadFile);
363
}
364
};
365
const got = this.cache.files[ciUuid];
366
if(got){
367
onload(got);
368
return this;
369
}
370
D.disable(selFiles,this.e.btnLoadFile);
371
D.clearElement(selFiles);
372
D.append(D.clearElement(this.e.fileListLabel),
373
"Loading files for "+F.hashDigits(ciUuid)+"...");
374
F.fetch('fileedit/filelist',{
375
urlParams:{checkin: ciUuid},
376
responseType: 'json',
377
onload
378
});
379
return this;
380
},
381
382
/**
383
If this object has ever loaded the given checkin version via
384
loadLeaves(), this returns the branch name associated with that
385
version, else returns undefined;
386
*/
387
checkinBranchName: function(uuid){
388
return this.cache.branchNames[F.hashDigits(uuid,true)];
389
},
390
391
/**
392
Initializes the checkin/file selector widget. Must only be
393
called once.
394
*/
395
init: function(){
396
this.cache.branchNames = F.storage.getJSON(this.cache.branchKey, {});
397
const selCi = this.e.selectCi = D.addClass(D.select(), 'flex-grow'),
398
selFiles = this.e.selectFiles
399
= D.addClass(D.select(), 'file-list'),
400
btnLoad = this.e.btnLoadFile =
401
D.addClass(D.button("Load file"), "flex-shrink"),
402
filesLabel = this.e.fileListLabel =
403
D.addClass(D.div(),'flex-shrink','file-list-label'),
404
ciLabelWrapper = D.addClass(
405
D.div(), 'flex-container','flex-row', 'flex-shrink',
406
'stretch', 'child-gap-small'
407
),
408
btnReload = D.addClass(
409
D.button('Reload'), 'flex-shrink'
410
),
411
ciLabel = this.e.ciListLabel =
412
D.addClass(D.span(),'flex-shrink','checkin-list-label')
413
;
414
D.attr(selCi, 'title',"The list of opened leaves.");
415
D.attr(selFiles, 'title',
416
"The list of editable files for the selected checkin.");
417
D.attr(btnLoad, 'title',
418
"Load the selected file into the editor.");
419
D.disable(selCi, selFiles, btnLoad);
420
D.attr(selFiles, 'size', 12);
421
D.append(
422
this.e.container,
423
ciLabel,
424
D.append(ciLabelWrapper,
425
selCi,
426
btnReload),
427
filesLabel,
428
selFiles,
429
/* Use a wrapper for btnLoad so that the button itself does not
430
stretch to fill the parent width: */
431
D.append(D.addClass(D.div(), 'flex-shrink'), btnLoad)
432
);
433
if(F.config['fileedit-glob']){
434
D.append(
435
this.e.container,
436
D.append(
437
D.span(),
438
D.append(D.code(),"fileedit-glob"),
439
" config setting = ",
440
D.append(D.code(), JSON.stringify(F.config['fileedit-glob']))
441
)
442
);
443
}
444
445
this.loadLeaves();
446
selCi.addEventListener(
447
'change', (e)=>this.loadFiles(e.target.value), false
448
);
449
const doLoad = (e)=>{
450
this.finfo.filename = selFiles.value;
451
if(this.finfo.filename){
452
P.loadFile(this.finfo.filename, this.finfo.checkin);
453
}
454
};
455
btnLoad.addEventListener('click', doLoad, false);
456
selFiles.addEventListener('dblclick', doLoad, false);
457
btnReload.addEventListener(
458
'click', (e)=>this.loadLeaves(), false
459
);
460
delete this.init;
461
}
462
}/*P.fileSelectWidget*/;
463
464
/**
465
Widget for listing and selecting $stash entries.
466
*/
467
P.stashWidget = {
468
e:{/*DOM element(s)*/},
469
init: function(domInsertPoint/*insert widget BEFORE this element*/){
470
const wrapper = D.addClass(
471
D.attr(D.div(),'id','fileedit-stash-selector'),
472
'input-with-label'
473
);
474
const sel = this.e.select = D.select();
475
const btnClear = this.e.btnClear = D.button("Discard Edits"),
476
btnHelp = D.append(
477
D.addClass(D.div(), "help-buttonlet"),
478
'Locally-edited files. Timestamps are the last local edit time. ',
479
'Only the ',P.config.defaultMaxStashSize,' most recent files ',
480
'are retained. Saving or reloading a file removes it from this list. ',
481
D.append(D.code(),F.storage.storageImplName()),
482
' = ',F.storage.storageHelpDescription()
483
);
484
485
D.append(wrapper, "Local edits (",
486
D.append(D.code(),
487
F.storage.storageImplName()),
488
"):",
489
btnHelp, sel, btnClear);
490
F.helpButtonlets.setup(btnHelp);
491
D.option(D.disable(sel), undefined, "(empty)");
492
F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
493
F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
494
sel.addEventListener('change',function(e){
495
const opt = this.selectedOptions[0];
496
if(opt && opt._finfo) P.loadFile(opt._finfo);
497
});
498
if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/
499
D.append(wrapper, D.append(
500
D.addClass(D.span(),'warning'),
501
"Warning: persistent storage is not available, "+
502
"so uncomitted edits will not survive a page reload."
503
));
504
}
505
domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint);
506
P.tabs.switchToTab(1/*DOM visibility workaround*/);
507
F.confirmer(btnClear, {
508
/* must come after insertion into the DOM for the pinSize option to work. */
509
pinSize: true,
510
confirmText: "DISCARD all local edits?",
511
onconfirm: function(e){
512
if(P.finfo){
513
const stashed = P.getStashedFinfo(P.finfo);
514
P.clearStash();
515
if(stashed) P.loadFile(/*reload after discarding edits*/);
516
}else{
517
P.clearStash();
518
}
519
},
520
ticks: F.config.confirmerButtonTicks
521
});
522
D.addClass(this.e.btnClear,'hidden' /* must not be set until after confirmer is set up!*/);
523
$stash._fireStashEvent(/*read the page-load-time stash*/);
524
P.tabs.switchToTab(0/*DOM visibility workaround*/);
525
delete this.init;
526
},
527
/**
528
Regenerates the edit selection list.
529
*/
530
updateList: function f(stasher,theFinfo){
531
if(!f.compare){
532
const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1);
533
f.compare = function(l,r){
534
const cmp = cmpBase(l.filename, r.filename);
535
return cmp ? cmp : cmpBase(l.checkin, r.checkin);
536
};
537
f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */;
538
const pad=(x)=>(''+x).length>1 ? x : '0'+x;
539
f.timestring = function ff(d){
540
return [
541
d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()),
542
'@',pad(d.getHours()),':',pad(d.getMinutes())
543
].join('');
544
};
545
}
546
const index = stasher.getIndex(), ilist = [];
547
Object.keys(index).forEach((finfo)=>{
548
ilist.push(index[finfo]);
549
});
550
const self = this;
551
D.clearElement(this.e.select);
552
if(0===ilist.length){
553
D.addClass(this.e.btnClear, 'hidden');
554
D.option(D.disable(this.e.select),undefined,"No local edits");
555
return;
556
}
557
D.enable(this.e.select);
558
D.removeClass(this.e.btnClear, 'hidden');
559
D.disable(D.option(this.e.select,0,"Select a local edit..."));
560
const currentFinfo = theFinfo || P.finfo || {filename:''};
561
ilist.sort(f.compare).forEach(function(finfo,n){
562
const key = stasher.indexKey(finfo),
563
branch = finfo.branch
564
|| P.fileSelectWidget.checkinBranchName(finfo.checkin)||'';
565
/* Remember that we don't know the branch name for non-leaf versions
566
which P.fileSelectWidget() has never seen/cached. */
567
const opt = D.option(
568
self.e.select, n+1/*value is (almost) irrelevant*/,
569
[F.hashDigits(finfo.checkin), ' [',branch||'?branch?','] ',
570
f.timestring(new Date(finfo.stashTime)),' ',
571
false ? finfo.filename : F.shortenFilename(finfo.filename)
572
].join('')
573
);
574
opt._finfo = finfo;
575
if(0===f.compare(currentFinfo, finfo)){
576
D.attr(opt, 'selected', true);
577
}
578
});
579
}
580
}/*P.stashWidget*/;
581
582
/**
583
Internal workaround to select the current preview mode
584
and fire a change event if the value actually changes
585
or if forceEvent is truthy.
586
*/
587
P.selectPreviewMode = function(modeValue, forceEvent){
588
const s = this.e.selectPreviewMode;
589
if(!modeValue) modeValue = s.value;
590
else if(s.value != modeValue){
591
s.value = modeValue;
592
forceEvent = true;
593
}
594
if(forceEvent){
595
// Force UI update
596
s.dispatchEvent(new Event('change',{target:s}));
597
}
598
};
599
600
/**
601
Keep track of how many in-flight AJAX requests there are so we
602
can disable input elements while any are pending. For
603
simplicity's sake we simply disable ALL OF IT while any AJAX is
604
pending, rather than disabling operation-specific UI elements,
605
which would be a huge maintenance hassle.
606
607
Noting, however, that this global on/off is not *quite*
608
pedantically correct. Pedantically speaking. If an element is
609
disabled before an XHR starts, this code "should" notice that and
610
not include it in the to-re-enable list. That would be annoying
611
to do, and becomes impossible to do properly once multiple XHRs
612
are in transit and an element is disabled seprately between two
613
of those in-transit requests (that would be an unlikely, but
614
possible, corner case). As of this writing, the only elements
615
which are ever normally programmatically toggled between
616
enabled/disabled...
617
618
1) Belong to the file selection list and remain disabled until
619
the list of leaves and files are loaded. i.e. they would be
620
disabled *anyway* during their own XHR requests.
621
622
2) The stashWidget's SELECT list when no local edits are
623
stashed. Curiously, the all-or-nothing re-enabling implemented
624
here does not re-enable that particular selection list. That's
625
because of timing, though: that widget is "manually" disabled
626
when the list is empty, and that list is normally emptied in
627
conjunction with an XHR request.
628
*/
629
const ajaxState = {
630
count: 0 /* in-flight F.fetch() requests */,
631
toDisable: undefined /* elements to disable during ajax activity */
632
};
633
F.fetch.beforesend = function f(){
634
if(!ajaxState.toDisable){
635
ajaxState.toDisable = document.querySelectorAll(
636
'button, input, select, textarea'
637
);
638
}
639
if(1===++ajaxState.count){
640
D.addClass(document.body, 'waiting');
641
D.disable(ajaxState.toDisable);
642
}
643
};
644
F.fetch.aftersend = function(){
645
if(0===--ajaxState.count){
646
D.removeClass(document.body, 'waiting');
647
D.enable(ajaxState.toDisable);
648
}
649
};
650
651
F.onPageLoad(function() {
652
P.base = {tag: E('base')};
653
P.base.originalHref = P.base.tag.href;
654
P.tabs = new F.TabManager('#fileedit-tabs');
655
P.e = { /* various DOM elements we work with... */
656
taEditor: E('#fileedit-content-editor'),
657
taCommentSmall: E('#fileedit-comment'),
658
taCommentBig: E('#fileedit-comment-big'),
659
taComment: undefined/*gets set to one of taComment{Big,Small}*/,
660
ajaxContentTarget: E('#ajax-target'),
661
btnCommit: E("#fileedit-btn-commit"),
662
btnReload: E("#fileedit-tab-content button.fileedit-content-reload"),
663
selectPreviewMode: E('#select-preview-mode select'),
664
selectHtmlEmsWrap: E('#select-preview-html-ems'),
665
selectEolWrap: E('#select-eol-style'),
666
selectEol: E('#select-eol-style select[name=eol]'),
667
selectFontSizeWrap: E('#select-font-size'),
668
selectDiffWS: E('select[name=diff_ws]'),
669
cbLineNumbersWrap: E('#cb-line-numbers'),
670
cbAutoPreview: E('#cb-preview-autorefresh'),
671
previewTarget: E('#fileedit-tab-preview-wrapper'),
672
manifestTarget: E('#fileedit-manifest'),
673
diffTarget: E('#fileedit-tab-diff-wrapper'),
674
cbIsExe: E('input[type=checkbox][name=exec_bit]'),
675
cbManifest: E('input[type=checkbox][name=include_manifest]'),
676
editStatus: E('#fileedit-edit-status'),
677
tabs:{
678
content: E('#fileedit-tab-content'),
679
preview: E('#fileedit-tab-preview'),
680
diff: E('#fileedit-tab-diff'),
681
commit: E('#fileedit-tab-commit'),
682
fileSelect: E('#fileedit-tab-fileselect')
683
}
684
};
685
/* Figure out which comment editor to show by default and
686
hide the other one. By default we take the one which does
687
not have the 'hidden' CSS class. If neither do, we default
688
to single-line mode. */
689
if(D.hasClass(P.e.taCommentSmall, 'hidden')){
690
P.e.taComment = P.e.taCommentBig;
691
}else if(D.hasClass(P.e.taCommentBig,'hidden')){
692
P.e.taComment = P.e.taCommentSmall;
693
}else{
694
P.e.taComment = P.e.taCommentSmall;
695
D.addClass(P.e.taCommentBig, 'hidden');
696
}
697
D.removeClass(P.e.taComment, 'hidden');
698
P.tabs.addCustomWidget( E('#fossil-status-bar') ).addCustomWidget(P.e.editStatus);
699
let currentTab/*used for ctrl-enter switch between editor and preview*/;
700
P.tabs.addEventListener(
701
/* Set up auto-refresh of the preview tab... */
702
'before-switch-to', function(ev){
703
currentTab = ev.detail;
704
if(ev.detail===P.e.tabs.preview){
705
P.baseHrefForFile();
706
if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
707
}else if(ev.detail===P.e.tabs.diff){
708
/* Work around a weird bug where the page gets wider than
709
the window when the diff tab is NOT in view and the
710
current SBS diff widget is wider than the window. When
711
the diff IS in view then CSS overflow magically reduces
712
the page size again. Weird. Maybe FF-specific. Note that
713
this weirdness happens even though P.e.diffTarget's parent
714
is hidden (and therefore P.e.diffTarget is also hidden).
715
*/
716
D.removeClass(P.e.diffTarget, 'hidden');
717
}
718
}
719
);
720
P.tabs.addEventListener(
721
/* Set up auto-refresh of the preview tab... */
722
'before-switch-from', function(ev){
723
if(ev.detail===P.e.tabs.preview){
724
P.baseHrefRestore();
725
}else if(ev.detail===P.e.tabs.diff){
726
/* See notes in the before-switch-to handler. */
727
D.addClass(P.e.diffTarget, 'hidden');
728
}
729
}
730
);
731
////////////////////////////////////////////////////////////
732
// Trigger preview on Ctrl-Enter. This only works on the built-in
733
// editor widget, not a client-provided one.
734
P.e.taEditor.addEventListener('keydown',function(ev){
735
if(P.config.shiftEnterPreview && ev.shiftKey && 13===ev.keyCode){
736
ev.preventDefault();
737
ev.stopPropagation();
738
P.e.taEditor.blur(/*force change event, if needed*/);
739
P.tabs.switchToTab(P.e.tabs.preview);
740
if(!P.e.cbAutoPreview.checked){/* If NOT in auto-preview mode, trigger an update. */
741
P.preview();
742
}
743
return false;
744
}
745
}, false);
746
// If we're in the preview tab, have ctrl-enter switch back to the editor.
747
document.body.addEventListener('keydown',function(ev){
748
if(ev.shiftKey && 13 === ev.keyCode){
749
if(currentTab === P.e.tabs.preview){
750
ev.preventDefault();
751
ev.stopPropagation();
752
P.tabs.switchToTab(P.e.tabs.content);
753
P.e.taEditor.focus(/*doesn't work for client-supplied editor widget!
754
And it's slow as molasses for long docs, as focus()
755
forces a document reflow.*/);
756
return false;
757
}
758
}
759
}, true);
760
761
F.connectPagePreviewers(
762
P.e.tabs.preview.querySelector(
763
'#btn-preview-refresh'
764
)
765
);
766
767
const diffButtons = E('#fileedit-tab-diff-buttons');
768
diffButtons.querySelector('button.sbs').addEventListener(
769
"click",(e)=>P.diff(true), false
770
);
771
diffButtons.querySelector('button.unified').addEventListener(
772
"click",(e)=>P.diff(false), false
773
);
774
P.e.btnCommit.addEventListener(
775
"click",(e)=>P.commit(), false
776
);
777
P.tabs.switchToTab(1/*DOM visibility workaround*/);
778
F.confirmer(P.e.btnReload, {
779
pinSize: true,
780
confirmText: "Really reload, losing edits?",
781
onconfirm: (e)=>P.unstashContent().loadFile(),
782
ticks: F.config.confirmerButtonTicks
783
});
784
E('#comment-toggle').addEventListener(
785
"click",(e)=>P.toggleCommentMode(), false
786
);
787
788
P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
789
P.e.cbIsExe.addEventListener(
790
'change', ()=>P.stashContentChange(true), false
791
);
792
793
/**
794
Cosmetic: jump through some hoops to enable/disable
795
certain preview options depending on the current
796
preview mode...
797
*/
798
P.e.selectPreviewMode.addEventListener(
799
"change", function(e){
800
const mode = e.target.value,
801
name = P.previewModes[mode],
802
hide = [], unhide = [];
803
P.previewModes.current = name;
804
if('guess'===name){
805
unhide.push(P.e.cbLineNumbersWrap,
806
P.e.selectHtmlEmsWrap);
807
}else{
808
if('text'===name) unhide.push(P.e.cbLineNumbersWrap);
809
else hide.push(P.e.cbLineNumbersWrap);
810
if('htmlIframe'===name) unhide.push(P.e.selectHtmlEmsWrap);
811
else hide.push(P.e.selectHtmlEmsWrap);
812
}
813
hide.forEach((e)=>e.classList.add('hidden'));
814
unhide.forEach((e)=>e.classList.remove('hidden'));
815
}, false
816
);
817
P.selectPreviewMode(false, true);
818
const selectFontSize = E('select[name=editor_font_size]');
819
if(selectFontSize){
820
selectFontSize.addEventListener(
821
"change",function(e){
822
const ed = P.e.taEditor;
823
ed.className = ed.className.replace(
824
/\bfont-size-\d+/g, '' );
825
ed.classList.add('font-size-'+e.target.value);
826
}, false
827
);
828
selectFontSize.dispatchEvent(
829
// Force UI update
830
new Event('change',{target:selectFontSize})
831
);
832
}
833
834
P.addEventListener(
835
// Clear certain views when new content is loaded/set
836
'fileedit-content-replaced',
837
()=>{
838
P.previewNeedsUpdate = true;
839
D.clearElement(P.e.diffTarget, P.e.previewTarget, P.e.manifestTarget);
840
}
841
);
842
P.addEventListener(
843
// Clear certain views after a non-dry-run commit
844
'fileedit-committed',
845
(e)=>{
846
if(!e.detail.dryRun){
847
D.clearElement(P.e.diffTarget, P.e.previewTarget);
848
}
849
}
850
);
851
852
P.fileSelectWidget.init();
853
P.stashWidget.init(
854
P.e.tabs.content.lastElementChild
855
);
856
857
const cbEditPreview = E('#edit-shift-enter-preview');
858
cbEditPreview.addEventListener('change', function(e){
859
F.storage.set('edit-shift-enter-preview',
860
P.config.shiftEnterPreview = e.target.checked);
861
}, false);
862
cbEditPreview.checked = P.config.shiftEnterPreview;
863
}/*F.onPageLoad()*/);
864
865
/**
866
Getter (if called with no args) or setter (if passed an arg) for
867
the current file content.
868
869
The setter form sets the content, dispatches a
870
'fileedit-content-replaced' event, and returns this object.
871
*/
872
P.fileContent = function f(){
873
if(0===arguments.length){
874
return f.get();
875
}else{
876
f.set(arguments[0] || '');
877
this.dispatchEvent('fileedit-content-replaced', this);
878
return this;
879
}
880
};
881
/* Default get/set impls for file content */
882
P.fileContent.get = function(){return P.e.taEditor.value};
883
P.fileContent.set = function(content){P.e.taEditor.value = content};
884
885
/**
886
For use when installing a custom editor widget. Pass it the
887
getter and setter callbacks to fetch resp. set the content of the
888
custom widget. They will be triggered via
889
P.fileContent(). Returns this object.
890
*/
891
P.setContentMethods = function(getter, setter){
892
this.fileContent.get = getter;
893
this.fileContent.set = setter;
894
return this;
895
};
896
897
/**
898
Alerts the editor app that a "change" has happened in the editor.
899
When connecting 3rd-party editor widgets to this app, it is (or
900
may be) necessary to call this for any "change" events the widget
901
emits. Whether or not "change" means that there were "really"
902
edits is irrelevant.
903
904
This function may perform an arbitrary amount of work, so it
905
should not be called for every keypress within the editor
906
widget. Calling it for "blur" events is generally sufficient, and
907
calling it for each Enter keypress is generally reasonable but
908
also computationally costly.
909
*/
910
P.notifyOfChange = function(){
911
P.stashContentChange();
912
};
913
914
/**
915
Removes the default editor widget (and any dependent elements)
916
from the DOM, adds the given element in its place, removes this
917
method from this object, and returns this object.
918
*/
919
P.replaceEditorElement = function(newEditor){
920
P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor);
921
P.e.taEditor.remove();
922
P.e.selectFontSizeWrap.remove();
923
delete this.replaceEditorElement;
924
return P;
925
};
926
927
/**
928
If either of...
929
930
- P.previewModes.current==='wiki'
931
932
- P.previewModes.current==='guess' AND the currently-loaded file
933
has a mimetype of "text/x-fossil-wiki" or "text/x-markdown".
934
935
... then this function updates the document's base.href to a
936
repo-relative /doc/{{this.finfo.checkin}}/{{directory part of
937
this.finfo.filename}}/
938
939
If neither of those conditions applies, this is a no-op.
940
*/
941
P.baseHrefForFile = function f(){
942
const fn = this.finfo ? this.finfo.filename : undefined;
943
if(!fn) return this;
944
if(!f.wikiMimeTypes){
945
f.wikiMimeTypes = ["text/x-fossil-wiki", "text/x-markdown"];
946
}
947
if('wiki'===P.previewModes.current
948
|| ('guess'===P.previewModes.current
949
&& f.wikiMimeTypes.indexOf(this.finfo.mimetype)>=0)){
950
const a = fn.split('/');
951
a.pop();
952
this.base.tag.href = F.repoUrl(
953
'doc/'+F.hashDigits(this.finfo.checkin)
954
+'/'+(a.length ? a.join('/')+'/' : '')
955
);
956
}
957
return this;
958
};
959
960
/**
961
Sets the document's base.href value to its page-load-time
962
setting.
963
*/
964
P.baseHrefRestore = function(){
965
P.base.tag.href = P.base.originalHref;
966
};
967
968
/**
969
Toggles between single- and multi-line comment
970
mode.
971
*/
972
P.toggleCommentMode = function(){
973
var s, h, c = this.e.taComment.value;
974
if(this.e.taComment === this.e.taCommentSmall){
975
s = this.e.taCommentBig;
976
h = this.e.taCommentSmall;
977
}else{
978
s = this.e.taCommentSmall;
979
h = this.e.taCommentBig;
980
/*
981
Doing (input[type=text].value = textarea.value) unfortunately
982
strips all newlines. To compensate we'll replace each EOL with
983
a space. Not ideal. If we were to instead escape them as \n,
984
and do the reverse when toggling again, then they would get
985
committed as escaped newlines if the user did not first switch
986
back to multi-line mode. We cannot blindly unescape the
987
newlines, in the off chance that the user actually enters \n
988
in the comment.
989
*/
990
c = c.replace(/\r?\n/g,' ');
991
}
992
s.value = c;
993
this.e.taComment = s;
994
D.addClass(h, 'hidden');
995
D.removeClass(s, 'hidden');
996
};
997
998
/**
999
Returns true if fossil.page.finfo is set, indicating that a file
1000
has been loaded, else it reports an error and returns false.
1001
1002
If passed a truthy value any error message about not having
1003
a file loaded is suppressed.
1004
*/
1005
const affirmHasFile = function(quiet){
1006
if(!P.finfo){
1007
if(!quiet) F.error("No file is loaded.");
1008
}
1009
return !!P.finfo;
1010
};
1011
1012
/**
1013
updateVersion() updates the filename and version in various UI
1014
elements...
1015
1016
Returns this object.
1017
*/
1018
P.updateVersion = function f(file,rev){
1019
if(!f.eLinks){
1020
f.eName = P.e.editStatus.querySelector('span.name');
1021
f.eLinks = P.e.editStatus.querySelector('span.links');
1022
}
1023
if(1===arguments.length){/*assume object*/
1024
this.finfo = arguments[0];
1025
file = this.finfo.filename;
1026
rev = this.finfo.checkin;
1027
}else if(0===arguments.length){
1028
if(affirmHasFile()){
1029
file = this.finfo.filename;
1030
rev = this.finfo.checkin;
1031
}
1032
}else{
1033
this.finfo = {filename:file,checkin:rev};
1034
}
1035
const fi = this.finfo;
1036
D.clearElement(f.eName, f.eLinks);
1037
if(!fi){
1038
D.append(f.eName, '(no file loaded)');
1039
return this;
1040
}
1041
const rHuman = F.hashDigits(rev),
1042
rUrl = F.hashDigits(rev,true);
1043
1044
//TODO? port over is-edited marker from /wikiedit
1045
//var marker = getEditMarker(wi, false);
1046
D.append(f.eName/*,marker*/,D.a(F.repoUrl('finfo',{name:file, m:rUrl}), file));
1047
1048
D.append(
1049
f.eLinks,
1050
D.append(D.span(), fi.mimetype||'?mimetype?'),
1051
D.a(F.repoUrl('info/'+rUrl), rHuman),
1052
D.a(F.repoUrl('timeline',{m:rUrl}), "timeline"),
1053
D.a(F.repoUrl('annotate',{filename:file, checkin:rUrl}),'annotate'),
1054
D.a(F.repoUrl('blame',{filename:file, checkin:rUrl}),'blame')
1055
);
1056
const purlArgs = F.encodeUrlArgs({
1057
filename: this.finfo.filename,
1058
checkin: rUrl
1059
},false,true);
1060
const purl = F.repoUrl('fileedit',purlArgs);
1061
D.append( f.eLinks, D.a(purl,"editor permalink") );
1062
this.setPageTitle("Edit: "+fi.filename);
1063
return this;
1064
};
1065
1066
/**
1067
loadFile() loads (file,checkinVersion) and updates the relevant
1068
UI elements to reflect the loaded state. If passed no arguments
1069
then it re-uses the values from the currently-loaded file, reloading
1070
it (emitting an error message if no file is loaded).
1071
1072
Returns this object, noting that the load is async. After loading
1073
it triggers a 'fileedit-file-loaded' event, passing it
1074
this.finfo.
1075
1076
If a locally-edited copy of the given file/rev is found, that
1077
copy is used instead of one fetched from the server, but it is
1078
still treated as a load event.
1079
1080
Alternate call forms:
1081
1082
- no arguments: re-loads from this.finfo.
1083
1084
- 1 argument: assumed to be an finfo-style object. Must have at
1085
least {filename, checkin} properties, but need not have other
1086
finfo state.
1087
*/
1088
P.loadFile = function(file,rev){
1089
if(0===arguments.length){
1090
/* Reload from this.finfo */
1091
if(!affirmHasFile()) return this;
1092
file = this.finfo.filename;
1093
rev = this.finfo.checkin;
1094
}else if(1===arguments.length){
1095
/* Assume finfo-like object */
1096
const arg = arguments[0];
1097
file = arg.filename;
1098
rev = arg.checkin;
1099
}
1100
const self = this;
1101
const onload = (r,headers)=>{
1102
delete self.finfo;
1103
self.updateVersion({
1104
filename: file,
1105
checkin: rev,
1106
branch: headers['x-fileedit-checkin-branch'],
1107
isExe: ('x'===headers['x-fileedit-file-perm']),
1108
mimetype: headers['content-type'].split(';').shift()
1109
});
1110
self.tabs.switchToTab(self.e.tabs.content);
1111
self.e.cbIsExe.checked = self.finfo.isExe;
1112
self.fileContent(r);
1113
P.previewNeedsUpdate = true;
1114
self.dispatchEvent('fileedit-file-loaded', self.finfo);
1115
};
1116
const semiFinfo = {filename: file, checkin: rev};
1117
const stashFinfo = this.getStashedFinfo(semiFinfo);
1118
if(stashFinfo){ // fake a response from the stash...
1119
this.finfo = stashFinfo;
1120
this.e.cbIsExe.checked = !!stashFinfo.isExe;
1121
onload(this.contentFromStash()||'',{
1122
'x-fileedit-file-perm': stashFinfo.isExe ? 'x' : undefined,
1123
'content-type': stashFinfo.mimetype,
1124
'x-fileedit-checkin-branch': stashFinfo.branch
1125
});
1126
F.message("Fetched from the local-edit storage:",
1127
F.hashDigits(stashFinfo.checkin),
1128
stashFinfo.filename);
1129
return this;
1130
}
1131
F.message(
1132
"Loading content..."
1133
).fetch('fileedit/content',{
1134
urlParams: {
1135
filename:file,
1136
checkin:rev
1137
},
1138
responseHeaders: [
1139
'x-fileedit-file-perm',
1140
'x-fileedit-checkin-branch',
1141
'content-type'],
1142
onload:(r,headers)=>{
1143
onload(r,headers);
1144
F.message('Loaded content for',
1145
F.hashDigits(self.finfo.checkin),
1146
self.finfo.filename);
1147
}
1148
});
1149
return this;
1150
};
1151
1152
/**
1153
Fetches the page preview based on the contents and settings of
1154
this page's input fields, and updates the UI with the
1155
preview.
1156
1157
Returns this object, noting that the operation is async.
1158
*/
1159
P.preview = function f(switchToTab){
1160
if(!affirmHasFile()) return this;
1161
return this._postPreview(this.fileContent(), function(c){
1162
P._previewTo(c);
1163
if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
1164
});
1165
};
1166
1167
/**
1168
Callback for use with F.connectPagePreviewers(). Gets passed
1169
the preview content.
1170
*/
1171
P._previewTo = function(c){
1172
const target = this.e.previewTarget;
1173
D.clearElement(target);
1174
if('string'===typeof c) D.parseHtml(target,c);
1175
if(F.pikchr){
1176
F.pikchr.addSrcView(target.querySelectorAll('svg.pikchr'));
1177
}
1178
};
1179
1180
/**
1181
Callback for use with F.connectPagePreviewers()
1182
*/
1183
P._postPreview = function(content,callback){
1184
if(!affirmHasFile()) return this;
1185
if(!content){
1186
callback(content);
1187
return this;
1188
}
1189
const fd = new FormData();
1190
fd.append('render_mode',this.e.selectPreviewMode.value);
1191
fd.append('filename',this.finfo.filename);
1192
fd.append('ln',E('[name=preview_ln]').checked ? 1 : 0);
1193
fd.append('iframe_height', E('[name=preview_html_ems]').value);
1194
fd.append('content',content || '');
1195
F.message(
1196
"Fetching preview..."
1197
).fetch('ajax/preview-text',{
1198
payload: fd,
1199
responseHeaders: 'x-ajax-render-mode',
1200
onload: (r,header)=>{
1201
P.selectPreviewMode(P.previewModes[header]);
1202
if('wiki'===header) P.baseHrefForFile();
1203
else P.baseHrefRestore();
1204
callback(r);
1205
F.message('Updated preview.');
1206
P.previewNeedsUpdate = false;
1207
P.dispatchEvent('fileedit-preview-updated',{
1208
previewMode: P.previewModes.current,
1209
mimetype: P.finfo.mimetype,
1210
element: P.e.previewTarget
1211
});
1212
},
1213
onerror: (e)=>{
1214
F.fetch.onerror(e);
1215
callback("Error fetching preview: "+e);
1216
}
1217
});
1218
return this;
1219
};
1220
1221
/**
1222
Fetches the content diff based on the contents and settings of
1223
this page's input fields, and updates the UI with the diff view.
1224
1225
Returns this object, noting that the operation is async.
1226
*/
1227
P.diff = function f(sbs){
1228
if(!affirmHasFile()) return this;
1229
const content = this.fileContent(),
1230
self = this,
1231
target = this.e.diffTarget;
1232
const fd = new FormData();
1233
fd.append('filename',this.finfo.filename);
1234
fd.append('checkin', this.finfo.checkin);
1235
fd.append('sbs', sbs ? 1 : 0);
1236
fd.append('content',content);
1237
if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
1238
F.message(
1239
"Fetching diff..."
1240
).fetch('fileedit/diff',{
1241
payload: fd,
1242
onload: function(c){
1243
D.parseHtml(D.clearElement(target),[
1244
"<div>Diff <code>[",
1245
self.finfo.checkin,
1246
"]</code> &rarr; Local Edits</div>",
1247
c||'No changes.'
1248
].join(''));
1249
F.diff.setupDiffContextLoad();
1250
if(sbs) P.tweakSbsDiffs();
1251
F.message('Updated diff.');
1252
self.tabs.switchToTab(self.e.tabs.diff);
1253
}
1254
});
1255
return this;
1256
};
1257
1258
/**
1259
Performs an async commit based on the form contents and updates
1260
the UI.
1261
1262
Returns this object.
1263
*/
1264
P.commit = function f(){
1265
if(!affirmHasFile()) return this;
1266
const self = this;
1267
const content = this.fileContent(),
1268
target = D.clearElement(P.e.manifestTarget),
1269
cbDryRun = E('[name=dry_run]'),
1270
isDryRun = cbDryRun.checked,
1271
filename = this.finfo.filename;
1272
if(!f.onload){
1273
f.onload = function(c){
1274
const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
1275
if(c.manifest){
1276
D.parseHtml(D.clearElement(target), [
1277
"<h3>Manifest",
1278
(c.dryRun?" (dry run)":""),
1279
": ", F.hashDigits(c.checkin),"</h3>",
1280
"<pre><code class='fileedit-manifest'>",
1281
c.manifest.replace(/</g,'&lt;'),
1282
/* ^^^ replace() necessary or this breaks if the manifest
1283
comment contains an unclosed HTML tags,
1284
e.g. <script> */
1285
"</code></pre>"
1286
].join(''));
1287
delete c.manifest/*so we don't stash this with finfo*/;
1288
}
1289
const msg = [
1290
'Committed',
1291
c.dryRun ? '(dry run)' : '',
1292
'[', F.hashDigits(c.checkin) ,'].'
1293
];
1294
if(!c.dryRun){
1295
self.unstashContent(oldFinfo);
1296
self.finfo = c;
1297
self.e.taComment.value = '';
1298
self.updateVersion();
1299
self.fileSelectWidget.loadLeaves();
1300
}
1301
self.dispatchEvent('fileedit-committed', c);
1302
F.message.apply(F, msg);
1303
self.tabs.switchToTab(self.e.tabs.commit);
1304
};
1305
}
1306
const fd = new FormData();
1307
fd.append('filename',filename);
1308
fd.append('checkin', this.finfo.checkin);
1309
fd.append('content',content);
1310
fd.append('dry_run',isDryRun ? 1 : 0);
1311
fd.append('eol', this.e.selectEol.value || 0);
1312
/* Text fields or select lists... */
1313
fd.append('comment', this.e.taComment.value);
1314
if(0){
1315
// Comment mimetype is currently not supported by the UI...
1316
['comment_mimetype'
1317
].forEach(function(name){
1318
var e = E('[name='+name+']');
1319
if(e) fd.append(name,e.value);
1320
});
1321
}
1322
/* Checkboxes: */
1323
['allow_fork',
1324
'allow_older',
1325
'exec_bit',
1326
'allow_merge_conflict',
1327
'include_manifest',
1328
'prefer_delta'
1329
].forEach(function(name){
1330
var e = E('[name='+name+']');
1331
if(e){
1332
fd.append(name, e.checked ? 1 : 0);
1333
}else{
1334
console.error("Missing checkbox? name =",name);
1335
}
1336
});
1337
F.message(
1338
"Checking in..."
1339
).fetch('fileedit/commit',{
1340
payload: fd,
1341
responseType: 'json',
1342
onload: f.onload
1343
});
1344
return this;
1345
};
1346
1347
/**
1348
Updates P.finfo for certain state and stashes P.finfo, with the
1349
current content fetched via P.fileContent().
1350
1351
If passed truthy AND the stash already has stashed content for
1352
the current file, only the stashed finfo record is updated, else
1353
both the finfo and content are updated.
1354
*/
1355
P.stashContentChange = function(onlyFinfo){
1356
if(affirmHasFile(true)){
1357
const fi = this.finfo;
1358
fi.isExe = this.e.cbIsExe.checked;
1359
if(!fi.branch) fi.branch = this.fileSelectWidget.checkinBranchName(fi.checkin);
1360
if(onlyFinfo && $stash.hasStashedContent(fi)){
1361
$stash.updateFile(fi);
1362
}else{
1363
$stash.updateFile(fi, P.fileContent());
1364
}
1365
F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename);
1366
$stash.prune();
1367
this.previewNeedsUpdate = true;
1368
}
1369
return this;
1370
};
1371
1372
/**
1373
Removes any stashed state for the current P.finfo (if set) from
1374
F.storage. Returns this.
1375
*/
1376
P.unstashContent = function(){
1377
const finfo = arguments[0] || this.finfo;
1378
if(finfo){
1379
this.previewNeedsUpdate = true;
1380
$stash.unstash(finfo);
1381
//console.debug("Unstashed",finfo);
1382
F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename);
1383
}
1384
return this;
1385
};
1386
1387
/**
1388
Clears all stashed file state from F.storage. Returns this.
1389
*/
1390
P.clearStash = function(){
1391
$stash.clear();
1392
return this;
1393
};
1394
1395
/**
1396
If stashed content for P.finfo exists, it is returned, else
1397
undefined is returned.
1398
*/
1399
P.contentFromStash = function(){
1400
return affirmHasFile(true) ? $stash.stashedContent(this.finfo) : undefined;
1401
};
1402
1403
/**
1404
If a stashed version of the given finfo object exists (same
1405
filename/checkin values), return it, else return undefined.
1406
*/
1407
P.getStashedFinfo = function(finfo){
1408
return $stash.getFinfo(finfo);
1409
};
1410
1411
P.$stash = $stash /*only for testing/debugging - not part of the API.*/;
1412
1413
})(window.fossil);
1414

Keyboard Shortcuts

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