Fossil SCM

Added widget to browse/select locally-edited files. Related cleanups and refactoring.

stephan 2020-05-16 05:03 fileedit-ajaxify
Commit aefceac57c98bda86718ad5b8a546fa52f048e2708558d3707dab0a93bfa8a51
+175 -156
--- src/default_css.txt
+++ src/default_css.txt
@@ -860,44 +860,11 @@
860860
// }
861861
// #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div {
862862
// max-width: 30em;
863863
// overflow: auto;
864864
// }
865
-// .fileedit-XXX => /fileedit page
866
-.hidden {
867
- position: absolute;
868
- opacity: 0;
869
- pointer-events: none;
870
-}
871
-//.hidden {
872
-// display: none;
873
-//}
874
-#fossil-status-bar {
875
- display: block;
876
- font-family: monospace;
877
- border-width: 1px;
878
- border-style: inset;
879
- border-color: inherit;
880
- min-height: 1.5em;
881
- font-size: 1.2em;
882
- padding: 0.2em;
883
- margin: 0.25em 0;
884
- flex: 0 0 auto;
885
-}
886
-.error {
887
- color: darkred;
888
- background: yellow;
889
-}
890
-body.fileedit .error {
891
- padding: 0.25em;
892
-}
893
-body.fileedit .warning {
894
- color: darkred;
895
- background: yellow;
896
- padding: 0.25em;
897
- opacity: 0.6;
898
-}
865
+
899866
//////////////////////////////////
900867
// Styles for fossil.tabs.js:
901868
.tab-container {
902869
width: 100%;
903870
display: flex;
@@ -948,102 +915,11 @@
948915
opacity: 1.0;
949916
border-top-style: outset;
950917
border-left-style: outset;
951918
border-right-style: outset;
952919
}
953
-//////////////////////////////////
954
-// Styles for /fileedit:
955
-body.fileedit textarea {
956
- font-family: monospace;
957
- width: 100%;
958
- flex: 10 1 auto;
959
- height: initial;
960
-}
961
-body.fileedit fieldset {
962
- margin: 0.5em 0 0.5em 0;
963
- padding: 0.25em 0;
964
- border-radius: 0.5em;
965
- border-color: inherit;
966
- border-width: 1px;
967
- font-size: 90%;
968
- overflow: auto;
969
-}
970
-body.fileedit fieldset > legend {
971
- margin: 0 0 0 1em;
972
- padding: 0 0.5em 0 0.5em;
973
-}
974
-body.fileedit fieldset > div {
975
- margin: 0 0.25em 0 0.25em;
976
- padding: 0;
977
- overflow: auto;
978
-}
979
-body.fileedit fieldset > div > .input-with-label {
980
- margin: 0.25em 0.5em;
981
-}
982
-body.fileedit fieldset > div > button {
983
- margin: 0.25em 0.5em;
984
-}
985
-.fileedit-hint {
986
- font-size: 80%;
987
- opacity: 0.75;
988
-}
989
-.fileedit-error-report {
990
- background: yellow;
991
- color: darkred;
992
- margin: 1em 0;
993
- padding: 0.5em;
994
- border-radius: 0.5em;
995
-}
996
-code.fileedit-manifest {
997
- display: block;
998
- height: 16em;
999
- overflow: auto;
1000
- white-space: pre;
1001
-}
1002
-div.fileedit-preview {
1003
- margin: 0;
1004
- padding: 0;
1005
-}
1006
-#fileedit-tab-diff-wrapper {
1007
- margin: 0;
1008
- padding: 0;
1009
- overflow: auto;
1010
-}
1011
-#fileedit-tab-preview-wrapper {
1012
- overflow: auto;
1013
-}
1014
-.fileedit-options.commit-message > div {
1015
- display: flex;
1016
- flex-direction: column;
1017
- justify-content: stretch;
1018
- font-family: monospace;
1019
-}
1020
-.fileedit-options.commit-message > div > * {
1021
- margin: 0.25em;
1022
-}
1023
-#fileedit-commit-button-wrapper {
1024
- margin: 0.25em;
1025
-}
1026
-.tab-container > .tabs > .tab-panel > .fileedit-options {
1027
- margin-top: 0;
1028
- border: none;
1029
- border-radius: 0;
1030
- border-bottom-width: 1px;
1031
- border-bottom-style: dotted;
1032
-}
1033
-.tab-container > .tabs > .tab-panel > .fileedit-options > button {
1034
- vertical-align: middle;
1035
- margin: 0.5em;
1036
-}
1037
-.tab-container > .tabs > .tab-panel > .fileedit-options > input {
1038
- vertical-align: middle;
1039
- margin: 0.5em;
1040
-}
1041
-.tab-container > .tabs > .tab-panel > .fileedit-options > .input-with-label {
1042
- vertical-align: middle;
1043
- margin: 0.5em;
1044
-}
920
+
1045921
////////////////////////////////////////////////////////////////////
1046922
// Styles developed for /fileedit but which have wider
1047923
// applicability:
1048924
.flex-container {
1049925
display: flex;
@@ -1052,50 +928,43 @@
1052928
flex-direction: row;
1053929
flex-wrap: wrap;
1054930
justify-content: center;
1055931
align-items: center;
1056932
}
1057
-.fileedit-options.flex-container.flex-row {
1058
- align-items: first baseline;
1059
-}
1060933
.flex-container .flex-grow {
1061934
flex-grow: 10;
1062935
flex-shrink: 0;
1063936
}
1064937
.flex-container .flex-shrink {
1065938
flex-grow: 0;
1066939
flex-shrink: 10;
1067940
}
1068
-.fileedit-options > div > * {
1069
- margin: 0.25em;
1070
-}
1071
-#fileedit-file-selector {
1072
- display: flex;
1073
- flex-direction: column;
1074
- align-content: flex-start;
1075
- border-color: inherit;
941
+#fossil-status-bar {
942
+ display: block;
943
+ font-family: monospace;
1076944
border-width: 1px;
1077945
border-style: inset;
1078
- border-radius: 0.5em;
1079
- padding: 0 0.25em;
1080
- margin: 0;
1081
- min-height: 12em;
1082
-}
1083
-#fileedit-file-selector select {
1084
- margin: 0 0 0.5em 0;
1085
- height: initial;
1086
- font-family: monospace;
1087
-}
1088
-#fileedit-file-selector select:focus {
1089
- border: none;
1090
-}
1091
-#fileedit-file-selector > div {
1092
- padding: 0;
1093
- margin: 0;
1094
-}
1095
-#fileedit-file-selector > div > * {
1096
- margin: 0.25em 0.5em 0.25em 0;
946
+ border-color: inherit;
947
+ min-height: 1.5em;
948
+ font-size: 1.2em;
949
+ padding: 0.2em;
950
+ margin: 0.25em 0;
951
+ flex: 0 0 auto;
952
+}
953
+.error {
954
+ color: darkred;
955
+ background: yellow;
956
+}
957
+.warning {
958
+ color: darkred;
959
+ background: yellow;
960
+ opacity: 0.7;
961
+}
962
+.hidden {
963
+ position: absolute;
964
+ opacity: 0;
965
+ pointer-events: none;
1097966
}
1098967
.flex-container.flex-row.stretch {
1099968
flex-wrap: wrap;
1100969
align-items: baseline;
1101970
justify-content: stretch;
@@ -1127,10 +996,11 @@
1127996
font-size: 175%;
1128997
}
1129998
.font-size-200 {
1130999
font-size: 200%;
11311000
}
1001
+
11321002
//////////////////////////////////////////////////////////////////
11331003
// .input-with-label is intended to be a wrapper element which
11341004
// contains a SPAN label and an INPUT control.
11351005
.input-with-label {
11361006
border: 1px inset #808080;
@@ -1143,10 +1013,13 @@
11431013
.input-with-label > * {
11441014
vertical-align: middle;
11451015
}
11461016
.input-with-label > input {
11471017
margin: 0;
1018
+}
1019
+.input-with-label > button {
1020
+ margin: 0;
11481021
}
11491022
.input-with-label > select {
11501023
margin: 0;
11511024
}
11521025
.input-with-label > input[type=text] {
@@ -1164,5 +1037,151 @@
11641037
.input-with-label > label {
11651038
font-weight: initial;
11661039
margin: 0 0.25em 0 0.25em;
11671040
vertical-align: middle;
11681041
}
1042
+
1043
+////////////////////////////////////////////////////////////
1044
+// Styles for /fileedit:
1045
+// body.fileedit => /fileedit page
1046
+body.fileedit .error {
1047
+ padding: 0.25em;
1048
+}
1049
+body.fileedit .warning {
1050
+ padding: 0.25em;
1051
+}
1052
+body.fileedit textarea {
1053
+ font-family: monospace;
1054
+ width: 100%;
1055
+ flex: 10 1 auto;
1056
+ height: initial;
1057
+}
1058
+body.fileedit fieldset {
1059
+ margin: 0.5em 0 0.5em 0;
1060
+ padding: 0.25em 0;
1061
+ border-radius: 0.5em;
1062
+ border-color: inherit;
1063
+ border-width: 1px;
1064
+ font-size: 90%;
1065
+ overflow: auto;
1066
+}
1067
+body.fileedit fieldset > legend {
1068
+ margin: 0 0 0 1em;
1069
+ padding: 0 0.5em 0 0.5em;
1070
+}
1071
+body.fileedit fieldset > div {
1072
+ margin: 0 0.25em 0 0.25em;
1073
+ padding: 0;
1074
+ overflow: auto;
1075
+}
1076
+body.fileedit fieldset > div > .input-with-label {
1077
+ margin: 0.25em 0.5em;
1078
+}
1079
+body.fileedit fieldset > div > button {
1080
+ margin: 0.25em 0.5em;
1081
+}
1082
+body.fileedit .fileedit-hint {
1083
+ font-size: 80%;
1084
+ opacity: 0.75;
1085
+}
1086
+body.fileedit .fileedit-error-report {
1087
+ background: yellow;
1088
+ color: darkred;
1089
+ margin: 1em 0;
1090
+ padding: 0.5em;
1091
+ border-radius: 0.5em;
1092
+}
1093
+body.fileedit code.fileedit-manifest {
1094
+ display: block;
1095
+ height: 16em;
1096
+ overflow: auto;
1097
+ white-space: pre;
1098
+}
1099
+body.fileedit div.fileedit-preview {
1100
+ margin: 0;
1101
+ padding: 0;
1102
+}
1103
+body.fileedit #fileedit-tab-diff-wrapper {
1104
+ margin: 0;
1105
+ padding: 0;
1106
+ overflow: auto;
1107
+}
1108
+body.fileedit #fileedit-tab-preview-wrapper {
1109
+ overflow: auto;
1110
+}
1111
+body.fileedit .fileedit-options.commit-message > div {
1112
+ display: flex;
1113
+ flex-direction: column;
1114
+ justify-content: stretch;
1115
+ font-family: monospace;
1116
+}
1117
+body.fileedit .fileedit-options.commit-message > div > * {
1118
+ margin: 0.25em;
1119
+}
1120
+body.fileedit #fileedit-commit-button-wrapper {
1121
+ margin: 0.25em;
1122
+}
1123
+body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options {
1124
+ margin-top: 0;
1125
+ border: none;
1126
+ border-radius: 0;
1127
+ border-bottom-width: 1px;
1128
+ border-bottom-style: dotted;
1129
+}
1130
+body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > button {
1131
+ vertical-align: middle;
1132
+ margin: 0.5em;
1133
+}
1134
+body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > input {
1135
+ vertical-align: middle;
1136
+ margin: 0.5em;
1137
+}
1138
+body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > .input-with-label {
1139
+ vertical-align: middle;
1140
+ margin: 0.5em;
1141
+}
1142
+body.fileedit .fileedit-options > div > * {
1143
+ margin: 0.25em;
1144
+}
1145
+body.fileedit .fileedit-options.flex-container.flex-row {
1146
+ align-items: first baseline;
1147
+}
1148
+body.fileedit #fileedit-file-selector {
1149
+ display: flex;
1150
+ flex-direction: column;
1151
+ align-content: flex-start;
1152
+ border-color: inherit;
1153
+ border-width: 1px;
1154
+ border-style: inset;
1155
+ border-radius: 0.5em;
1156
+ padding: 0 0.25em;
1157
+ margin: 0;
1158
+ min-height: 12em;
1159
+}
1160
+body.fileedit #fileedit-file-selector select {
1161
+ margin: 0 0 0.5em 0;
1162
+ height: initial;
1163
+ font-family: monospace;
1164
+}
1165
+body.fileedit select:focus {
1166
+ border: none;
1167
+}
1168
+body.fileedit option:focus {
1169
+ border: none;
1170
+}
1171
+body.fileedit #fileedit-file-selector > div {
1172
+ padding: 0;
1173
+ margin: 0;
1174
+}
1175
+body.fileedit #fileedit-file-selector > div > * {
1176
+ margin: 0.25em 0.5em 0.25em 0;
1177
+}
1178
+body.fileedit #fileedit-stash-selector {
1179
+ border-bottom-width: 1px;
1180
+ border-bottom-style: dotted;
1181
+ margin: 0.25em;
1182
+}
1183
+body.fileedit #fileedit-stash-selector select {
1184
+ margin: 0;
1185
+ height: initial;
1186
+ font-family: monospace;
1187
+}
11691188
--- src/default_css.txt
+++ src/default_css.txt
@@ -860,44 +860,11 @@
860 // }
861 // #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div {
862 // max-width: 30em;
863 // overflow: auto;
864 // }
865 // .fileedit-XXX => /fileedit page
866 .hidden {
867 position: absolute;
868 opacity: 0;
869 pointer-events: none;
870 }
871 //.hidden {
872 // display: none;
873 //}
874 #fossil-status-bar {
875 display: block;
876 font-family: monospace;
877 border-width: 1px;
878 border-style: inset;
879 border-color: inherit;
880 min-height: 1.5em;
881 font-size: 1.2em;
882 padding: 0.2em;
883 margin: 0.25em 0;
884 flex: 0 0 auto;
885 }
886 .error {
887 color: darkred;
888 background: yellow;
889 }
890 body.fileedit .error {
891 padding: 0.25em;
892 }
893 body.fileedit .warning {
894 color: darkred;
895 background: yellow;
896 padding: 0.25em;
897 opacity: 0.6;
898 }
899 //////////////////////////////////
900 // Styles for fossil.tabs.js:
901 .tab-container {
902 width: 100%;
903 display: flex;
@@ -948,102 +915,11 @@
948 opacity: 1.0;
949 border-top-style: outset;
950 border-left-style: outset;
951 border-right-style: outset;
952 }
953 //////////////////////////////////
954 // Styles for /fileedit:
955 body.fileedit textarea {
956 font-family: monospace;
957 width: 100%;
958 flex: 10 1 auto;
959 height: initial;
960 }
961 body.fileedit fieldset {
962 margin: 0.5em 0 0.5em 0;
963 padding: 0.25em 0;
964 border-radius: 0.5em;
965 border-color: inherit;
966 border-width: 1px;
967 font-size: 90%;
968 overflow: auto;
969 }
970 body.fileedit fieldset > legend {
971 margin: 0 0 0 1em;
972 padding: 0 0.5em 0 0.5em;
973 }
974 body.fileedit fieldset > div {
975 margin: 0 0.25em 0 0.25em;
976 padding: 0;
977 overflow: auto;
978 }
979 body.fileedit fieldset > div > .input-with-label {
980 margin: 0.25em 0.5em;
981 }
982 body.fileedit fieldset > div > button {
983 margin: 0.25em 0.5em;
984 }
985 .fileedit-hint {
986 font-size: 80%;
987 opacity: 0.75;
988 }
989 .fileedit-error-report {
990 background: yellow;
991 color: darkred;
992 margin: 1em 0;
993 padding: 0.5em;
994 border-radius: 0.5em;
995 }
996 code.fileedit-manifest {
997 display: block;
998 height: 16em;
999 overflow: auto;
1000 white-space: pre;
1001 }
1002 div.fileedit-preview {
1003 margin: 0;
1004 padding: 0;
1005 }
1006 #fileedit-tab-diff-wrapper {
1007 margin: 0;
1008 padding: 0;
1009 overflow: auto;
1010 }
1011 #fileedit-tab-preview-wrapper {
1012 overflow: auto;
1013 }
1014 .fileedit-options.commit-message > div {
1015 display: flex;
1016 flex-direction: column;
1017 justify-content: stretch;
1018 font-family: monospace;
1019 }
1020 .fileedit-options.commit-message > div > * {
1021 margin: 0.25em;
1022 }
1023 #fileedit-commit-button-wrapper {
1024 margin: 0.25em;
1025 }
1026 .tab-container > .tabs > .tab-panel > .fileedit-options {
1027 margin-top: 0;
1028 border: none;
1029 border-radius: 0;
1030 border-bottom-width: 1px;
1031 border-bottom-style: dotted;
1032 }
1033 .tab-container > .tabs > .tab-panel > .fileedit-options > button {
1034 vertical-align: middle;
1035 margin: 0.5em;
1036 }
1037 .tab-container > .tabs > .tab-panel > .fileedit-options > input {
1038 vertical-align: middle;
1039 margin: 0.5em;
1040 }
1041 .tab-container > .tabs > .tab-panel > .fileedit-options > .input-with-label {
1042 vertical-align: middle;
1043 margin: 0.5em;
1044 }
1045 ////////////////////////////////////////////////////////////////////
1046 // Styles developed for /fileedit but which have wider
1047 // applicability:
1048 .flex-container {
1049 display: flex;
@@ -1052,50 +928,43 @@
1052 flex-direction: row;
1053 flex-wrap: wrap;
1054 justify-content: center;
1055 align-items: center;
1056 }
1057 .fileedit-options.flex-container.flex-row {
1058 align-items: first baseline;
1059 }
1060 .flex-container .flex-grow {
1061 flex-grow: 10;
1062 flex-shrink: 0;
1063 }
1064 .flex-container .flex-shrink {
1065 flex-grow: 0;
1066 flex-shrink: 10;
1067 }
1068 .fileedit-options > div > * {
1069 margin: 0.25em;
1070 }
1071 #fileedit-file-selector {
1072 display: flex;
1073 flex-direction: column;
1074 align-content: flex-start;
1075 border-color: inherit;
1076 border-width: 1px;
1077 border-style: inset;
1078 border-radius: 0.5em;
1079 padding: 0 0.25em;
1080 margin: 0;
1081 min-height: 12em;
1082 }
1083 #fileedit-file-selector select {
1084 margin: 0 0 0.5em 0;
1085 height: initial;
1086 font-family: monospace;
1087 }
1088 #fileedit-file-selector select:focus {
1089 border: none;
1090 }
1091 #fileedit-file-selector > div {
1092 padding: 0;
1093 margin: 0;
1094 }
1095 #fileedit-file-selector > div > * {
1096 margin: 0.25em 0.5em 0.25em 0;
 
1097 }
1098 .flex-container.flex-row.stretch {
1099 flex-wrap: wrap;
1100 align-items: baseline;
1101 justify-content: stretch;
@@ -1127,10 +996,11 @@
1127 font-size: 175%;
1128 }
1129 .font-size-200 {
1130 font-size: 200%;
1131 }
 
1132 //////////////////////////////////////////////////////////////////
1133 // .input-with-label is intended to be a wrapper element which
1134 // contains a SPAN label and an INPUT control.
1135 .input-with-label {
1136 border: 1px inset #808080;
@@ -1143,10 +1013,13 @@
1143 .input-with-label > * {
1144 vertical-align: middle;
1145 }
1146 .input-with-label > input {
1147 margin: 0;
 
 
 
1148 }
1149 .input-with-label > select {
1150 margin: 0;
1151 }
1152 .input-with-label > input[type=text] {
@@ -1164,5 +1037,151 @@
1164 .input-with-label > label {
1165 font-weight: initial;
1166 margin: 0 0.25em 0 0.25em;
1167 vertical-align: middle;
1168 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1169
--- src/default_css.txt
+++ src/default_css.txt
@@ -860,44 +860,11 @@
860 // }
861 // #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div {
862 // max-width: 30em;
863 // overflow: auto;
864 // }
865
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866 //////////////////////////////////
867 // Styles for fossil.tabs.js:
868 .tab-container {
869 width: 100%;
870 display: flex;
@@ -948,102 +915,11 @@
915 opacity: 1.0;
916 border-top-style: outset;
917 border-left-style: outset;
918 border-right-style: outset;
919 }
920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
921 ////////////////////////////////////////////////////////////////////
922 // Styles developed for /fileedit but which have wider
923 // applicability:
924 .flex-container {
925 display: flex;
@@ -1052,50 +928,43 @@
928 flex-direction: row;
929 flex-wrap: wrap;
930 justify-content: center;
931 align-items: center;
932 }
 
 
 
933 .flex-container .flex-grow {
934 flex-grow: 10;
935 flex-shrink: 0;
936 }
937 .flex-container .flex-shrink {
938 flex-grow: 0;
939 flex-shrink: 10;
940 }
941 #fossil-status-bar {
942 display: block;
943 font-family: monospace;
 
 
 
 
 
944 border-width: 1px;
945 border-style: inset;
946 border-color: inherit;
947 min-height: 1.5em;
948 font-size: 1.2em;
949 padding: 0.2em;
950 margin: 0.25em 0;
951 flex: 0 0 auto;
952 }
953 .error {
954 color: darkred;
955 background: yellow;
956 }
957 .warning {
958 color: darkred;
959 background: yellow;
960 opacity: 0.7;
961 }
962 .hidden {
963 position: absolute;
964 opacity: 0;
965 pointer-events: none;
966 }
967 .flex-container.flex-row.stretch {
968 flex-wrap: wrap;
969 align-items: baseline;
970 justify-content: stretch;
@@ -1127,10 +996,11 @@
996 font-size: 175%;
997 }
998 .font-size-200 {
999 font-size: 200%;
1000 }
1001
1002 //////////////////////////////////////////////////////////////////
1003 // .input-with-label is intended to be a wrapper element which
1004 // contains a SPAN label and an INPUT control.
1005 .input-with-label {
1006 border: 1px inset #808080;
@@ -1143,10 +1013,13 @@
1013 .input-with-label > * {
1014 vertical-align: middle;
1015 }
1016 .input-with-label > input {
1017 margin: 0;
1018 }
1019 .input-with-label > button {
1020 margin: 0;
1021 }
1022 .input-with-label > select {
1023 margin: 0;
1024 }
1025 .input-with-label > input[type=text] {
@@ -1164,5 +1037,151 @@
1037 .input-with-label > label {
1038 font-weight: initial;
1039 margin: 0 0.25em 0 0.25em;
1040 vertical-align: middle;
1041 }
1042
1043 ////////////////////////////////////////////////////////////
1044 // Styles for /fileedit:
1045 // body.fileedit => /fileedit page
1046 body.fileedit .error {
1047 padding: 0.25em;
1048 }
1049 body.fileedit .warning {
1050 padding: 0.25em;
1051 }
1052 body.fileedit textarea {
1053 font-family: monospace;
1054 width: 100%;
1055 flex: 10 1 auto;
1056 height: initial;
1057 }
1058 body.fileedit fieldset {
1059 margin: 0.5em 0 0.5em 0;
1060 padding: 0.25em 0;
1061 border-radius: 0.5em;
1062 border-color: inherit;
1063 border-width: 1px;
1064 font-size: 90%;
1065 overflow: auto;
1066 }
1067 body.fileedit fieldset > legend {
1068 margin: 0 0 0 1em;
1069 padding: 0 0.5em 0 0.5em;
1070 }
1071 body.fileedit fieldset > div {
1072 margin: 0 0.25em 0 0.25em;
1073 padding: 0;
1074 overflow: auto;
1075 }
1076 body.fileedit fieldset > div > .input-with-label {
1077 margin: 0.25em 0.5em;
1078 }
1079 body.fileedit fieldset > div > button {
1080 margin: 0.25em 0.5em;
1081 }
1082 body.fileedit .fileedit-hint {
1083 font-size: 80%;
1084 opacity: 0.75;
1085 }
1086 body.fileedit .fileedit-error-report {
1087 background: yellow;
1088 color: darkred;
1089 margin: 1em 0;
1090 padding: 0.5em;
1091 border-radius: 0.5em;
1092 }
1093 body.fileedit code.fileedit-manifest {
1094 display: block;
1095 height: 16em;
1096 overflow: auto;
1097 white-space: pre;
1098 }
1099 body.fileedit div.fileedit-preview {
1100 margin: 0;
1101 padding: 0;
1102 }
1103 body.fileedit #fileedit-tab-diff-wrapper {
1104 margin: 0;
1105 padding: 0;
1106 overflow: auto;
1107 }
1108 body.fileedit #fileedit-tab-preview-wrapper {
1109 overflow: auto;
1110 }
1111 body.fileedit .fileedit-options.commit-message > div {
1112 display: flex;
1113 flex-direction: column;
1114 justify-content: stretch;
1115 font-family: monospace;
1116 }
1117 body.fileedit .fileedit-options.commit-message > div > * {
1118 margin: 0.25em;
1119 }
1120 body.fileedit #fileedit-commit-button-wrapper {
1121 margin: 0.25em;
1122 }
1123 body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options {
1124 margin-top: 0;
1125 border: none;
1126 border-radius: 0;
1127 border-bottom-width: 1px;
1128 border-bottom-style: dotted;
1129 }
1130 body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > button {
1131 vertical-align: middle;
1132 margin: 0.5em;
1133 }
1134 body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > input {
1135 vertical-align: middle;
1136 margin: 0.5em;
1137 }
1138 body.fileedit .tab-container > .tabs > .tab-panel > .fileedit-options > .input-with-label {
1139 vertical-align: middle;
1140 margin: 0.5em;
1141 }
1142 body.fileedit .fileedit-options > div > * {
1143 margin: 0.25em;
1144 }
1145 body.fileedit .fileedit-options.flex-container.flex-row {
1146 align-items: first baseline;
1147 }
1148 body.fileedit #fileedit-file-selector {
1149 display: flex;
1150 flex-direction: column;
1151 align-content: flex-start;
1152 border-color: inherit;
1153 border-width: 1px;
1154 border-style: inset;
1155 border-radius: 0.5em;
1156 padding: 0 0.25em;
1157 margin: 0;
1158 min-height: 12em;
1159 }
1160 body.fileedit #fileedit-file-selector select {
1161 margin: 0 0 0.5em 0;
1162 height: initial;
1163 font-family: monospace;
1164 }
1165 body.fileedit select:focus {
1166 border: none;
1167 }
1168 body.fileedit option:focus {
1169 border: none;
1170 }
1171 body.fileedit #fileedit-file-selector > div {
1172 padding: 0;
1173 margin: 0;
1174 }
1175 body.fileedit #fileedit-file-selector > div > * {
1176 margin: 0.25em 0.5em 0.25em 0;
1177 }
1178 body.fileedit #fileedit-stash-selector {
1179 border-bottom-width: 1px;
1180 border-bottom-style: dotted;
1181 margin: 0.25em;
1182 }
1183 body.fileedit #fileedit-stash-selector select {
1184 margin: 0;
1185 height: initial;
1186 font-family: monospace;
1187 }
1188
+8 -16
--- src/fileedit.c
+++ src/fileedit.c
@@ -1818,23 +1818,16 @@
18181818
"data-tab-parent='fileedit-tabs' "
18191819
"data-tab-label='File Content'"
18201820
">");
18211821
CX("<div class='fileedit-options "
18221822
"flex-container flex-row child-gap-small'>");
1823
- if(1){
1824
- /* Discard/reload button. Leave this out until we have a
1825
- ** nice way of offering confirmation, e.g. like the old
1826
- ** jQuery.confirmer plugin which required a 2nd click of the
1827
- ** button within X seconds to confirm. Right now it's simply
1828
- ** to easy to tap by accident. */
1829
- CX("<button class='fileedit-content-reload confirmer' "
1830
- "title='Reload the file from the server, discarding "
1831
- "any local edits. To help avoid accidental loss of "
1832
- "edits, it requires confirmation (a second click) within "
1833
- "a few seconds or it will not reload.'"
1834
- ">Discard &amp; Reload</button>");
1835
- }
1823
+ CX("<button class='fileedit-content-reload confirmer' "
1824
+ "title='Reload the file from the server, discarding "
1825
+ "any local edits. To help avoid accidental loss of "
1826
+ "edits, it requires confirmation (a second click) within "
1827
+ "a few seconds or it will not reload.'"
1828
+ ">Discard &amp; Reload</button>");
18361829
style_select_list_int("select-font-size",
18371830
"editor_font_size", "Editor font size",
18381831
NULL/*tooltip*/,
18391832
100,
18401833
"100%", 100, "125%", 125,
@@ -2089,13 +2082,12 @@
20892082
"with many files.</li>");
20902083
CX("<li>The file selector allows, for usability's sake, only files "
20912084
"in leaf checkins to be selected, but files may be edited via "
20922085
"non-leaf checkins by passing them as the <code>filename</code> "
20932086
"and <code>checkin</code> URL arguments to this page.</li>");
2094
- CX("<li>The editor \"stashes\" local edits to the last 7 "
2095
- "checkin/file combinations in one of "
2096
- "<code>window.fileStorage</code> or "
2087
+ CX("<li>The editor \"stashes\" some number of local edits in "
2088
+ "one of <code>window.fileStorage</code> or "
20972089
"<code>window.sessionStorage</code>, if able, but which storage "
20982090
"is unspecified and may differ across environments. When saving "
20992091
"or force-reloading a file, stashed edits to that version are "
21002092
"discarded.</li>");
21012093
CX("</ul>");
21022094
--- src/fileedit.c
+++ src/fileedit.c
@@ -1818,23 +1818,16 @@
1818 "data-tab-parent='fileedit-tabs' "
1819 "data-tab-label='File Content'"
1820 ">");
1821 CX("<div class='fileedit-options "
1822 "flex-container flex-row child-gap-small'>");
1823 if(1){
1824 /* Discard/reload button. Leave this out until we have a
1825 ** nice way of offering confirmation, e.g. like the old
1826 ** jQuery.confirmer plugin which required a 2nd click of the
1827 ** button within X seconds to confirm. Right now it's simply
1828 ** to easy to tap by accident. */
1829 CX("<button class='fileedit-content-reload confirmer' "
1830 "title='Reload the file from the server, discarding "
1831 "any local edits. To help avoid accidental loss of "
1832 "edits, it requires confirmation (a second click) within "
1833 "a few seconds or it will not reload.'"
1834 ">Discard &amp; Reload</button>");
1835 }
1836 style_select_list_int("select-font-size",
1837 "editor_font_size", "Editor font size",
1838 NULL/*tooltip*/,
1839 100,
1840 "100%", 100, "125%", 125,
@@ -2089,13 +2082,12 @@
2089 "with many files.</li>");
2090 CX("<li>The file selector allows, for usability's sake, only files "
2091 "in leaf checkins to be selected, but files may be edited via "
2092 "non-leaf checkins by passing them as the <code>filename</code> "
2093 "and <code>checkin</code> URL arguments to this page.</li>");
2094 CX("<li>The editor \"stashes\" local edits to the last 7 "
2095 "checkin/file combinations in one of "
2096 "<code>window.fileStorage</code> or "
2097 "<code>window.sessionStorage</code>, if able, but which storage "
2098 "is unspecified and may differ across environments. When saving "
2099 "or force-reloading a file, stashed edits to that version are "
2100 "discarded.</li>");
2101 CX("</ul>");
2102
--- src/fileedit.c
+++ src/fileedit.c
@@ -1818,23 +1818,16 @@
1818 "data-tab-parent='fileedit-tabs' "
1819 "data-tab-label='File Content'"
1820 ">");
1821 CX("<div class='fileedit-options "
1822 "flex-container flex-row child-gap-small'>");
1823 CX("<button class='fileedit-content-reload confirmer' "
1824 "title='Reload the file from the server, discarding "
1825 "any local edits. To help avoid accidental loss of "
1826 "edits, it requires confirmation (a second click) within "
1827 "a few seconds or it will not reload.'"
1828 ">Discard &amp; Reload</button>");
 
 
 
 
 
 
 
1829 style_select_list_int("select-font-size",
1830 "editor_font_size", "Editor font size",
1831 NULL/*tooltip*/,
1832 100,
1833 "100%", 100, "125%", 125,
@@ -2089,13 +2082,12 @@
2082 "with many files.</li>");
2083 CX("<li>The file selector allows, for usability's sake, only files "
2084 "in leaf checkins to be selected, but files may be edited via "
2085 "non-leaf checkins by passing them as the <code>filename</code> "
2086 "and <code>checkin</code> URL arguments to this page.</li>");
2087 CX("<li>The editor \"stashes\" some number of local edits in "
2088 "one of <code>window.fileStorage</code> or "
 
2089 "<code>window.sessionStorage</code>, if able, but which storage "
2090 "is unspecified and may differ across environments. When saving "
2091 "or force-reloading a file, stashed edits to that version are "
2092 "discarded.</li>");
2093 CX("</ul>");
2094
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -153,20 +153,23 @@
153153
154154
/**
155155
Expects to be passed as hash code as its first argument. It
156156
returns a "shortened" form of hash, with a length which depends
157157
on the 2nd argument: truthy = fossil.config.hashDigitsUrl, falsy
158
- = fossil.config.hashDigits. Both of those values are derived from
159
- the 'hash-digits' repo-level config setting or the
158
+ = fossil.config.hashDigits, number == that many digits. The
159
+ fossil.config values are derived from the 'hash-digits'
160
+ repo-level config setting or the
160161
FOSSIL_HASH_DIGITS_URL/FOSSIL_HASH_DIGITS compile-time options.
161162
162163
If its first arugment is a non-string, that value is returned
163164
as-is.
164165
*/
165166
F.hashDigits = function(hash,forUrl){
167
+ const n = ('number'===typeof forUrl)
168
+ ? forUrl : F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits'];
166169
return ('string'==typeof hash ? hash.substr(
167
- 0, F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits']
170
+ 0, n
168171
) : hash);
169172
};
170173
171174
/**
172175
Sets up pseudo-automatic content preview handling between a
@@ -274,10 +277,39 @@
274277
}, false
275278
);
276279
});
277280
return this;
278281
};
282
+
283
+ /**
284
+ Convenience wrapper which adds an onload event listener to the
285
+ window object. Returns this.
286
+ */
287
+ F.onPageLoad = function(callback){
288
+ window.addEventListener('load', callback, false);
289
+ return this;
290
+ };
291
+
292
+ /**
293
+ Assuming name is a repo-style filename, this function returns
294
+ a shortened form of that name:
295
+
296
+ .../LastDirectoryPart/FilenamePart
297
+
298
+ If the name has 0-1 directory parts, it is returned as-is.
299
+
300
+ Design note: in practice it is generally not helpful to elide the
301
+ *last* directory part because embedded docs (in particular) often
302
+ include x/y/index.md and x/z/index.md, both of which would be
303
+ shortened to something like x/.../index.md.
304
+ */
305
+ F.shortenFilename = function(name){
306
+ const a = name.split('/');
307
+ if(a.length<=2) return name;
308
+ while(a.length>2) a.shift();
309
+ return '.../'+a.join('/');
310
+ };
279311
280312
/**
281313
Adds a listener for fossil-level custom events. Events are
282314
delivered to their callbacks as CustomEvent objects with a
283315
'detail' property holding the event's app-level data.
284316
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -153,20 +153,23 @@
153
154 /**
155 Expects to be passed as hash code as its first argument. It
156 returns a "shortened" form of hash, with a length which depends
157 on the 2nd argument: truthy = fossil.config.hashDigitsUrl, falsy
158 = fossil.config.hashDigits. Both of those values are derived from
159 the 'hash-digits' repo-level config setting or the
 
160 FOSSIL_HASH_DIGITS_URL/FOSSIL_HASH_DIGITS compile-time options.
161
162 If its first arugment is a non-string, that value is returned
163 as-is.
164 */
165 F.hashDigits = function(hash,forUrl){
 
 
166 return ('string'==typeof hash ? hash.substr(
167 0, F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits']
168 ) : hash);
169 };
170
171 /**
172 Sets up pseudo-automatic content preview handling between a
@@ -274,10 +277,39 @@
274 }, false
275 );
276 });
277 return this;
278 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
280 /**
281 Adds a listener for fossil-level custom events. Events are
282 delivered to their callbacks as CustomEvent objects with a
283 'detail' property holding the event's app-level data.
284
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -153,20 +153,23 @@
153
154 /**
155 Expects to be passed as hash code as its first argument. It
156 returns a "shortened" form of hash, with a length which depends
157 on the 2nd argument: truthy = fossil.config.hashDigitsUrl, falsy
158 = fossil.config.hashDigits, number == that many digits. The
159 fossil.config values are derived from the 'hash-digits'
160 repo-level config setting or the
161 FOSSIL_HASH_DIGITS_URL/FOSSIL_HASH_DIGITS compile-time options.
162
163 If its first arugment is a non-string, that value is returned
164 as-is.
165 */
166 F.hashDigits = function(hash,forUrl){
167 const n = ('number'===typeof forUrl)
168 ? forUrl : F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits'];
169 return ('string'==typeof hash ? hash.substr(
170 0, n
171 ) : hash);
172 };
173
174 /**
175 Sets up pseudo-automatic content preview handling between a
@@ -274,10 +277,39 @@
277 }, false
278 );
279 });
280 return this;
281 };
282
283 /**
284 Convenience wrapper which adds an onload event listener to the
285 window object. Returns this.
286 */
287 F.onPageLoad = function(callback){
288 window.addEventListener('load', callback, false);
289 return this;
290 };
291
292 /**
293 Assuming name is a repo-style filename, this function returns
294 a shortened form of that name:
295
296 .../LastDirectoryPart/FilenamePart
297
298 If the name has 0-1 directory parts, it is returned as-is.
299
300 Design note: in practice it is generally not helpful to elide the
301 *last* directory part because embedded docs (in particular) often
302 include x/y/index.md and x/z/index.md, both of which would be
303 shortened to something like x/.../index.md.
304 */
305 F.shortenFilename = function(name){
306 const a = name.split('/');
307 if(a.length<=2) return name;
308 while(a.length>2) a.shift();
309 return '.../'+a.join('/');
310 };
311
312 /**
313 Adds a listener for fossil-level custom events. Events are
314 delivered to their callbacks as CustomEvent objects with a
315 'detail' property holding the event's app-level data.
316
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1,10 +1,12 @@
11
(function(F/*the fossil object*/){
22
"use strict";
33
/**
44
Code for the /filepage app. Requires that the fossil JS
5
- bootstrapping is complete and fossil.fetch() has been installed.
5
+ bootstrapping is complete and that several fossil JS APIs have
6
+ been installed: fossil.fetch, fossil.dom, fossil.tabs,
7
+ fossil.storage, fossil.confirmer.
68
79
Custom events, handled via fossil.page.addEventListener():
810
911
- Event 'fileedit-file-loaded': passes on information when it
1012
loads a file, in the form of an object:
@@ -58,21 +60,207 @@
5860
*/
5961
const E = (s)=>document.querySelector(s),
6062
D = F.dom,
6163
P = F.page;
6264
65
+ P.config = {
66
+ defaultMaxStashSize: 7
67
+ };
68
+
69
+ /**
70
+ $stash is an internal-use-only object for managing "stashed"
71
+ local edits, to help avoid that users accidentally lose content
72
+ by switching tabs or following links or some such. The basic
73
+ theory of operation is...
74
+
75
+ All "stashed" state is stored using fossil.storage.
76
+
77
+ - When the current file content is modified by the user, the
78
+ current stathe of the current P.finfo and its the content
79
+ is stashed. For the built-in editor widget, "changes" is
80
+ notified via a 'change' event. For a client-side custom
81
+ widget, the client needs to call P.stashContentChange() when
82
+ their widget triggers the equivalent of a 'change' event.
83
+
84
+ - For certain non-content updates (as of this writing, only the
85
+ is-executable checkbox), only the P.finfo stash entry is
86
+ updated, not the content (unless the content has not yet been
87
+ stashed, in which case it is also stashed so that the stash
88
+ always has matching pairs of finfo/content).
89
+
90
+ - When saving, the stashed entry for the previous version is removed
91
+ from the stash.
92
+
93
+ - When "loading", we use any stashed state for the given
94
+ checkin/file combination. When forcing a re-load of content,
95
+ any stashed entry for that combination is removed from the
96
+ stash.
97
+
98
+ - Every time P.stashContentChange() updates the stash, it is
99
+ pruned to $stash.prune.defaultMaxCount most-recently-updated
100
+ entries.
101
+
102
+ - This API often refers to "finfo objects." Those are objects
103
+ with a minimum of {checkin,filename} properties (which must be
104
+ valid), and a combination of those two properties is used as
105
+ basis for the stash keys for any given checkin/filename
106
+ combination.
107
+
108
+ The structure of the stash is a bit convoluted for efficiency's
109
+ sake: we store a map of file info (finfo) objects separately from
110
+ those files' contents because otherwise we would be required to
111
+ JSONize/de-JSONize the file content when stashing/restoring it,
112
+ and that would be horribly inefficient (meaning "battery-consuming"
113
+ on mobile devices).
114
+ */
115
+ const $stash = {
116
+ keys: {
117
+ index: F.page.name+':index'
118
+ },
119
+ /**
120
+ index: {
121
+ "CHECKIN_HASH:FILENAME": {file info w/o content}
122
+ ...
123
+ }
124
+
125
+ In F.storage we...
126
+
127
+ - Store this.index under the key this.keys.index.
128
+
129
+ - Store each file's content under the key
130
+ (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
131
+ from the index entries to avoid having to JSONize/de-JSONize
132
+ the content. The assumption/hope is that the browser can store
133
+ those records "directly," without any intermediary
134
+ encoding/decoding going on.
135
+ */
136
+ indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
137
+ /** Returns the key for storing content for the given key suffix,
138
+ by prepending P.name to suffix. */
139
+ contentKey: function(suffix){return P.name+'/'+suffix},
140
+ /** Returns the index object, fetching it from the stash or creating
141
+ it anew on the first call. */
142
+ getIndex: function(){
143
+ if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
144
+ return this.index;
145
+ },
146
+ _fireStashEvent: function(){
147
+ if(this._disableNextEvent) delete this._disableNextEvent;
148
+ else F.page.dispatchEvent('fileedit-stash-updated', this);
149
+ },
150
+ /**
151
+ Returns the stashed version, if any, for the given finfo object.
152
+ */
153
+ getFinfo: function(finfo){
154
+ const ndx = this.getIndex();
155
+ return ndx[this.indexKey(finfo)];
156
+ },
157
+ /** Serializes this object's index to F.storage. Returns this. */
158
+ storeIndex: function(){
159
+ if(this.index) F.storage.setJSON(this.keys.index,this.index);
160
+ return this;
161
+ },
162
+ /** Updates the stash record for the given finfo
163
+ and (optionally) content. If passed 1 arg, only
164
+ the finfo stash is updated, else both the finfo
165
+ and its contents are (re-)stashed. Returns this.
166
+ */
167
+ updateFile: function(finfo,content){
168
+ const ndx = this.getIndex(),
169
+ key = this.indexKey(finfo),
170
+ old = ndx[key];
171
+ const record = old || (ndx[key]={
172
+ checkin: finfo.checkin,
173
+ filename: finfo.filename,
174
+ mimetype: finfo.mimetype
175
+ });
176
+ record.isExe = !!finfo.isExe;
177
+ record.stashTime = new Date().getTime();
178
+ this.storeIndex();
179
+ if(arguments.length>1){
180
+ F.storage.set(this.contentKey(key), content);
181
+ }
182
+ this._fireStashEvent();
183
+ return this;
184
+ },
185
+ /**
186
+ Returns the stashed content, if any, for the given finfo
187
+ object.
188
+ */
189
+ stashedContent: function(finfo){
190
+ return F.storage.get(this.contentKey(this.indexKey(finfo)));
191
+ },
192
+ /** Returns true if we have stashed content for the given finfo
193
+ record. */
194
+ hasStashedContent: function(finfo){
195
+ return F.storage.contains(this.contentKey(this.indexKey(finfo)));
196
+ },
197
+ /** Unstashes the given finfo record and its content.
198
+ Returns this. */
199
+ unstash: function(finfo){
200
+ const ndx = this.getIndex(),
201
+ key = this.indexKey(finfo);
202
+ delete finfo.stashTime;
203
+ delete ndx[key];
204
+ F.storage.remove(this.contentKey(key));
205
+ this.storeIndex();
206
+ this._fireStashEvent();
207
+ return this;
208
+ },
209
+ /**
210
+ Clears all $stash entries from F.storage. Returns this.
211
+ */
212
+ clear: function(){
213
+ const ndx = this.getIndex(),
214
+ self = this;
215
+ let count = 0;
216
+ Object.keys(ndx).forEach(function(k){
217
+ ++count;
218
+ const e = ndx[k];
219
+ delete ndx[k];
220
+ F.storage.remove(self.contentKey(k));
221
+ });
222
+ F.storage.remove(this.keys.index);
223
+ delete this.index;
224
+ if(count) this._fireStashEvent();
225
+ return this;
226
+ },
227
+ /**
228
+ Removes all but the maxCount most-recently-updated stash
229
+ entries, where maxCount defaults to this.prune.defaultMaxCount.
230
+ */
231
+ prune: function f(maxCount){
232
+ const ndx = this.getIndex();
233
+ const li = [];
234
+ if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
235
+ Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
236
+ li.sort((l,r)=>l.stashTime - r.stashTime);
237
+ let n = 0;
238
+ while(li.length>maxCount){
239
+ ++n;
240
+ const e = li.shift();
241
+ this._disableNextEvent = true;
242
+ this.unstash(e);
243
+ console.warn("Pruned oldest stash entry:",e);
244
+ }
245
+ if(n) this._fireStashEvent();
246
+ }
247
+ };
248
+ $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize;
249
+
63250
/**
64251
Widget for the checkin/file selection list.
65252
*/
66
- P.fileSelector = {
253
+ P.fileSelectWidget = {
67254
e:{
68255
container: E('#fileedit-file-selector')
69256
},
70257
finfo: {},
71258
cache: {
72259
checkins: undefined,
73
- files:{}
260
+ files:{},
261
+ branchNames: {}
74262
},
75263
/**
76264
Fetches the list of leaf checkins from the server and updates
77265
the UI with that list.
78266
*/
@@ -93,10 +281,11 @@
93281
self.cache.checkins = list;
94282
D.clearElement(D.enable(self.e.selectCi));
95283
let loadThisOne;
96284
list.forEach(function(o,n){
97285
if(!n) loadThisOne = o;
286
+ self.cache.branchNames[F.hashDigits(o.checkin)] = o.branch;
98287
D.option(self.e.selectCi, o.checkin,
99288
o.timestamp+' ['+o.branch+']: '
100289
+F.hashDigits(o.checkin));
101290
});
102291
self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
@@ -151,10 +340,20 @@
151340
responseType: 'json',
152341
onload
153342
});
154343
return this;
155344
},
345
+
346
+ /**
347
+ If this object has ever loaded the given checkin version via
348
+ loadLeaves(), this returns the branch name associated with that
349
+ version, else returns undefined;
350
+ */
351
+ checkinBranchName: function(uuid){
352
+ return this.cache.branchNames[F.hashDigits(uuid)];
353
+ },
354
+
156355
/**
157356
Initializes the checkin/file selector widget. Must only be
158357
called once.
159358
*/
160359
init: function(){
@@ -206,12 +405,108 @@
206405
btnReload.addEventListener(
207406
'click', (e)=>this.loadLeaves(), false
208407
);
209408
delete this.init;
210409
}
211
- }/*P.fileSelector*/;
410
+ }/*P.fileSelectWidget*/;
212411
412
+ /**
413
+ Widget for listing and selecting $stash entries.
414
+ */
415
+ P.stashWidget = {
416
+ e:{/*DOM element(s)*/},
417
+ init: function(domInsertPoint/*insert widget BEFORE this element*/){
418
+ const flow = D.addClass(D.div(), 'flex-container','flex-column');
419
+ const wrapper = D.addClass(
420
+ D.attr(D.div(),'id','fileedit-stash-selector'),
421
+ 'input-with-label'
422
+ );
423
+ const sel = this.e.select = D.select();
424
+ const btnClear = this.e.btnClear
425
+ = D.addClass(D.button("Clear"),'hidden');
426
+ D.append(flow, wrapper);
427
+ D.append(wrapper, "Local edits ("+(F.storage.storageImplName())+"):",
428
+ sel, btnClear);
429
+ D.attr(wrapper, "title", [
430
+ 'Locally "stashed" edits. Timestamps are the last local edit time.',
431
+ 'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
432
+ 'combinations are retained.',
433
+ 'Committing or reloading a file removes it from this stash.'
434
+ ].join(' '));
435
+ D.option(D.disable(sel), "(empty)");
436
+ F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
437
+ F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
438
+ sel.addEventListener('change',function(e){
439
+ const opt = this.selectedOptions[0];
440
+ if(opt && opt._finfo) P.loadFile(opt._finfo);
441
+ });
442
+ F.confirmer(btnClear, {
443
+ confirmText: "REALLY delete ALL local edits?",
444
+ onconfirm: (e)=>P.clearStash().loadFile(/*in case P.finfo() was in the stash*/),
445
+ ticks: 3
446
+ });
447
+ if(F.storage.isTransient()){/*Warn if transient storage is in use...*/
448
+ D.append(flow, D.append(D.addClass(D.div(),'warning'),
449
+ "Warning: persistent storage is not avaible, "+
450
+ "so uncomitted edits will not survive a page reload.")
451
+ );
452
+ }
453
+ domInsertPoint.parentNode.insertBefore(flow, domInsertPoint);
454
+ $stash._fireStashEvent(/*update this object with the load-time stash*/);
455
+ delete this.init;
456
+ },
457
+ /**
458
+ Regenerates the edit selection list.
459
+ */
460
+ updateList: function f(stasher,theFinfo){
461
+ if(!f.compare){
462
+ const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1);
463
+ f.compare = function(l,r){
464
+ const cmp = cmpBase(l.filename, r.filename);
465
+ return cmp ? cmp : cmpBase(l.checkin, r.checkin);
466
+ };
467
+ f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */;
468
+ const pad=(x)=>(''+x).length>1 ? x : '0'+x;
469
+ f.timestring = function ff(d){
470
+ return [
471
+ d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()),
472
+ '@',pad(d.getHours()),':',pad(d.getMinutes())
473
+ ].join('');
474
+ };
475
+ }
476
+ const index = stasher.getIndex(), ilist = [];
477
+ Object.keys(index).forEach((finfo)=>{
478
+ ilist.push(index[finfo]);
479
+ });
480
+ const self = this;
481
+ D.clearElement(this.e.select);
482
+ if(0===ilist.length){
483
+ D.addClass(this.e.btnClear, 'hidden');
484
+ D.option(D.disable(this.e.select),"No local edits");
485
+ return;
486
+ }
487
+ D.enable(this.e.select);
488
+ D.removeClass(this.e.btnClear, 'hidden');
489
+ D.disable(D.option(this.e.select,0,"Select a local edit..."));
490
+ const currentFinfo = theFinfo || P.finfo || {};
491
+ ilist.sort(f.compare).forEach(function(finfo,n){
492
+ const key = stasher.indexKey(finfo),
493
+ branch = P.fileSelectWidget.checkinBranchName(finfo.checkin);
494
+ const opt = D.option(
495
+ self.e.select, n+1/*value is (almost) irrelevant*/,
496
+ [F.hashDigits(finfo.checkin, 6), branch,
497
+ f.timestring(new Date(finfo.stashTime)),
498
+ false ? finfo.filename : F.shortenFilename(finfo.filename)
499
+ ].join(' ')
500
+ );
501
+ opt._finfo = finfo;
502
+ if(0===f.compare(currentFinfo, finfo)){
503
+ D.attr(opt, 'selected', true);
504
+ }
505
+ });
506
+ }
507
+ }/*P.stashWidget*/;
213508
214509
/**
215510
Internal workaround to select the current preview mode
216511
and fire a change event if the value actually changes
217512
or if forceEvent is truthy.
@@ -227,11 +522,11 @@
227522
// Force UI update
228523
s.dispatchEvent(new Event('change',{target:s}));
229524
}
230525
};
231526
232
- window.addEventListener("load", function() {
527
+ F.onPageLoad(function() {
233528
P.base = {tag: E('base')};
234529
P.base.originalHref = P.base.tag.href;
235530
P.tabs = new fossil.TabManager('#fileedit-tabs');
236531
P.e = {
237532
taEditor: E('#fileedit-content-editor'),
@@ -258,11 +553,10 @@
258553
preview: E('#fileedit-tab-preview'),
259554
diff: E('#fileedit-tab-diff'),
260555
commit: E('#fileedit-tab-commit')
261556
}
262557
};
263
- P.fileSelector.init();
264558
/* Figure out which comment editor to show by default and
265559
hide the other one. By default we take the one which does
266560
not have the 'hidden' CSS class. If neither do, we default
267561
to single-line mode. */
268562
if(D.hasClass(P.e.taCommentSmall, 'hidden')){
@@ -384,29 +678,13 @@
384678
// Clear diff/preview when new content is loaded/set
385679
'fileedit-content-replaced',
386680
()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
387681
);
388682
389
- /* Tell the user about which fossil.storage is being used... */
390
- let storageMsg = D.addClass(D.div(),'flex-container','flex-row',
391
- 'fileedit-hint');
392
- if(F.storage.isTransient()){
393
- D.append(
394
- D.addClass(storageMsg,'warning'),
395
- "Warning: persistent storage is not avaible, "+
396
- "so unsaved edits "+
397
- "will not survive a page reload."
398
- );
399
- }else{
400
- D.append(
401
- storageMsg,
402
- "Current storage mechanism for local edits: "+
403
- F.storage.storageImplName()
404
- );
405
- }
406
- P.e.tabs.content.insertBefore(storageMsg, P.e.tabs.content.lastElementChild);
407
- }, false)/*onload event handler*/;
683
+ P.fileSelectWidget.init();
684
+ P.stashWidget.init(P.e.tabs.content.lastElementChild);
685
+ }/*F.onPageLoad()*/);
408686
409687
/**
410688
Getter (if called with no args) or setter (if passed an arg) for
411689
the current file content.
412690
@@ -608,13 +886,19 @@
608886
it triggers a 'fileedit-file-loaded' event, passing it
609887
this.finfo.
610888
*/
611889
P.loadFile = function(file,rev){
612890
if(0===arguments.length){
891
+ /* Reload from this.finfo */
613892
if(!affirmHasFile()) return this;
614893
file = this.finfo.filename;
615894
rev = this.finfo.checkin;
895
+ }else if(1===arguments.length){
896
+ /* Assume finfo-like object */
897
+ const arg = arguments[0];
898
+ file = arg.filename;
899
+ rev = arg.checkin;
616900
}
617901
const self = this;
618902
const onload = (r,headers)=>{
619903
delete self.finfo;
620904
self.updateVersion({
@@ -796,11 +1080,11 @@
7961080
self.unstashContent(oldFinfo);
7971081
delete c.manifest;
7981082
self.finfo = c;
7991083
self.e.taComment.value = '';
8001084
self.updateVersion();
801
- self.fileSelector.loadLeaves();
1085
+ self.fileSelectWidget.loadLeaves();
8021086
}
8031087
F.message.apply(F, msg);
8041088
self.tabs.switchToTab(self.e.tabs.commit);
8051089
};
8061090
}
@@ -843,176 +1127,10 @@
8431127
onload: f.onload
8441128
});
8451129
return this;
8461130
};
8471131
848
- /**
849
- $stash is an internal-use-only object for managing "stashed"
850
- local edits, to help avoid that users accidentally lose content
851
- by switching tabs or following links or some such. The basic
852
- theory of operation is...
853
-
854
- All "stashed" state is stored using fossil.storage.
855
-
856
- - When the current file content is modified by the user, the
857
- current stathe of the current P.finfo and its the content
858
- is stashed. For the built-in editor widget, "changes" is
859
- notified via a 'change' event. For a client-side custom
860
- widget, the client needs to call P.stashContentChange() when
861
- their widget triggers the equivalent of a 'change' event.
862
-
863
- - For certain non-content updates (as of this writing, only the
864
- is-executable checkbox), only the P.finfo stash entry is
865
- updated, not the content (unless the content has not yet been
866
- stashed, in which case it is also stashed so that the stash
867
- always has matching pairs of finfo/content).
868
-
869
- - When saving, the stashed entry for the previous version is removed
870
- from the stash.
871
-
872
- - When "loading", we use any stashed state for the given
873
- checkin/file combination. When forcing a re-load of content,
874
- any stashed entry for that combination is removed from the
875
- stash.
876
-
877
- - Every time P.stashContentChange() updates the stash, it is
878
- pruned to $stash.prune.defaultMaxCount most-recently-updated
879
- entries.
880
-
881
- - This API often refers to "finfo objects." Those are objects
882
- with a minimum of {checkin,filename} properties (which must be
883
- valid), and a combination of those two properties is used as
884
- basis for the stash keys for any given checkin/filename
885
- combination.
886
-
887
- The structure of the stash is a bit convoluted for efficiency's
888
- sake: we store a map of file info (finfo) objects separately from
889
- those files' contents because otherwise we would be required to
890
- JSONize/de-JSONize the file content when stashing/restoring it,
891
- and that would be horribly inefficient (meaning "battery-consuming"
892
- on mobile devices).
893
- */
894
- const $stash = {
895
- keys: {
896
- index: F.page.name+':index'
897
- },
898
- /**
899
- index: {
900
- "CHECKIN_HASH:FILENAME": {file info w/o content}
901
- ...
902
- }
903
-
904
- In F.storage we...
905
-
906
- - Store this.index under the key this.keys.index.
907
-
908
- - Store each file's content under the key
909
- (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
910
- from the index entries to avoid having to JSONize/de-JSONize
911
- the content. The assumption/hope is that the browser can store
912
- those records "directly," without any intermediary
913
- encoding/decoding going on.
914
- */
915
- indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
916
- /** Returns the key for storing content for the given key suffix,
917
- by prepending P.name to suffix. */
918
- contentKey: function(suffix){return P.name+'/'+suffix},
919
- /** Returns the index object, fetching it from the stash or creating
920
- it anew on the first call. */
921
- getIndex: function(){
922
- if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
923
- return this.index;
924
- },
925
- /**
926
- Returns the stashed version, if any, for the given finfo object.
927
- */
928
- getFinfo: function(finfo){
929
- const ndx = this.getIndex();
930
- return ndx[this.indexKey(finfo)];
931
- },
932
- /** Serializes this object's index to F.storage. Returns this. */
933
- storeIndex: function(){
934
- if(this.index) F.storage.setJSON(this.keys.index,this.index);
935
- return this;
936
- },
937
- /** Updates the stash record for the given finfo
938
- and (optionally) content. If passed 1 arg, only
939
- the finfo stash is updated, else both the finfo
940
- and its contents are (re-)stashed. Returns this.
941
- */
942
- updateFile: function(finfo,content){
943
- const ndx = this.getIndex(),
944
- key = this.indexKey(finfo);
945
- const record = ndx[key] || (ndx[key]={
946
- checkin: finfo.checkin,
947
- filename: finfo.filename,
948
- mimetype: finfo.mimetype
949
- });
950
- record.isExe = !!finfo.isExe;
951
- record.stashTime = new Date().getTime();
952
- this.storeIndex();
953
- if(arguments.length>1){
954
- F.storage.set(this.contentKey(key), content);
955
- }
956
- return this;
957
- },
958
- /**
959
- Returns the stashed content, if any, for the given finfo
960
- object.
961
- */
962
- stashedContent: function(finfo){
963
- return F.storage.get(this.contentKey(this.indexKey(finfo)));
964
- },
965
- /** Returns true if we have stashed content for the given finfo
966
- record. */
967
- hasStashedContent: function(finfo){
968
- return F.storage.contains(this.contentKey(this.indexKey(finfo)));
969
- },
970
- /** Unstashes the given finfo record and its content.
971
- Returns this. */
972
- unstash: function(finfo){
973
- const ndx = this.getIndex(),
974
- key = this.indexKey(finfo);
975
- delete finfo.stashTime;
976
- delete ndx[key];
977
- F.storage.remove(this.contentKey(key));
978
- return this.storeIndex();
979
- },
980
- /**
981
- Clears all $stash entries from F.storage. Returns this.
982
- */
983
- clear: function(){
984
- const ndx = this.getIndex(),
985
- self = this;
986
- Object.keys(ndx).forEach(function(k){
987
- const e = ndx[k];
988
- delete ndx[k];
989
- F.storage.remove(self.contentKey(k));
990
- });
991
- F.storage.remove(this.keys.index);
992
- delete this.index;
993
- return this;
994
- },
995
- /**
996
- Removes all but the maxCount most-recently-updated stash
997
- entries, where maxCount defaults to this.prune.defaultMaxCount.
998
- */
999
- prune: function f(maxCount){
1000
- const ndx = this.getIndex();
1001
- const li = [];
1002
- if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
1003
- Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
1004
- li.sort((l,r)=>l.stashTime - r.stashTime);
1005
- while(li.length>maxCount){
1006
- const e = li.shift();
1007
- this.unstash(e);
1008
- console.warn("Pruned oldest stash entry:",e);
1009
- }
1010
- }
1011
- };
1012
- $stash.prune.defaultMaxCount = 7;
1013
-
10141132
/**
10151133
Updates P.finfo for certain state and stashes P.finfo, with the
10161134
current content fetched via P.fileContent().
10171135
10181136
If passed truthy AND the stash already has stashed content for
@@ -1070,8 +1188,6 @@
10701188
*/
10711189
P.getStashedFinfo = function(finfo){
10721190
return $stash.getFinfo(finfo);
10731191
};
10741192
1075
- P.$stash = $stash /*only for testing/debugging */;
1076
-
10771193
})(window.fossil);
10781194
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1,10 +1,12 @@
1 (function(F/*the fossil object*/){
2 "use strict";
3 /**
4 Code for the /filepage app. Requires that the fossil JS
5 bootstrapping is complete and fossil.fetch() has been installed.
 
 
6
7 Custom events, handled via fossil.page.addEventListener():
8
9 - Event 'fileedit-file-loaded': passes on information when it
10 loads a file, in the form of an object:
@@ -58,21 +60,207 @@
58 */
59 const E = (s)=>document.querySelector(s),
60 D = F.dom,
61 P = F.page;
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63 /**
64 Widget for the checkin/file selection list.
65 */
66 P.fileSelector = {
67 e:{
68 container: E('#fileedit-file-selector')
69 },
70 finfo: {},
71 cache: {
72 checkins: undefined,
73 files:{}
 
74 },
75 /**
76 Fetches the list of leaf checkins from the server and updates
77 the UI with that list.
78 */
@@ -93,10 +281,11 @@
93 self.cache.checkins = list;
94 D.clearElement(D.enable(self.e.selectCi));
95 let loadThisOne;
96 list.forEach(function(o,n){
97 if(!n) loadThisOne = o;
 
98 D.option(self.e.selectCi, o.checkin,
99 o.timestamp+' ['+o.branch+']: '
100 +F.hashDigits(o.checkin));
101 });
102 self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
@@ -151,10 +340,20 @@
151 responseType: 'json',
152 onload
153 });
154 return this;
155 },
 
 
 
 
 
 
 
 
 
 
156 /**
157 Initializes the checkin/file selector widget. Must only be
158 called once.
159 */
160 init: function(){
@@ -206,12 +405,108 @@
206 btnReload.addEventListener(
207 'click', (e)=>this.loadLeaves(), false
208 );
209 delete this.init;
210 }
211 }/*P.fileSelector*/;
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
214 /**
215 Internal workaround to select the current preview mode
216 and fire a change event if the value actually changes
217 or if forceEvent is truthy.
@@ -227,11 +522,11 @@
227 // Force UI update
228 s.dispatchEvent(new Event('change',{target:s}));
229 }
230 };
231
232 window.addEventListener("load", function() {
233 P.base = {tag: E('base')};
234 P.base.originalHref = P.base.tag.href;
235 P.tabs = new fossil.TabManager('#fileedit-tabs');
236 P.e = {
237 taEditor: E('#fileedit-content-editor'),
@@ -258,11 +553,10 @@
258 preview: E('#fileedit-tab-preview'),
259 diff: E('#fileedit-tab-diff'),
260 commit: E('#fileedit-tab-commit')
261 }
262 };
263 P.fileSelector.init();
264 /* Figure out which comment editor to show by default and
265 hide the other one. By default we take the one which does
266 not have the 'hidden' CSS class. If neither do, we default
267 to single-line mode. */
268 if(D.hasClass(P.e.taCommentSmall, 'hidden')){
@@ -384,29 +678,13 @@
384 // Clear diff/preview when new content is loaded/set
385 'fileedit-content-replaced',
386 ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
387 );
388
389 /* Tell the user about which fossil.storage is being used... */
390 let storageMsg = D.addClass(D.div(),'flex-container','flex-row',
391 'fileedit-hint');
392 if(F.storage.isTransient()){
393 D.append(
394 D.addClass(storageMsg,'warning'),
395 "Warning: persistent storage is not avaible, "+
396 "so unsaved edits "+
397 "will not survive a page reload."
398 );
399 }else{
400 D.append(
401 storageMsg,
402 "Current storage mechanism for local edits: "+
403 F.storage.storageImplName()
404 );
405 }
406 P.e.tabs.content.insertBefore(storageMsg, P.e.tabs.content.lastElementChild);
407 }, false)/*onload event handler*/;
408
409 /**
410 Getter (if called with no args) or setter (if passed an arg) for
411 the current file content.
412
@@ -608,13 +886,19 @@
608 it triggers a 'fileedit-file-loaded' event, passing it
609 this.finfo.
610 */
611 P.loadFile = function(file,rev){
612 if(0===arguments.length){
 
613 if(!affirmHasFile()) return this;
614 file = this.finfo.filename;
615 rev = this.finfo.checkin;
 
 
 
 
 
616 }
617 const self = this;
618 const onload = (r,headers)=>{
619 delete self.finfo;
620 self.updateVersion({
@@ -796,11 +1080,11 @@
796 self.unstashContent(oldFinfo);
797 delete c.manifest;
798 self.finfo = c;
799 self.e.taComment.value = '';
800 self.updateVersion();
801 self.fileSelector.loadLeaves();
802 }
803 F.message.apply(F, msg);
804 self.tabs.switchToTab(self.e.tabs.commit);
805 };
806 }
@@ -843,176 +1127,10 @@
843 onload: f.onload
844 });
845 return this;
846 };
847
848 /**
849 $stash is an internal-use-only object for managing "stashed"
850 local edits, to help avoid that users accidentally lose content
851 by switching tabs or following links or some such. The basic
852 theory of operation is...
853
854 All "stashed" state is stored using fossil.storage.
855
856 - When the current file content is modified by the user, the
857 current stathe of the current P.finfo and its the content
858 is stashed. For the built-in editor widget, "changes" is
859 notified via a 'change' event. For a client-side custom
860 widget, the client needs to call P.stashContentChange() when
861 their widget triggers the equivalent of a 'change' event.
862
863 - For certain non-content updates (as of this writing, only the
864 is-executable checkbox), only the P.finfo stash entry is
865 updated, not the content (unless the content has not yet been
866 stashed, in which case it is also stashed so that the stash
867 always has matching pairs of finfo/content).
868
869 - When saving, the stashed entry for the previous version is removed
870 from the stash.
871
872 - When "loading", we use any stashed state for the given
873 checkin/file combination. When forcing a re-load of content,
874 any stashed entry for that combination is removed from the
875 stash.
876
877 - Every time P.stashContentChange() updates the stash, it is
878 pruned to $stash.prune.defaultMaxCount most-recently-updated
879 entries.
880
881 - This API often refers to "finfo objects." Those are objects
882 with a minimum of {checkin,filename} properties (which must be
883 valid), and a combination of those two properties is used as
884 basis for the stash keys for any given checkin/filename
885 combination.
886
887 The structure of the stash is a bit convoluted for efficiency's
888 sake: we store a map of file info (finfo) objects separately from
889 those files' contents because otherwise we would be required to
890 JSONize/de-JSONize the file content when stashing/restoring it,
891 and that would be horribly inefficient (meaning "battery-consuming"
892 on mobile devices).
893 */
894 const $stash = {
895 keys: {
896 index: F.page.name+':index'
897 },
898 /**
899 index: {
900 "CHECKIN_HASH:FILENAME": {file info w/o content}
901 ...
902 }
903
904 In F.storage we...
905
906 - Store this.index under the key this.keys.index.
907
908 - Store each file's content under the key
909 (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
910 from the index entries to avoid having to JSONize/de-JSONize
911 the content. The assumption/hope is that the browser can store
912 those records "directly," without any intermediary
913 encoding/decoding going on.
914 */
915 indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
916 /** Returns the key for storing content for the given key suffix,
917 by prepending P.name to suffix. */
918 contentKey: function(suffix){return P.name+'/'+suffix},
919 /** Returns the index object, fetching it from the stash or creating
920 it anew on the first call. */
921 getIndex: function(){
922 if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
923 return this.index;
924 },
925 /**
926 Returns the stashed version, if any, for the given finfo object.
927 */
928 getFinfo: function(finfo){
929 const ndx = this.getIndex();
930 return ndx[this.indexKey(finfo)];
931 },
932 /** Serializes this object's index to F.storage. Returns this. */
933 storeIndex: function(){
934 if(this.index) F.storage.setJSON(this.keys.index,this.index);
935 return this;
936 },
937 /** Updates the stash record for the given finfo
938 and (optionally) content. If passed 1 arg, only
939 the finfo stash is updated, else both the finfo
940 and its contents are (re-)stashed. Returns this.
941 */
942 updateFile: function(finfo,content){
943 const ndx = this.getIndex(),
944 key = this.indexKey(finfo);
945 const record = ndx[key] || (ndx[key]={
946 checkin: finfo.checkin,
947 filename: finfo.filename,
948 mimetype: finfo.mimetype
949 });
950 record.isExe = !!finfo.isExe;
951 record.stashTime = new Date().getTime();
952 this.storeIndex();
953 if(arguments.length>1){
954 F.storage.set(this.contentKey(key), content);
955 }
956 return this;
957 },
958 /**
959 Returns the stashed content, if any, for the given finfo
960 object.
961 */
962 stashedContent: function(finfo){
963 return F.storage.get(this.contentKey(this.indexKey(finfo)));
964 },
965 /** Returns true if we have stashed content for the given finfo
966 record. */
967 hasStashedContent: function(finfo){
968 return F.storage.contains(this.contentKey(this.indexKey(finfo)));
969 },
970 /** Unstashes the given finfo record and its content.
971 Returns this. */
972 unstash: function(finfo){
973 const ndx = this.getIndex(),
974 key = this.indexKey(finfo);
975 delete finfo.stashTime;
976 delete ndx[key];
977 F.storage.remove(this.contentKey(key));
978 return this.storeIndex();
979 },
980 /**
981 Clears all $stash entries from F.storage. Returns this.
982 */
983 clear: function(){
984 const ndx = this.getIndex(),
985 self = this;
986 Object.keys(ndx).forEach(function(k){
987 const e = ndx[k];
988 delete ndx[k];
989 F.storage.remove(self.contentKey(k));
990 });
991 F.storage.remove(this.keys.index);
992 delete this.index;
993 return this;
994 },
995 /**
996 Removes all but the maxCount most-recently-updated stash
997 entries, where maxCount defaults to this.prune.defaultMaxCount.
998 */
999 prune: function f(maxCount){
1000 const ndx = this.getIndex();
1001 const li = [];
1002 if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
1003 Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
1004 li.sort((l,r)=>l.stashTime - r.stashTime);
1005 while(li.length>maxCount){
1006 const e = li.shift();
1007 this.unstash(e);
1008 console.warn("Pruned oldest stash entry:",e);
1009 }
1010 }
1011 };
1012 $stash.prune.defaultMaxCount = 7;
1013
1014 /**
1015 Updates P.finfo for certain state and stashes P.finfo, with the
1016 current content fetched via P.fileContent().
1017
1018 If passed truthy AND the stash already has stashed content for
@@ -1070,8 +1188,6 @@
1070 */
1071 P.getStashedFinfo = function(finfo){
1072 return $stash.getFinfo(finfo);
1073 };
1074
1075 P.$stash = $stash /*only for testing/debugging */;
1076
1077 })(window.fossil);
1078
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -1,10 +1,12 @@
1 (function(F/*the fossil object*/){
2 "use strict";
3 /**
4 Code for the /filepage app. Requires that the fossil JS
5 bootstrapping is complete and that several fossil JS APIs have
6 been installed: fossil.fetch, fossil.dom, fossil.tabs,
7 fossil.storage, fossil.confirmer.
8
9 Custom events, handled via fossil.page.addEventListener():
10
11 - Event 'fileedit-file-loaded': passes on information when it
12 loads a file, in the form of an object:
@@ -58,21 +60,207 @@
60 */
61 const E = (s)=>document.querySelector(s),
62 D = F.dom,
63 P = F.page;
64
65 P.config = {
66 defaultMaxStashSize: 7
67 };
68
69 /**
70 $stash is an internal-use-only object for managing "stashed"
71 local edits, to help avoid that users accidentally lose content
72 by switching tabs or following links or some such. The basic
73 theory of operation is...
74
75 All "stashed" state is stored using fossil.storage.
76
77 - When the current file content is modified by the user, the
78 current stathe of the current P.finfo and its the content
79 is stashed. For the built-in editor widget, "changes" is
80 notified via a 'change' event. For a client-side custom
81 widget, the client needs to call P.stashContentChange() when
82 their widget triggers the equivalent of a 'change' event.
83
84 - For certain non-content updates (as of this writing, only the
85 is-executable checkbox), only the P.finfo stash entry is
86 updated, not the content (unless the content has not yet been
87 stashed, in which case it is also stashed so that the stash
88 always has matching pairs of finfo/content).
89
90 - When saving, the stashed entry for the previous version is removed
91 from the stash.
92
93 - When "loading", we use any stashed state for the given
94 checkin/file combination. When forcing a re-load of content,
95 any stashed entry for that combination is removed from the
96 stash.
97
98 - Every time P.stashContentChange() updates the stash, it is
99 pruned to $stash.prune.defaultMaxCount most-recently-updated
100 entries.
101
102 - This API often refers to "finfo objects." Those are objects
103 with a minimum of {checkin,filename} properties (which must be
104 valid), and a combination of those two properties is used as
105 basis for the stash keys for any given checkin/filename
106 combination.
107
108 The structure of the stash is a bit convoluted for efficiency's
109 sake: we store a map of file info (finfo) objects separately from
110 those files' contents because otherwise we would be required to
111 JSONize/de-JSONize the file content when stashing/restoring it,
112 and that would be horribly inefficient (meaning "battery-consuming"
113 on mobile devices).
114 */
115 const $stash = {
116 keys: {
117 index: F.page.name+':index'
118 },
119 /**
120 index: {
121 "CHECKIN_HASH:FILENAME": {file info w/o content}
122 ...
123 }
124
125 In F.storage we...
126
127 - Store this.index under the key this.keys.index.
128
129 - Store each file's content under the key
130 (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
131 from the index entries to avoid having to JSONize/de-JSONize
132 the content. The assumption/hope is that the browser can store
133 those records "directly," without any intermediary
134 encoding/decoding going on.
135 */
136 indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
137 /** Returns the key for storing content for the given key suffix,
138 by prepending P.name to suffix. */
139 contentKey: function(suffix){return P.name+'/'+suffix},
140 /** Returns the index object, fetching it from the stash or creating
141 it anew on the first call. */
142 getIndex: function(){
143 if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
144 return this.index;
145 },
146 _fireStashEvent: function(){
147 if(this._disableNextEvent) delete this._disableNextEvent;
148 else F.page.dispatchEvent('fileedit-stash-updated', this);
149 },
150 /**
151 Returns the stashed version, if any, for the given finfo object.
152 */
153 getFinfo: function(finfo){
154 const ndx = this.getIndex();
155 return ndx[this.indexKey(finfo)];
156 },
157 /** Serializes this object's index to F.storage. Returns this. */
158 storeIndex: function(){
159 if(this.index) F.storage.setJSON(this.keys.index,this.index);
160 return this;
161 },
162 /** Updates the stash record for the given finfo
163 and (optionally) content. If passed 1 arg, only
164 the finfo stash is updated, else both the finfo
165 and its contents are (re-)stashed. Returns this.
166 */
167 updateFile: function(finfo,content){
168 const ndx = this.getIndex(),
169 key = this.indexKey(finfo),
170 old = ndx[key];
171 const record = old || (ndx[key]={
172 checkin: finfo.checkin,
173 filename: finfo.filename,
174 mimetype: finfo.mimetype
175 });
176 record.isExe = !!finfo.isExe;
177 record.stashTime = new Date().getTime();
178 this.storeIndex();
179 if(arguments.length>1){
180 F.storage.set(this.contentKey(key), content);
181 }
182 this._fireStashEvent();
183 return this;
184 },
185 /**
186 Returns the stashed content, if any, for the given finfo
187 object.
188 */
189 stashedContent: function(finfo){
190 return F.storage.get(this.contentKey(this.indexKey(finfo)));
191 },
192 /** Returns true if we have stashed content for the given finfo
193 record. */
194 hasStashedContent: function(finfo){
195 return F.storage.contains(this.contentKey(this.indexKey(finfo)));
196 },
197 /** Unstashes the given finfo record and its content.
198 Returns this. */
199 unstash: function(finfo){
200 const ndx = this.getIndex(),
201 key = this.indexKey(finfo);
202 delete finfo.stashTime;
203 delete ndx[key];
204 F.storage.remove(this.contentKey(key));
205 this.storeIndex();
206 this._fireStashEvent();
207 return this;
208 },
209 /**
210 Clears all $stash entries from F.storage. Returns this.
211 */
212 clear: function(){
213 const ndx = this.getIndex(),
214 self = this;
215 let count = 0;
216 Object.keys(ndx).forEach(function(k){
217 ++count;
218 const e = ndx[k];
219 delete ndx[k];
220 F.storage.remove(self.contentKey(k));
221 });
222 F.storage.remove(this.keys.index);
223 delete this.index;
224 if(count) this._fireStashEvent();
225 return this;
226 },
227 /**
228 Removes all but the maxCount most-recently-updated stash
229 entries, where maxCount defaults to this.prune.defaultMaxCount.
230 */
231 prune: function f(maxCount){
232 const ndx = this.getIndex();
233 const li = [];
234 if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
235 Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
236 li.sort((l,r)=>l.stashTime - r.stashTime);
237 let n = 0;
238 while(li.length>maxCount){
239 ++n;
240 const e = li.shift();
241 this._disableNextEvent = true;
242 this.unstash(e);
243 console.warn("Pruned oldest stash entry:",e);
244 }
245 if(n) this._fireStashEvent();
246 }
247 };
248 $stash.prune.defaultMaxCount = P.config.defaultMaxStashSize;
249
250 /**
251 Widget for the checkin/file selection list.
252 */
253 P.fileSelectWidget = {
254 e:{
255 container: E('#fileedit-file-selector')
256 },
257 finfo: {},
258 cache: {
259 checkins: undefined,
260 files:{},
261 branchNames: {}
262 },
263 /**
264 Fetches the list of leaf checkins from the server and updates
265 the UI with that list.
266 */
@@ -93,10 +281,11 @@
281 self.cache.checkins = list;
282 D.clearElement(D.enable(self.e.selectCi));
283 let loadThisOne;
284 list.forEach(function(o,n){
285 if(!n) loadThisOne = o;
286 self.cache.branchNames[F.hashDigits(o.checkin)] = o.branch;
287 D.option(self.e.selectCi, o.checkin,
288 o.timestamp+' ['+o.branch+']: '
289 +F.hashDigits(o.checkin));
290 });
291 self.loadFiles(loadThisOne ? loadThisOne.checkin : false);
@@ -151,10 +340,20 @@
340 responseType: 'json',
341 onload
342 });
343 return this;
344 },
345
346 /**
347 If this object has ever loaded the given checkin version via
348 loadLeaves(), this returns the branch name associated with that
349 version, else returns undefined;
350 */
351 checkinBranchName: function(uuid){
352 return this.cache.branchNames[F.hashDigits(uuid)];
353 },
354
355 /**
356 Initializes the checkin/file selector widget. Must only be
357 called once.
358 */
359 init: function(){
@@ -206,12 +405,108 @@
405 btnReload.addEventListener(
406 'click', (e)=>this.loadLeaves(), false
407 );
408 delete this.init;
409 }
410 }/*P.fileSelectWidget*/;
411
412 /**
413 Widget for listing and selecting $stash entries.
414 */
415 P.stashWidget = {
416 e:{/*DOM element(s)*/},
417 init: function(domInsertPoint/*insert widget BEFORE this element*/){
418 const flow = D.addClass(D.div(), 'flex-container','flex-column');
419 const wrapper = D.addClass(
420 D.attr(D.div(),'id','fileedit-stash-selector'),
421 'input-with-label'
422 );
423 const sel = this.e.select = D.select();
424 const btnClear = this.e.btnClear
425 = D.addClass(D.button("Clear"),'hidden');
426 D.append(flow, wrapper);
427 D.append(wrapper, "Local edits ("+(F.storage.storageImplName())+"):",
428 sel, btnClear);
429 D.attr(wrapper, "title", [
430 'Locally "stashed" edits. Timestamps are the last local edit time.',
431 'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
432 'combinations are retained.',
433 'Committing or reloading a file removes it from this stash.'
434 ].join(' '));
435 D.option(D.disable(sel), "(empty)");
436 F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
437 F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
438 sel.addEventListener('change',function(e){
439 const opt = this.selectedOptions[0];
440 if(opt && opt._finfo) P.loadFile(opt._finfo);
441 });
442 F.confirmer(btnClear, {
443 confirmText: "REALLY delete ALL local edits?",
444 onconfirm: (e)=>P.clearStash().loadFile(/*in case P.finfo() was in the stash*/),
445 ticks: 3
446 });
447 if(F.storage.isTransient()){/*Warn if transient storage is in use...*/
448 D.append(flow, D.append(D.addClass(D.div(),'warning'),
449 "Warning: persistent storage is not avaible, "+
450 "so uncomitted edits will not survive a page reload.")
451 );
452 }
453 domInsertPoint.parentNode.insertBefore(flow, domInsertPoint);
454 $stash._fireStashEvent(/*update this object with the load-time stash*/);
455 delete this.init;
456 },
457 /**
458 Regenerates the edit selection list.
459 */
460 updateList: function f(stasher,theFinfo){
461 if(!f.compare){
462 const cmpBase = (l,r)=>l<r ? -1 : (l===r ? 0 : 1);
463 f.compare = function(l,r){
464 const cmp = cmpBase(l.filename, r.filename);
465 return cmp ? cmp : cmpBase(l.checkin, r.checkin);
466 };
467 f.rxZ = /\.\d+Z$/ /* ms and 'Z' part of date string */;
468 const pad=(x)=>(''+x).length>1 ? x : '0'+x;
469 f.timestring = function ff(d){
470 return [
471 d.getFullYear(),'-',pad(d.getMonth()+1/*sigh*/),'-',pad(d.getDate()),
472 '@',pad(d.getHours()),':',pad(d.getMinutes())
473 ].join('');
474 };
475 }
476 const index = stasher.getIndex(), ilist = [];
477 Object.keys(index).forEach((finfo)=>{
478 ilist.push(index[finfo]);
479 });
480 const self = this;
481 D.clearElement(this.e.select);
482 if(0===ilist.length){
483 D.addClass(this.e.btnClear, 'hidden');
484 D.option(D.disable(this.e.select),"No local edits");
485 return;
486 }
487 D.enable(this.e.select);
488 D.removeClass(this.e.btnClear, 'hidden');
489 D.disable(D.option(this.e.select,0,"Select a local edit..."));
490 const currentFinfo = theFinfo || P.finfo || {};
491 ilist.sort(f.compare).forEach(function(finfo,n){
492 const key = stasher.indexKey(finfo),
493 branch = P.fileSelectWidget.checkinBranchName(finfo.checkin);
494 const opt = D.option(
495 self.e.select, n+1/*value is (almost) irrelevant*/,
496 [F.hashDigits(finfo.checkin, 6), branch,
497 f.timestring(new Date(finfo.stashTime)),
498 false ? finfo.filename : F.shortenFilename(finfo.filename)
499 ].join(' ')
500 );
501 opt._finfo = finfo;
502 if(0===f.compare(currentFinfo, finfo)){
503 D.attr(opt, 'selected', true);
504 }
505 });
506 }
507 }/*P.stashWidget*/;
508
509 /**
510 Internal workaround to select the current preview mode
511 and fire a change event if the value actually changes
512 or if forceEvent is truthy.
@@ -227,11 +522,11 @@
522 // Force UI update
523 s.dispatchEvent(new Event('change',{target:s}));
524 }
525 };
526
527 F.onPageLoad(function() {
528 P.base = {tag: E('base')};
529 P.base.originalHref = P.base.tag.href;
530 P.tabs = new fossil.TabManager('#fileedit-tabs');
531 P.e = {
532 taEditor: E('#fileedit-content-editor'),
@@ -258,11 +553,10 @@
553 preview: E('#fileedit-tab-preview'),
554 diff: E('#fileedit-tab-diff'),
555 commit: E('#fileedit-tab-commit')
556 }
557 };
 
558 /* Figure out which comment editor to show by default and
559 hide the other one. By default we take the one which does
560 not have the 'hidden' CSS class. If neither do, we default
561 to single-line mode. */
562 if(D.hasClass(P.e.taCommentSmall, 'hidden')){
@@ -384,29 +678,13 @@
678 // Clear diff/preview when new content is loaded/set
679 'fileedit-content-replaced',
680 ()=>D.clearElement(P.e.diffTarget, P.e.previewTarget)
681 );
682
683 P.fileSelectWidget.init();
684 P.stashWidget.init(P.e.tabs.content.lastElementChild);
685 }/*F.onPageLoad()*/);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
687 /**
688 Getter (if called with no args) or setter (if passed an arg) for
689 the current file content.
690
@@ -608,13 +886,19 @@
886 it triggers a 'fileedit-file-loaded' event, passing it
887 this.finfo.
888 */
889 P.loadFile = function(file,rev){
890 if(0===arguments.length){
891 /* Reload from this.finfo */
892 if(!affirmHasFile()) return this;
893 file = this.finfo.filename;
894 rev = this.finfo.checkin;
895 }else if(1===arguments.length){
896 /* Assume finfo-like object */
897 const arg = arguments[0];
898 file = arg.filename;
899 rev = arg.checkin;
900 }
901 const self = this;
902 const onload = (r,headers)=>{
903 delete self.finfo;
904 self.updateVersion({
@@ -796,11 +1080,11 @@
1080 self.unstashContent(oldFinfo);
1081 delete c.manifest;
1082 self.finfo = c;
1083 self.e.taComment.value = '';
1084 self.updateVersion();
1085 self.fileSelectWidget.loadLeaves();
1086 }
1087 F.message.apply(F, msg);
1088 self.tabs.switchToTab(self.e.tabs.commit);
1089 };
1090 }
@@ -843,176 +1127,10 @@
1127 onload: f.onload
1128 });
1129 return this;
1130 };
1131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1132 /**
1133 Updates P.finfo for certain state and stashes P.finfo, with the
1134 current content fetched via P.fileContent().
1135
1136 If passed truthy AND the stash already has stashed content for
@@ -1070,8 +1188,6 @@
1188 */
1189 P.getStashedFinfo = function(finfo){
1190 return $stash.getFinfo(finfo);
1191 };
1192
 
 
1193 })(window.fossil);
1194
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -85,15 +85,16 @@
8585
8686
- Which storage is used is unspecified and may differ across
8787
environments.
8888
- If neither of those is available, the storage is transient and
8989
will not survive a page reload.
90
-- It stores only the most recent last 7 checkin/file combinations
91
- which have been modified. Note that changing the "executable bit"
92
- is counted as a modification, but the checkin comment is not
93
- stored separately for each file. If the limit is exceeded, it
94
- silently discards the oldest edits.
90
+- It stores only the most recent checkin/file combinations which have
91
+ been modified (exactly how many may differ - the number will be
92
+ noted somewhere in the UI). Note that changing the "executable bit"
93
+ is counted as a modification, but the checkin comment is not stored
94
+ separately for each file. If the limit is exceeded, it silently
95
+ discards the oldest edits.
9596
9697
Exactly how long `fileStorage` will survive, and how much it or
9798
`sessionStorage` can hold, is environment-dependent. `sessionStorage`
9899
will survive until the current browser tab is closed, but it survives
99100
across reloads of the same tab.
100101
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -85,15 +85,16 @@
85
86 - Which storage is used is unspecified and may differ across
87 environments.
88 - If neither of those is available, the storage is transient and
89 will not survive a page reload.
90 - It stores only the most recent last 7 checkin/file combinations
91 which have been modified. Note that changing the "executable bit"
92 is counted as a modification, but the checkin comment is not
93 stored separately for each file. If the limit is exceeded, it
94 silently discards the oldest edits.
 
95
96 Exactly how long `fileStorage` will survive, and how much it or
97 `sessionStorage` can hold, is environment-dependent. `sessionStorage`
98 will survive until the current browser tab is closed, but it survives
99 across reloads of the same tab.
100
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -85,15 +85,16 @@
85
86 - Which storage is used is unspecified and may differ across
87 environments.
88 - If neither of those is available, the storage is transient and
89 will not survive a page reload.
90 - It stores only the most recent checkin/file combinations which have
91 been modified (exactly how many may differ - the number will be
92 noted somewhere in the UI). Note that changing the "executable bit"
93 is counted as a modification, but the checkin comment is not stored
94 separately for each file. If the limit is exceeded, it silently
95 discards the oldest edits.
96
97 Exactly how long `fileStorage` will survive, and how much it or
98 `sessionStorage` can hold, is environment-dependent. `sessionStorage`
99 will survive until the current browser tab is closed, but it survives
100 across reloads of the same tab.
101

Keyboard Shortcuts

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