-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSlyEdit_Misc.js
5141 lines (4784 loc) · 184 KB
/
SlyEdit_Misc.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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
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
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// $Id: SlyEdit_Misc.js,v 1.59 2020/03/04 20:59:50 nightfox Exp $
/* This file declares some general helper functions and variables
* that are used by SlyEdit.
*
* Author: Eric Oulashin (AKA Nightfox)
* BBS: Digital Distortion
* BBS address: digdist.bbsindex.com
*
* Date User Description
* 2009-06-06 Eric Oulashin Started development
* 2009-06-11 Eric Oulashin Taking a break from development
* 2009-08-09 Eric Oulashin Started more development & testing
* 2009-08-22 Eric Oulashin Version 1.00
* Initial public release
* ....Removed some comments...
* 2017-12-16 Eric Oulashin Updated ReadSlyEditConfigFile() to include the
* allowEditQuoteLines option.
* 2017-12-18 Eric Oulashin Update the KEY_PAGE_UP and KEY_PAGE_DOWN keys to
* ensure they mat what's in sbbsdef.js
* 2017-12-24 Eric Oulashin Updated firstNonQuoteTxtIndex() to better handle
* lines with 3 non-space characters before a >, to
* not consider those sequences a quote when using
* author initials. When using author initials,
* SlyEdit considers a quote sequence to only have 2
* non-space characters (such as "EO>").
* 2017-12-25 Eric Oulashin Updated wrapTextLines() - Added an optional
* parameter for the lineInfo object array so it
* can be updated when lines are split (for quoting
* with author initials). That should fix an
* issue where some wrapped/split quote lines
* were missing the quote line prefix.
* 2017-12-26 Eric Oulashin Updated wrapTextLines() to (hopefully) better
* handle situations when it wraps text into the
* next line when that next line is blank - Ensuring
* it adds a blank line below that.
* 2019-05-04 Eric Oulashin Updated to use require() instead of load() if possible.
* 2020-03-03 Eric Oulashin Updated the postMsgToSubBoard() to ensure the user
* has posting access to the sub-board before posting the
* message.
* 2020-03-04 Eric Oulashin Updated the way postMsgToSubBoard() checks whether
* the user can post in a sub-board by checking the can_post
* property of the sub-board rather than checking the
* ARS. The can_post property covers more cases.
*/
if (typeof(require) === "function")
require("text.js", "Pause");
else
load("text.js");
// Note: These variables are declared with "var" instead of "const" to avoid
// multiple declaration errors when this file is loaded more than once.
// Values for attribute types (for text attribute substitution)
var FORE_ATTR = 1; // Foreground color attribute
var BKG_ATTR = 2; // Background color attribute
var SPECIAL_ATTR = 3; // Special attribute
// Box-drawing/border characters: Single-line
var UPPER_LEFT_SINGLE = "Ú";
var HORIZONTAL_SINGLE = "Ä";
var UPPER_RIGHT_SINGLE = "¿";
var VERTICAL_SINGLE = "³";
var LOWER_LEFT_SINGLE = "À";
var LOWER_RIGHT_SINGLE = "Ù";
var T_SINGLE = "Â";
var LEFT_T_SINGLE = "Ã";
var RIGHT_T_SINGLE = "´";
var BOTTOM_T_SINGLE = "Á";
var CROSS_SINGLE = "Å";
// Box-drawing/border characters: Double-line
var UPPER_LEFT_DOUBLE = "É";
var HORIZONTAL_DOUBLE = "Í";
var UPPER_RIGHT_DOUBLE = "»";
var VERTICAL_DOUBLE = "º";
var LOWER_LEFT_DOUBLE = "È";
var LOWER_RIGHT_DOUBLE = "¼";
var T_DOUBLE = "Ë";
var LEFT_T_DOUBLE = "Ì";
var RIGHT_T_DOUBLE = "¹";
var BOTTOM_T_DOUBLE = "Ê";
var CROSS_DOUBLE = "Î";
// Box-drawing/border characters: Vertical single-line with horizontal double-line
var UPPER_LEFT_VSINGLE_HDOUBLE = "Õ";
var UPPER_RIGHT_VSINGLE_HDOUBLE = "¸";
var LOWER_LEFT_VSINGLE_HDOUBLE = "Ô";
var LOWER_RIGHT_VSINGLE_HDOUBLE = "¾";
// Other special characters
var DOT_CHAR = "ú";
var CHECK_CHAR = "û";
var THIN_RECTANGLE_LEFT = "Ý";
var THIN_RECTANGLE_RIGHT = "Þ";
var BLOCK1 = "°"; // Dimmest block
var BLOCK2 = "±";
var BLOCK3 = "²";
var BLOCK4 = "Û"; // Brightest block
// Navigational keys
var UP_ARROW = "";
var DOWN_ARROW = "";
// CTRL keys
var CTRL_A = "\x01";
var CTRL_B = "\x02";
//var KEY_HOME = CTRL_B;
var CTRL_C = "\x03";
var CTRL_D = "\x04";
var CTRL_E = "\x05";
//var KEY_END = CTRL_E;
var CTRL_F = "\x06";
//var KEY_RIGHT = CTRL_F;
var CTRL_G = "\x07";
var BEEP = CTRL_G;
var CTRL_H = "\x08";
var BACKSPACE = CTRL_H;
var CTRL_I = "\x09";
var TAB = CTRL_I;
var CTRL_J = "\x0a";
//var KEY_DOWN = CTRL_J;
var CTRL_K = "\x0b";
var CTRL_L = "\x0c";
var INSERT_LINE = CTRL_L;
var CTRL_M = "\x0d";
var CR = CTRL_M;
var KEY_ENTER = CTRL_M;
var CTRL_N = "\x0e";
var CTRL_O = "\x0f";
var CTRL_P = "\x10";
var CTRL_Q = "\x11";
var XOFF = CTRL_Q;
var CTRL_R = "\x12";
var CTRL_S = "\x13";
var XON = CTRL_S;
var CTRL_T = "\x14";
var CTRL_U = "\x15";
var CTRL_V = "\x16";
var KEY_INSERT = CTRL_V;
var CTRL_W = "\x17";
var CTRL_X = "\x18";
var CTRL_Y = "\x19";
var CTRL_Z = "\x1a";
var KEY_ESC = "\x1b";
var KEY_F1 = "\1F1";
var KEY_F2 = "\1F2";
var KEY_F3 = "\1F3";
var KEY_F4 = "\1F4";
var KEY_F5 = "\1F5";
// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017
// use CTRL-P and CTRL-N for PageUp and PageDown, respectively. sbbsdefs.js
// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names
// in this script so that this script will work with Synchronet systems before
// and after the update containing those key definitions.
var KEY_PAGE_UP = CTRL_P;
var KEY_PAGE_DOWN = CTRL_N;
// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js
// for KEY_PAGEUP and KEY_PAGEDN in case they change. Note that this relies
// on sbbsdefs.js being loaded; SlyEdit.js loads sbbsdefs.js before this file,
// so this should work.
if (typeof(KEY_PAGEUP) === "string")
KEY_PAGE_UP = KEY_PAGEUP;
if (typeof(KEY_PAGEDN) === "string")
KEY_PAGE_DOWN = KEY_PAGEDN;
// ESC menu action codes to be returned
var ESC_MENU_SAVE = 0;
var ESC_MENU_ABORT = 1;
var ESC_MENU_INS_OVR_TOGGLE = 2;
var ESC_MENU_SYSOP_IMPORT_FILE = 3;
var ESC_MENU_SYSOP_EXPORT_FILE = 4;
var ESC_MENU_FIND_TEXT = 5;
var ESC_MENU_HELP_COMMAND_LIST = 6;
var ESC_MENU_HELP_GENERAL = 7;
var ESC_MENU_HELP_PROGRAM_INFO = 8;
var ESC_MENU_EDIT_MESSAGE = 9;
var ESC_MENU_CROSS_POST_MESSAGE = 10;
var ESC_MENU_LIST_TEXT_REPLACEMENTS = 11;
var ESC_MENU_USER_SETTINGS = 12;
var ESC_MENU_SPELL_CHECK = 13;
var COPYRIGHT_YEAR = 2020;
// Store the full path & filename of the Digital Distortion Message
// Lister, since it will be used more than once.
var gDDML_DROP_FILE_NAME = system.node_dir + "DDML_SyncSMBInfo.txt";
var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".SlyEdit_Settings";
///////////////////////////////////////////////////////////////////////////////////
// Object/class stuff
//////
// TextLine stuff
// TextLine object constructor: This is used to keep track of a text line,
// and whether it has a hard newline at the end (i.e., if the user pressed
// enter to break the line).
//
// Parameters (all optional):
// pText: The text for the line
// pHardNewlineEnd: Whether or not the line has a "hard newline" - What
// this means is that text below it won't be wrapped up
// to this line when re-adjusting the text lines.
// pIsQuoteLine: Whether or not the line is a quote line.
function TextLine(pText, pHardNewlineEnd, pIsQuoteLine)
{
this.text = ""; // The line text
this.hardNewlineEnd = false; // Whether or not the line has a hard newline at the end
this.isQuoteLine = false; // Whether or not this is a quote line
// Copy the parameters if they are valid.
if ((pText != null) && (typeof(pText) == "string"))
this.text = pText;
if ((pHardNewlineEnd != null) && (typeof(pHardNewlineEnd) == "boolean"))
this.hardNewlineEnd = pHardNewlineEnd;
if ((pIsQuoteLine != null) && (typeof(pIsQuoteLine) == "boolean"))
this.isQuoteLine = pIsQuoteLine;
// NEW & EXPERIMENTAL:
// For color support
this.attrs = new Array(); // An array of attributes for the line
// Functions
this.length = TextLine_Length;
this.print = TextLine_Print;
this.doMacroTxtReplacement = TextLine_doMacroTxtReplacement;
this.getWord = TextLine_getWord;
}
// For the TextLine class: Returns the length of the text.
function TextLine_Length()
{
return this.text.length;
}
// For the TextLine class: Prints the text line, using its text attributes.
//
// Parameters:
// pClearToEOL: Boolean - Whether or not to clear to the end of the line
function TextLine_Print(pClearToEOL)
{
console.print(this.text);
if (pClearToEOL)
console.cleartoeol();
}
// Performs text replacement (AKA macro replacement) in the text line.
//
// Parameters:
// pTxtReplacements: An associative array of text to be replaced (i.e.,
// gTxtReplacements)
// pCharIndex: The current character index in the text line
// pUseRegex: Whether or not to treat the text replacement search string as a
// regular expression.
//
// Return value: An object containing the following properties:
// textLineIndex: The updated text line index (integer)
// wordLenDiff: The change in length of the word that
// was replaced (integer)
// wordStartIdx: The index of the first character in the word.
// Only valid if a word was found. Otherwise, this
// will be 0.
// newTextEndIdx: The index of the last character in the new
// text. Only valid if a word was replaced.
// Otherwise, this will be 0.
// newTextLen: The length of the new text in the string. Will be
// the length of the existing word if the word wasn't
// replaced or 0 if no word was found.
// madeTxtReplacement: Whether or not a text replacement was made
// (boolean)
function TextLine_doMacroTxtReplacement(pTxtReplacements, pCharIndex, pUseRegex)
{
var retObj = {
textLineIndex: pCharIndex,
wordLenDiff: 0,
wordStartIdx: 0,
newTextEndIdx: 0,
newTextLen: 0,
madeTxtReplacement: false
};
var wordObj = this.getWord(retObj.textLineIndex);
if (wordObj.foundWord)
{
retObj.wordStartIdx = wordObj.startIdx;
retObj.newTextLen = wordObj.word.length;
// See if the word starts with a capital letter; if so, we'll capitalize
// the replacement word.
var firstCharUpper = false;
var txtReplacement = "";
if (pUseRegex)
{
// Since a regular expression might have more characters in addition
// to the actual word, we need to go through all the replacement strings
// in pTxtReplacements and use the first one that changes the text.
for (var prop in pTxtReplacements)
{
if (pTxtReplacements.hasOwnProperty(prop))
{
var regex = new RegExp(prop);
txtReplacement = wordObj.word.replace(regex, pTxtReplacements[prop]);
retObj.madeTxtReplacement = (txtReplacement != wordObj.word);
// If a text replacement was made, then check and see if the first
// letter in the original text was uppercase, and if so, make the
// first letter in the new text (txtReplacement) uppercase.
if (retObj.madeTxtReplacement)
{
if (firstLetterIsUppercase(wordObj.word))
{
var letterInfo = getFirstLetterFromStr(txtReplacement);
if (letterInfo.idx > -1)
{
txtReplacement = txtReplacement.substr(0, letterInfo.idx)
+ letterInfo.letter.toUpperCase()
+ txtReplacement.substr(letterInfo.idx+1);
}
}
// Now that we've made a text replacement, stop going through
// pTxtReplacements looking for a matching regex.
break;
}
}
}
}
else
{
// Not using a regular expression.
firstCharUpper = (wordObj.word.charAt(0) == wordObj.word.charAt(0).toUpperCase());
// Convert the word to all uppercase to do the case-insensitive lookup
// in pTxtReplacements.
wordObj.word = wordObj.word.toUpperCase();
if (pTxtReplacements.hasOwnProperty(wordObj.word))
{
txtReplacement = pTxtReplacements[wordObj.word];
retObj.madeTxtReplacement = true;
}
}
if (retObj.madeTxtReplacement)
{
if (firstCharUpper)
txtReplacement = txtReplacement.charAt(0).toUpperCase() + txtReplacement.substr(1);
this.text = this.text.substr(0, wordObj.startIdx) + txtReplacement + this.text.substr(wordObj.endIndex+1);
// Based on the difference in word length, update the data that
// matters (retObj.textLineIndex, which keeps track of the index of the current line).
// Note: The horizontal cursor position variable should be replaced after calling this
// function.
retObj.wordLenDiff = txtReplacement.length - wordObj.word.length;
retObj.textLineIndex += retObj.wordLenDiff;
retObj.newTextEndIdx = wordObj.endIndex + retObj.wordLenDiff;
retObj.newTextLen = txtReplacement.length;
}
}
return retObj;
}
// Returns the word in a text line at a given index. If the index
// is at a space, then this function will return the word before
// (to the left of) the space.
//
// Parameters:
// pEditLinesIndex: The index of the line to look at (0-based)
// pCharIndex: The character index in the text line (0-based)
//
// Return value: An object containing the following properties:
// foundWord: Whether or not a word was found (boolean)
// word: The word in the edit line at the given indexes (text).
// This might include control/color codes, etc..
// plainWord: The word in the edit line without any control
// or color codes, etc. This may or may not be
// the same as word.
// startIdx: The index of the first character of the word (integer)
// endIndex: The index of the last character of the word (integer)
// This includes any control/color codes, etc.
function TextLine_getWord(pCharIndex)
{
var retObj = {
foundWord: false,
word: "",
plainWord: "",
startIdx: 0,
endIndex: 0
};
// Parameter checking
if ((pCharIndex < 0) || (pCharIndex >= this.text.length))
return retObj;
// If pCharIndex specifies the index of a space, then look for a non-space
// character before it.
var charIndex = pCharIndex;
while (this.text.charAt(charIndex) == " ")
--charIndex;
// Look for the start & end of the word based on the indexes of a space
// before and at/after the given character index.
var wordStartIdx = charIndex;
var wordEndIdx = charIndex;
while ((this.text.charAt(wordStartIdx) != " ") && (wordStartIdx >= 0))
--wordStartIdx;
++wordStartIdx;
while ((this.text.charAt(wordEndIdx) != " ") && (wordEndIdx < this.text.length))
++wordEndIdx;
--wordEndIdx;
retObj.foundWord = true;
retObj.startIdx = wordStartIdx;
retObj.endIndex = wordEndIdx;
retObj.word = this.text.substring(wordStartIdx, wordEndIdx+1);
retObj.plainWord = strip_ctrl(retObj.word);
return retObj;
}
// AbortConfirmFuncParams constructor: This object contains parameters used by
// the abort confirmation function (actually, there are separate ones for
// IceEdit and DCT Edit styles).
function AbortConfirmFuncParams()
{
this.editTop = gEditTop;
this.editBottom = gEditBottom;
this.editWidth = gEditWidth;
this.editHeight = gEditHeight;
this.editLinesIndex = gEditLinesIndex;
this.displayMessageRectangle = displayMessageRectangle;
}
//////
// ChoiceScrollbox stuff
// Returns the minimum width for a ChoiceScrollbox
function ChoiceScrollbox_MinWidth()
{
return 73; // To leave room for the navigation text in the bottom border
}
// ChoiceScrollbox constructor
//
// Parameters:
// pLeftX: The horizontal component (column) of the upper-left coordinate
// pTopY: The vertical component (row) of the upper-left coordinate
// pWidth: The width of the box (including the borders)
// pHeight: The height of the box (including the borders)
// pTopBorderText: The text to include in the top border
// pSlyEdCfgObj: The SlyEdit configuration object (color settings are used)
// pAddTCharsAroundTopText: Optional, boolean - Whether or not to use left & right T characters
// around the top border text. Defaults to true.
// pReplaceTopTextSpacesWithBorderChars: Optional, boolean - Whether or not to replace
// spaces in the top border text with border characters.
// Defaults to false.
function ChoiceScrollbox(pLeftX, pTopY, pWidth, pHeight, pTopBorderText, pSlyEdCfgObj,
pAddTCharsAroundTopText, pReplaceTopTextSpacesWithBorderChars)
{
// The default is to add left & right T characters around the top border
// text. But also use pAddTCharsAroundTopText if it's a boolean.
var addTopTCharsAroundText = true;
if (typeof(pAddTCharsAroundTopText) == "boolean")
addTopTCharsAroundText = pAddTCharsAroundTopText;
// If pReplaceTopTextSpacesWithBorderChars is true, then replace the spaces
// in pTopBorderText with border characters.
if (pReplaceTopTextSpacesWithBorderChars)
{
var startIdx = 0;
var firstSpcIdx = pTopBorderText.indexOf(" ", 0);
// Look for the first non-space after firstSpaceIdx
var nonSpcIdx = -1;
for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i)
{
if (pTopBorderText.charAt(i) != " ")
nonSpcIdx = i;
}
var firstStrPart = "";
var lastStrPart = "";
var numSpaces = 0;
while ((firstSpcIdx > -1) && (nonSpcIdx > -1))
{
firstStrPart = pTopBorderText.substr(startIdx, (firstSpcIdx-startIdx));
lastStrPart = pTopBorderText.substr(nonSpcIdx);
numSpaces = nonSpcIdx - firstSpcIdx;
if (numSpaces > 0)
{
pTopBorderText = firstStrPart + "\1n" + pSlyEdCfgObj.genColors.listBoxBorder;
for (var i = 0; i < numSpaces; ++i)
pTopBorderText += HORIZONTAL_SINGLE;
pTopBorderText += "\1n" + pSlyEdCfgObj.genColors.listBoxBorderText + lastStrPart;
}
// Look for the next space and non-space character after that.
firstSpcIdx = pTopBorderText.indexOf(" ", nonSpcIdx);
// Look for the first non-space after firstSpaceIdx
nonSpcIdx = -1;
for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i)
{
if (pTopBorderText.charAt(i) != " ")
nonSpcIdx = i;
}
}
}
this.SlyEdCfgObj = pSlyEdCfgObj;
var minWidth = ChoiceScrollbox_MinWidth();
this.dimensions = new Object();
this.dimensions.topLeftX = pLeftX;
this.dimensions.topLeftY = pTopY;
// Make sure the width is the minimum width
if ((pWidth < 0) || (pWidth < minWidth))
this.dimensions.width = minWidth;
else
this.dimensions.width = pWidth;
this.dimensions.height = pHeight;
this.dimensions.bottomRightX = this.dimensions.topLeftX + this.dimensions.width - 1;
this.dimensions.bottomRightY = this.dimensions.topLeftY + this.dimensions.height - 1;
// The text item array and member variables relating to it and the items
// displayed on the screen during the input loop
this.txtItemList = new Array();
this.chosenTextItemIndex = -1;
this.topItemIndex = 0;
this.bottomItemIndex = 0;
// Top border string
var innerBorderWidth = this.dimensions.width - 2;
// Calculate the maximum top border text length to account for the left/right
// T chars and "Page #### of ####" text
var maxTopBorderTextLen = innerBorderWidth - (pAddTCharsAroundTopText ? 21 : 19);
if (strip_ctrl(pTopBorderText).length > maxTopBorderTextLen)
pTopBorderText = pTopBorderText.substr(0, maxTopBorderTextLen);
this.topBorder = "\1n" + pSlyEdCfgObj.genColors.listBoxBorder + UPPER_LEFT_SINGLE;
if (addTopTCharsAroundText)
this.topBorder += RIGHT_T_SINGLE;
this.topBorder += "\1n" + pSlyEdCfgObj.genColors.listBoxBorderText
+ pTopBorderText + "\1n" + pSlyEdCfgObj.genColors.listBoxBorder;
if (addTopTCharsAroundText)
this.topBorder += LEFT_T_SINGLE;
const topBorderTextLen = strip_ctrl(pTopBorderText).length;
var numHorizBorderChars = innerBorderWidth - topBorderTextLen - 20;
if (addTopTCharsAroundText)
numHorizBorderChars -= 2;
for (var i = 0; i <= numHorizBorderChars; ++i)
this.topBorder += HORIZONTAL_SINGLE;
this.topBorder += RIGHT_T_SINGLE + "\1n" + pSlyEdCfgObj.genColors.listBoxBorderText
+ "Page 1 of 1" + "\1n" + pSlyEdCfgObj.genColors.listBoxBorder + LEFT_T_SINGLE
+ UPPER_RIGHT_SINGLE;
// Bottom border string
this.btmBorderNavText = "\1n\1h\1c\1b, \1c\1b, \1cN\1y)\1bext, \1cP\1y)\1brev, "
+ "\1cF\1y)\1birst, \1cL\1y)\1bast, \1cHOME\1b, \1cEND\1b, \1cEnter\1y=\1bSelect, "
+ "\1cESC\1n\1c/\1h\1cQ\1y=\1bEnd";
this.bottomBorder = "\1n" + pSlyEdCfgObj.genColors.listBoxBorder + LOWER_LEFT_SINGLE
+ RIGHT_T_SINGLE + this.btmBorderNavText + "\1n" + pSlyEdCfgObj.genColors.listBoxBorder
+ LEFT_T_SINGLE;
var numCharsRemaining = this.dimensions.width - strip_ctrl(this.btmBorderNavText).length - 6;
for (var i = 0; i < numCharsRemaining; ++i)
this.bottomBorder += HORIZONTAL_SINGLE;
this.bottomBorder += LOWER_RIGHT_SINGLE;
// Item format strings
this.listIemFormatStr = "\1n" + pSlyEdCfgObj.genColors.listBoxItemText + "%-"
+ +(this.dimensions.width-2) + "s";
this.listIemHighlightFormatStr = "\1n" + pSlyEdCfgObj.genColors.listBoxItemHighlight + "%-"
+ +(this.dimensions.width-2) + "s";
// Key functionality override function pointers
this.enterKeyOverrideFn = null;
// inputLoopeExitKeys is an object containing additional keypresses that will
// exit the input loop.
this.inputLoopExitKeys = {};
// For drawing the menu
this.pageNum = 0;
this.numPages = 0;
this.numItemsPerPage = 0;
this.maxItemWidth = 0;
this.pageNumTxtStartX = 0;
// Object functions
this.addTextItem = ChoiceScrollbox_AddTextItem; // Returns the index of the item
this.getTextItem = ChoiceScrollbox_GetTextIem;
this.replaceTextItem = ChoiceScrollbox_ReplaceTextItem;
this.delTextItem = ChoiceScrollbox_DelTextItem;
this.chgCharInTextItem = ChoiceScrollbox_ChgCharInTextItem;
this.getChosenTextItemIndex = ChoiceScrollbox_GetChosenTextItemIndex;
this.setItemArray = ChoiceScrollbox_SetItemArray; // Sets the item array; returns whether or not it was set.
this.clearItems = ChoiceScrollbox_ClearItems; // Empties the array of items
this.setEnterKeyOverrideFn = ChoiceScrollbox_SetEnterKeyOverrideFn;
this.clearEnterKeyOverrideFn = ChoiceScrollbox_ClearEnterKeyOverrideFn;
this.addInputLoopExitKey = ChoiceScrollbox_AddInputLoopExitKey;
this.setBottomBorderText = ChoiceScrollbox_SetBottomBorderText;
this.drawBorder = ChoiceScrollbox_DrawBorder;
this.drawInnerMenu = ChoiceScrollbox_DrawInnerMenu;
this.refreshOnScreen = ChoiceScrollbox_RefreshOnScreen;
this.refreshItemCharOnScreen = ChoiceScrollbox_RefreshItemCharOnScreen;
// Does the input loop. Returns an object with the following properties:
// itemWasSelected: Boolean - Whether or not an item was selected
// selectedIndex: The index of the selected item
// selectedItem: The text of the selected item
// lastKeypress: The last key pressed by the user
this.doInputLoop = ChoiceScrollbox_DoInputLoop;
}
function ChoiceScrollbox_AddTextItem(pTextLine, pStripCtrl)
{
var stripCtrl = true;
if (typeof(pStripCtrl) == "boolean")
stripCtrl = pStripCtrl;
if (stripCtrl)
this.txtItemList.push(strip_ctrl(pTextLine));
else
this.txtItemList.push(pTextLine);
// Return the index of the added item
return this.txtItemList.length-1;
}
function ChoiceScrollbox_GetTextIem(pItemIndex)
{
if (typeof(pItemIndex) != "number")
return "";
if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length))
return "";
return this.txtItemList[pItemIndex];
}
function ChoiceScrollbox_ReplaceTextItem(pItemIndexOrStr, pNewItem)
{
if (typeof(pNewItem) != "string")
return false;
// Find the item index
var itemIndex = -1;
if (typeof(pItemIndexOrStr) == "number")
{
if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
return false;
else
itemIndex = pItemIndexOrStr;
}
else if (typeof(pItemIndexOrStr) == "string")
{
itemIndex = -1;
for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
{
if (this.txtItemList[i] == pItemIndexOrStr)
itemIndex = i;
}
}
else
return false;
// Replace the item
var replacedIt = false;
if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
{
this.txtItemList[itemIndex] = pNewItem;
replacedIt = true;
}
return replacedIt;
}
function ChoiceScrollbox_DelTextItem(pItemIndexOrStr)
{
// Find the item index
var itemIndex = -1;
if (typeof(pItemIndexOrStr) == "number")
{
if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
return false;
else
itemIndex = pItemIndexOrStr;
}
else if (typeof(pItemIndexOrStr) == "string")
{
itemIndex = -1;
for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
{
if (this.txtItemList[i] == pItemIndexOrStr)
itemIndex = i;
}
}
else
return false;
// Remove the item
var removedIt = false;
if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
{
this.txtItemList = this.txtItemList.splice(itemIndex, 1);
removedIt = true;
}
return removedIt;
}
function ChoiceScrollbox_ChgCharInTextItem(pItemIndexOrStr, pStrIndex, pNewText)
{
// Find the item index
var itemIndex = -1;
if (typeof(pItemIndexOrStr) == "number")
{
if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
return false;
else
itemIndex = pItemIndexOrStr;
}
else if (typeof(pItemIndexOrStr) == "string")
{
itemIndex = -1;
for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
{
if (this.txtItemList[i] == pItemIndexOrStr)
itemIndex = i;
}
}
else
return false;
// Change the character in the item
var changedIt = false;
if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
{
this.txtItemList[itemIndex] = chgCharInStr(this.txtItemList[itemIndex], pStrIndex, pNewText);
changedIt = true;
}
return changedIt;
}
function ChoiceScrollbox_GetChosenTextItemIndex()
{
return this.chosenTextItemIndex;
}
function ChoiceScrollbox_SetItemArray(pArray, pStripCtrl)
{
var safeToSet = false;
if (Object.prototype.toString.call(pArray) === "[object Array]")
{
if (pArray.length > 0)
safeToSet = (typeof(pArray[0]) == "string");
else
safeToSet = true; // It's safe to set an empty array
}
if (safeToSet)
{
delete this.txtItemList;
this.txtItemList = pArray;
var stripCtrl = true;
if (typeof(pStripCtrl) == "boolean")
stripCtrl = pStripCtrl;
if (stripCtrl)
{
// Remove attribute/color characters from the text lines in the array
for (var i = 0; i < this.txtItemList.length; ++i)
this.txtItemList[i] = strip_ctrl(this.txtItemList[i]);
}
}
return safeToSet;
}
function ChoiceScrollbox_ClearItems()
{
this.txtItemList.length = 0;
}
function ChoiceScrollbox_SetEnterKeyOverrideFn(pOverrideFn)
{
if (Object.prototype.toString.call(pOverrideFn) == "[object Function]")
this.enterKeyOverrideFn = pOverrideFn;
}
function ChoiceScrollbox_ClearEnterKeyOverrideFn()
{
this.enterKeyOverrideFn = null;
}
function ChoiceScrollbox_AddInputLoopExitKey(pKeypress)
{
this.inputLoopExitKeys[pKeypress] = true;
}
function ChoiceScrollbox_SetBottomBorderText(pText, pAddTChars, pAutoStripIfTooLong)
{
if (typeof(pText) != "string")
return;
const innerWidth = (pAddTChars ? this.dimensions.width-4 : this.dimensions.width-2);
if (pAutoStripIfTooLong)
{
if (strip_ctrl(pText).length > innerWidth)
pText = pText.substr(0, innerWidth);
}
// Re-build the bottom border string based on the new text
this.bottomBorder = "n" + this.SlyEdCfgObj.genColors.listBoxBorder + LOWER_LEFT_SINGLE;
if (pAddTChars)
this.bottomBorder += RIGHT_T_SINGLE;
if (pText.indexOf("n") != 0)
this.bottomBorder += "n";
this.bottomBorder += pText + "n" + this.SlyEdCfgObj.genColors.listBoxBorder;
if (pAddTChars)
this.bottomBorder += LEFT_T_SINGLE;
var numCharsRemaining = this.dimensions.width - strip_ctrl(this.bottomBorder).length - 3;
for (var i = 0; i < numCharsRemaining; ++i)
this.bottomBorder += HORIZONTAL_SINGLE;
this.bottomBorder += LOWER_RIGHT_SINGLE;
}
function ChoiceScrollbox_DrawBorder()
{
console.gotoxy(this.dimensions.topLeftX, this.dimensions.topLeftY);
console.print(this.topBorder);
// Draw the side border characters
var screenRow = this.dimensions.topLeftY + 1;
for (var screenRow = this.dimensions.topLeftY+1; screenRow <= this.dimensions.bottomRightY-1; ++screenRow)
{
console.gotoxy(this.dimensions.topLeftX, screenRow);
console.print(VERTICAL_SINGLE);
console.gotoxy(this.dimensions.bottomRightX, screenRow);
console.print(VERTICAL_SINGLE);
}
// Draw the bottom border
console.gotoxy(this.dimensions.topLeftX, this.dimensions.bottomRightY);
console.print(this.bottomBorder);
}
function ChoiceScrollbox_DrawInnerMenu(pSelectedIndex)
{
var selectedIndex = (typeof(pSelectedIndex) == "number" ? pSelectedIndex : -1);
var startArrIndex = this.pageNum * this.numItemsPerPage;
var endArrIndex = startArrIndex + this.numItemsPerPage;
if (endArrIndex > this.txtItemList.length)
endArrIndex = this.txtItemList.length;
var selectedItemRow = this.dimensions.topLeftY+1;
var screenY = this.dimensions.topLeftY + 1;
for (var i = startArrIndex; i < endArrIndex; ++i)
{
console.gotoxy(this.dimensions.topLeftX+1, screenY);
if (i == selectedIndex)
{
printf(this.listIemHighlightFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth));
selectedItemRow = screenY;
}
else
printf(this.listIemFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth));
++screenY;
}
// If the current screen row is below the bottom row inside the box,
// continue and write blank lines to the bottom of the inside of the box
// to blank out any text that might still be there.
while (screenY < this.dimensions.topLeftY+this.dimensions.height-1)
{
console.gotoxy(this.dimensions.topLeftX+1, screenY);
printf(this.listIemFormatStr, "");
++screenY;
}
// Update the page number in the top border of the box.
console.gotoxy(this.pageNumTxtStartX, this.dimensions.topLeftY);
printf("\1n" + this.SlyEdCfgObj.genColors.listBoxBorderText + "Page %4d of %4d", this.pageNum+1, this.numPages);
return selectedItemRow;
}
function ChoiceScrollbox_RefreshOnScreen(pSelectedIndex)
{
this.drawBorder();
this.drawInnerMenu(pSelectedIndex);
}
function ChoiceScrollbox_RefreshItemCharOnScreen(pItemIndex, pCharIndex)
{
if ((typeof(pItemIndex) != "number") || (typeof(pCharIndex) != "number"))
return;
if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length) ||
(pItemIndex < this.topItemIndex) || (pItemIndex > this.bottomItemIndex))
{
return;
}
if ((pCharIndex < 0) || (pCharIndex >= this.txtItemList[pItemIndex].length))
return;
// Save the current cursor position so that we can restore it later
const originalCurpos = console.getxy();
// Go to the character's position on the screen and set the highlight or
// normal color, depending on whether the item is the currently selected item,
// then print the character on the screen.
const charScreenX = this.dimensions.topLeftX + 1 + pCharIndex;
const itemScreenY = this.dimensions.topLeftY + 1 + (pItemIndex - this.topItemIndex);
console.gotoxy(charScreenX, itemScreenY);
if (pItemIndex == this.chosenTextItemIndex)
console.print(this.SlyEdCfgObj.genColors.listBoxItemHighlight);
else
console.print(this.SlyEdCfgObj.genColors.listBoxItemText);
console.print(this.txtItemList[pItemIndex].charAt(pCharIndex));
// Move the cursor back to where it was originally
console.gotoxy(originalCurpos);
}
function ChoiceScrollbox_DoInputLoop(pDrawBorder)
{
var retObj = {
itemWasSelected: false,
selectedIndex: -1,
selectedItem: "",
lastKeypress: ""
};
// Don't do anything if the item list doesn't contain any items
if (this.txtItemList.length == 0)
return retObj;
//////////////////////////////////
// Locally-defined functions
// This function returns the index of the bottommost item that
// can be displayed in the box.
//
// Parameters:
// pArray: The array containing the items
// pTopindex: The index of the topmost item displayed in the box
// pNumItemsPerPage: The number of items per page
function getBottommostItemIndex(pArray, pTopIndex, pNumItemsPerPage)
{
var bottomIndex = pTopIndex + pNumItemsPerPage - 1;
// If bottomIndex is beyond the last index, then adjust it.
if (bottomIndex >= pArray.length)
bottomIndex = pArray.length - 1;
return bottomIndex;
}
//////////////////////////////////
// Code
// Variables for keeping track of the item list
this.numItemsPerPage = this.dimensions.height - 2;
this.topItemIndex = 0; // The index of the message group at the top of the list
// Figure out the index of the last message group to appear on the screen.
this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage);
this.numPages = Math.ceil(this.txtItemList.length / this.numItemsPerPage);
const topIndexForLastPage = (this.numItemsPerPage * this.numPages) - this.numItemsPerPage;
if (pDrawBorder)
this.drawBorder();
// User input loop
// For the horizontal location of the page number text for the box border:
// Based on the fact that there can be up to 9999 text replacements and 10
// per page, there will be up to 1000 pages of replacements. To write the
// text, we'll want to be 20 characters to the left of the end of the border
// of the box.
this.pageNumTxtStartX = this.dimensions.topLeftX + this.dimensions.width - 19;
this.maxItemWidth = this.dimensions.width - 2;
this.pageNum = 0;
var startArrIndex = 0;
this.chosenTextItemIndex = retObj.selectedIndex = 0;
var endArrIndex = 0; // One past the last array item
var curpos = { // For keeping track of the current cursor position
x: 0,
y: 0
};
var refreshList = true; // For screen redraw optimizations
var continueOn = true;
while (continueOn)
{
if (refreshList)
{
this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage);
// Write the list of items for the current page. Also, drawInnerMenu()
// will return the selected item row.
var selectedItemRow = this.drawInnerMenu(retObj.selectedIndex);
// Just for sane appearance: Move the cursor to the first character of
// the currently-selected row and set the appropriate color.
curpos.x = this.dimensions.topLeftX+1;
curpos.y = selectedItemRow;
console.gotoxy(curpos.x, curpos.y);
console.print(this.SlyEdCfgObj.genColors.listBoxItemHighlight);
refreshList = false;
}
// Get a key from the user (upper-case) and take action based upon it.
retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOSPIN, this.SlyEdCfgObj);
switch (retObj.lastKeypress)
{
case 'N': // Next page
case KEY_PAGE_DOWN:
refreshList = (this.pageNum < this.numPages-1);
if (refreshList)
{
++this.pageNum;
this.topItemIndex += this.numItemsPerPage;
this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
// Note: this.bottomItemIndex is refreshed at the top of the loop
}
break;
case 'P': // Previous page
case KEY_PAGE_UP:
refreshList = (this.pageNum > 0);
if (refreshList)
{
--this.pageNum;
this.topItemIndex -= this.numItemsPerPage;
this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
// Note: this.bottomItemIndex is refreshed at the top of the loop
}
break;
case 'F': // First page
refreshList = (this.pageNum > 0);
if (refreshList)
{
this.pageNum = 0;
this.topItemIndex = 0;
this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;