StrictDoc Documentation
strictdoc/export/html/_static/project_tree.js
Source file coverage
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 taken
14
// from strictdoc/export/html/_static/source-code-coverage.js
15
// 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&nbsp;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
})();