Path:
strictdoc/export/html/_static/project_tree.js
Lines:
379
Non-empty lines:
325
Non-empty lines covered with requirements:
325 / 325 (100.0%)
Functions:
0
Functions covered by requirements:
0 / 0 (0.0%)
1
//2
// @relation(SDOC-SRS-53, scope=file)3
//4
5
(function () {
6
const FRAME_SELECTOR = '#frame_project_tree';
7
const SWITCH_SELECTOR = '#project_tree_controls';
8
const FRAGMENT_ATTR = 'included-document';
9
const FRAGMENT_SELECTOR = `.project_tree-file[${FRAGMENT_ATTR}]`;
10
const FILE_SELECTOR = `.project_tree-file`;
11
const FOLDER_SELECTOR = `.project_tree-folder`;
12
13
// This class Switch was taken14
// from strictdoc/export/html/_static/source-code-coverage.js15
// and improved a bit:16
class Switch {
17
constructor({
18
callback,
19
labelText,
20
checked,
21
componentClass,
22
colorOn,
23
colorOff,
24
size,
25
stroke,
26
units,
27
position,
28
topPosition,
29
leftPosition,
30
rightPosition,
31
bottomPosition,
32
dataTestID,
33
}) {
34
this.colorOn = colorOn || 'rgb(242, 100, 42)';
35
this.colorOff = colorOff || 'rgb(200, 200, 200)';
36
this.labelInitialText = labelText || '';
37
this.checked = (checked === false) ? false : true;
38
39
this.dataTestID = dataTestID || 'std-switch';
40
this.componentClass = componentClass || 'std-switch-scc';
41
this.size = size || 0.75;
42
this.stroke = stroke || 0.25;
43
this.units = units || 'rem';
44
45
this.topPosition = topPosition || 'unset',
46
this.leftPosition = leftPosition || 'unset',
47
this.rightPosition = rightPosition || 'unset',
48
this.bottomPosition = bottomPosition || 'unset',
49
this.position = position || 'static',
50
51
this.controlLabelTextSpan = document.createElement('span');
52
this.callback = callback;
53
}
54
55
create() {
56
const block = document.createElement('div');
57
block.classList.add(this.componentClass);
58
const label = document.createElement('label');
59
label.classList.add(`${this.componentClass}__label`);
60
const input = document.createElement('input');
61
input.classList.add(`${this.componentClass}__input`);
62
input.type = 'checkbox';
63
input.checked = this.checked;
64
const slider = document.createElement('span');
65
slider.classList.add(`${this.componentClass}__slider`);
66
67
label.append(input, slider, this.controlLabelTextSpan);
68
block.append(label);
69
70
this.insertStyle();
71
this.updateLabelText(this.labelInitialText);
72
input.addEventListener('change', () => this.callback(input.checked));
73
74
label.setAttribute('data-testid', this.dataTestID);
75
76
return block;
77
}
78
79
updateLabelText(text) {
80
this.controlLabelTextSpan.innerHTML = text;
81
}
82
83
insertStyle() {
84
85
const css = `
86
.${this.componentClass} {
87
display: inline-block;88
line-height: 0;89
position: ${this.position};
90
top: ${this.topPosition};
91
left: ${this.leftPosition};
92
right: ${this.rightPosition};
93
bottom: ${this.bottomPosition};
94
}95
.${this.componentClass}__label {
96
display: inline-flex;97
column-gap: 8px;98
line-height: ${this.size * 1.6}${this.units};
99
line-height: 1.25;100
align-items: flex-start;101
justify-content: flex-start;102
user-select: none;103
cursor: pointer;104
font-size: small;105
}106
.${this.componentClass}__input {
107
opacity: 0;108
width: 0;109
height: 0;110
position: absolute;111
}112
.${this.componentClass}__slider {
113
position: relative;114
cursor: pointer;115
background-color: ${this.colorOff};
116
-webkit-transition: .4s;117
transition: .4s;118
display: inline-block;119
width: ${this.size * 2 + this.stroke * 2}${this.units};
120
min-width: ${this.size * 2 + this.stroke * 2}${this.units};
121
height: ${this.size + this.stroke * 2}${this.units};
122
margin-right: ${this.size * 0.5}${this.units};
123
border-radius: ${this.size * 0.5 + this.stroke}${this.units};
124
}125
.${this.componentClass}__slider::before {
126
position: absolute;127
content: "";128
height: ${this.size}${this.units};
129
width: ${this.size}${this.units};
130
left: ${this.stroke}${this.units};
131
bottom: ${this.stroke}${this.units};
132
background-color: white;133
-webkit-transition: .4s;134
transition: .4s;135
border-radius: 50%;136
}137
input:checked + .${this.componentClass}__slider {
138
background-color: ${this.colorOn};
139
}140
input:focus + .${this.componentClass}__slider {
141
box-shadow: 0 0 1px ${this.colorOn};
142
}143
input:checked + .${this.componentClass}__slider::before {
144
-webkit-transform: translateX(${this.size}${this.units});
145
-ms-transform: translateX(${this.size}${this.units});
146
transform: translateX(${this.size}${this.units});
147
}148
`;
149
150
const head = document.querySelector('head');
151
const style = document.createElement('style');
152
style.append(document.createTextNode(css));
153
style.setAttribute("data-slider-styles", '');
154
head.append(style);
155
}
156
157
}158
159
class ProjectTree {
160
constructor({
161
mutatingFrame,
162
controlTarget
163
}) {
164
this.mutatingFrame = mutatingFrame;
165
this.controlTarget = controlTarget;
166
this.fragments = [];
167
168
this.control;
169
this.controlElement;
170
171
this.state = {
172
fragmentVisibility: {
173
_sessionStorageItemName: 'projectTreeFragmentVisibility',
174
initial: 'hide',
175
current: null,
176
}
177
};
178
}
179
180
init() {
181
// console.log('First time call.');
182
183
console.assert(this.mutatingFrame, `mutatingFrame not found on the page`);
184
this._addMutationObserver();
185
186
this._initStateAndStorage();
187
188
// * this.controlTarget may not be present on the page, for example, in the case of static export.
189
// console.assert(this.controlTarget, `controlTarget not found on the page`);
190
191
this._createControl();
192
this._addControl();
193
194
this.updateFragmentsAndControl();
195
}
196
197
getCurrentFragmentVisibilityBool() {
198
return (this.state.fragmentVisibility.current === 'show') ? true : false;
199
}
200
201
updateFragmentsAndControl() {
202
// Update list of fragment elements (files + folders) and toggle visibility
203
const fileFragments = this._getFileFragments();
204
const folderFragments = this._getFoldersWithOnlyFragments();
205
206
// Store both individual file fragments and folders that contain only fragments
207
this.fragments = [...fileFragments, ...folderFragments];
208
209
// Only show number of file fragments (folders are excluded from count)
210
this._updateControl(fileFragments.length);
211
this._updateFragmentsVisibility(this.getCurrentFragmentVisibilityBool());
212
}
213
214
_getFileFragments() {
215
// Find all file elements that are marked as included fragments
216
return [...this.mutatingFrame.querySelectorAll(FRAGMENT_SELECTOR)];
217
}
218
219
_getFoldersWithOnlyFragments() {
220
// Find folders that contain only fragment files and no other files
221
const folders = [...this.mutatingFrame.querySelectorAll(FOLDER_SELECTOR)];
222
return folders.filter(folder => {
223
// Consider only those folders if it contains only fragment files
224
const fragCount = folder.querySelectorAll(FRAGMENT_SELECTOR).length;
225
const fileCount = folder.querySelectorAll(FILE_SELECTOR).length;
226
return fragCount > 0 && fragCount === fileCount;
227
});
228
}
229
230
_getFragments() {
231
let fragments = [...this.mutatingFrame.querySelectorAll(FRAGMENT_SELECTOR)];
232
const folders = this._prepareFragmentsFolders(fragments);
233
return fragments.concat(folders);
234
}
235
236
_prepareFragmentsFolders(fragments) {
237
// Temporary solution.
238
// It is possible to optimize the search to reduce the number of runs.
239
240
const result = [];
241
// .project_tree-folder > .project_tree-folder-content > .project_tree-file === fragment
242
const folders = [...this.mutatingFrame.querySelectorAll(FOLDER_SELECTOR)];
243
folders.forEach(folder => {
244
const frag = folder.querySelectorAll(FRAGMENT_SELECTOR);
245
const file = folder.querySelectorAll(FILE_SELECTOR);
246
if (frag.length === file.length) {
247
result.push(folder);
248
}
249
});
250
return result
251
}
252
253
_updateFragmentsVisibility(bool) {
254
const display = bool ? '' : 'none';
255
this.fragments.forEach(element => {
256
element.style.display = display;
257
})
258
}
259
260
_updateControl(num) {
261
this.control.updateLabelText(`<b>Show ${num} fragment${num > 1 ? 's' : ''}</b> included in other documents in the Project document tree.`)
262
263
if (num) {
264
this._addControl();
265
} else {
266
this._removeControl();
267
}
268
}
269
270
toggleFragmentsVisibility(checked) {
271
this._updateFragmentsVisibility(checked);
272
273
if (checked) {
274
this._updateCurrentState('show', 'fragmentVisibility');
275
this._setSessionStorageItem('show', 'fragmentVisibility');
276
} else {
277
this._updateCurrentState('hide', 'fragmentVisibility');
278
this._setSessionStorageItem('hide', 'fragmentVisibility');
279
}
280
}
281
282
_initStateAndStorage(option = 'fragmentVisibility') {
283
const storage = this._getSessionStorageItem(option);
284
285
if (storage) {
286
this._updateCurrentState(storage);
287
} else {
288
this._updateCurrentState(this.state[option].initial);
289
this._setSessionStorageItem(this.state[option].initial);
290
}
291
}
292
293
_updateCurrentState(value, option = 'fragmentVisibility') {
294
this.state[option].current = value;
295
}
296
297
_setSessionStorageItem(nextState, option = 'fragmentVisibility') {
298
sessionStorage.setItem(this.state[option]._sessionStorageItemName, nextState);
299
}
300
301
_getSessionStorageItem(option = 'fragmentVisibility') {
302
const storage = sessionStorage.getItem(this.state[option]._sessionStorageItemName);
303
return storage;
304
}
305
306
_createControl() {
307
this.control = new Switch(
308
{
309
labelText: `<b>Show fragments</b>`, // * This text will be updated later.
310
dataTestID: 'show-hide-fragments-toggler',
311
size: 0.5,
312
stroke: 0.175,
313
// position: 'absolute',
314
// topPosition: '16px',
315
// leftPosition: 0,
316
// checked: false,
317
checked: this.getCurrentFragmentVisibilityBool(),
318
callback: (checked) => this.toggleFragmentsVisibility(checked),
319
}
320
);
321
this.controlElement = this.control.create();
322
}
323
324
_addControl() {
325
// * this.controlTarget may not be present on the page, for example, in the case of static export.
326
this.controlTarget && this.controlTarget.append(this.controlElement);
327
}
328
329
_removeControl() {
330
this.controlElement.remove();
331
}
332
333
_addMutationObserver() {
334
// console.log('Mutation observer added for', this.mutatingFrame);
335
336
new MutationObserver((mutationsList, observer) => {
337
for (let mutation of mutationsList) {
338
if (mutation.type === 'childList') {
339
// When re-rendering the frame content,
340
// the array of tracked elements should be updated
341
// and their visibility should be set according
342
// to the current settings available in the State.
343
this.updateFragmentsAndControl();
344
}
345
}
346
}).observe(
347
this.mutatingFrame,
348
{
349
childList: true,
350
// subtree: true
351
}
352
);
353
}
354
}355
356
window.addEventListener("DOMContentLoaded", function(){
357
358
const controlTarget = document.querySelector(SWITCH_SELECTOR);
359
// * This element may not be present on the page, for example, in the case of static export.
360
// if (!controlTarget) {
361
// console.error(`Selector "${SWITCH_SELECTOR}" not found on the page`);
362
// return;
363
// }
364
365
const mutatingFrame = document.querySelector(FRAME_SELECTOR);
366
if (!mutatingFrame) {
367
console.error(`Selector "${FRAME_SELECTOR}" not found on the page`);
368
return;
369
}
370
371
const projectTree = new ProjectTree({
372
mutatingFrame,
373
controlTarget
374
});
375
376
projectTree.init();
377
378
},false);
379
})();