All files / app/services/visibletoloadtext visible-to-load-text.service.ts

100% Statements 59/59
100% Branches 17/17
100% Functions 14/14
100% Lines 59/59

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245                                    1x         244x                                                                                 244x           244x                   244x 244x 244x   244x 54x   244x 40x   244x 244x 244x 244x 244x                 194x 194x                   285x 285x 285x 285x 285x 285x                 253x 253x 61x   253x 253x 253x                 426x                       1x 2x                   137x 133x 133x 133x 133x     4x 4x 4x 4x                     179x                 448x       16x 432x 432x         174x 174x           1x 254x 158x                   20x     12x         6x   12x 10x 10x     20x      
import { Injectable, OnDestroy } from '@angular/core';
import { ComponentWithText } from 'src/app/interfaces/ComponentWithText';
import { WindowScrollService } from '../windowScrollService/window-scroll.service';
import { WindowResizeService } from '../windowResizeService/window-resize.service';
import { Subscription } from 'rxjs';
import { DOMComputationService } from '../domcomputation/domcomputation.service';
import { debounce } from 'src/scripts/tools/debounce/debounce';
 
/**
 * Service responsible for managing {@link ComponentWithText} so that only
 * components in the viewport actually make the API calls to get the texts. To
 * avoid a somewhat disturbing user experience of having components loading
 * while scrolling or resizing, the loaded zone is actually a little bigger than
 * the viewport.
 */
@Injectable({
  providedIn: 'root',
})
export class VisibleToLoadTextService implements OnDestroy {
  /**
   * {@link ComponentWithText} managed. The visible components will load their
   * texts when appropriate.
   */
  subscribers: ComponentWithText[] = [];
 
  /** Map containing the visibility of each {@link ComponentWithText}. */
  visibility: Map<ComponentWithText, boolean>;
  /**
   * Map containing whether or not each {@link ComponentWithText} as loaded their
   * texts. This is important so that the {@link ComponentWithText} do not reload
   * their texts after it already has been done, and so that preloaders can be
   * displayed while the texts are loading.
   */
  loaded: Map<ComponentWithText, boolean>;
  /**
   * Map containing whether or not each {@link ComponentWithText} is currently
   * loading. This is important so that the {@link ComponentWithText} do not
   * reload their texts when they are already loading.
   */
  loading: Map<ComponentWithText, boolean>;
 
  /**
   * Map containing whether or not each {@link ComponentWithText} should reload.
   * Usefull for instance when a language change occurs while a component is
   * already loading.
   */
  toReload: Map<ComponentWithText, boolean>;
  /**
   * Allow any {@link ComponentWithText} to be loaded when appropriate only once.
   * Usefull for language names for instance, which are loaded in their onw
   * languages and thus do not need to be reloaded on language change.
   */
  onlyOnce: Map<ComponentWithText, boolean>;
 
  /** Scroll subscription to the {@link WindowScrollService} scroll observable */
  scroll: Subscription;
  /** Resize subscription to the {@link WindowResizeService} resize observable */
  resize: Subscription;
 
  /**
   * Buffer factor for the height. For instance, a buffer factor of 0 means no
   * buffer, a buffer factor of 1 means that the viewport height is extended
   * (both up and down) by another viewport height.
   */
  bufferFactorHeight = 0.5;
  /**
   * Buffer factor for the width. For instance, a buffer factor of 0 means no
   * buffer, a buffer factor of 1 means that the viewport width is extended
   * (both left and right) by another viewport width.
   */
  bufferFactorWidth = 0.25;
 
  /**
   * VisibleToLoadText service constructor
   *
   * @param windowScrollService The {@link WindowScrollService}
   * @param windowResizeService The {@link WindowResizeService}
   * @param domComputationService The {@link DOMComputationService}
   */
  constructor(
    private windowScrollService: WindowScrollService,
    private windowResizeService: WindowResizeService,
    private domComputationService: DOMComputationService
  ) {
    this.scroll = windowScrollService.scroll.subscribe(() => {
      this.loadNewTexts();
    });
    this.resize = windowResizeService.resize.subscribe(() => {
      this.loadNewTexts();
    });
    this.visibility = new Map<ComponentWithText, boolean>();
    this.loaded = new Map<ComponentWithText, boolean>();
    this.loading = new Map<ComponentWithText, boolean>();
    this.toReload = new Map<ComponentWithText, boolean>();
    this.onlyOnce = new Map<ComponentWithText, boolean>();
  }
 
  /**
   * On destroy, the service has to be unsubscribed from the scroll observable
   * from the {@link WindowScrollService} and the resize observable from the
   * {@link WindowResizeService}.
   */
  ngOnDestroy(): void {
    this.scroll.unsubscribe();
    this.resize.unsubscribe();
  }
 
  /**
   * Let a {@link ComponentWithText} subscribe to this observer to be notified
   * when appropriate.
   *
   * @param s The {@link ComponentWithText}
   */
  subscribe(s: ComponentWithText, onlyOnce = false) {
    this.subscribers.push(s);
    this.loaded.set(s, false);
    this.loading.set(s, false);
    this.toReload.set(s, false);
    this.onlyOnce.set(s, onlyOnce);
    this.loadNewTextsOf(s);
  }
 
  /**
   * Let a {@link ComponentWithText} unsubscribe to this observer.
   *
   * @param s The {@link ComponentWithText}
   */
  unsubscribe(s: ComponentWithText) {
    const index = this.subscribers.indexOf(s);
    if (index > -1) {
      this.subscribers.splice(index, 1);
    }
    this.visibility.delete(s);
    this.loading.delete(s);
    this.loaded.delete(s);
  }
 
  /**
   * Update the visibility of a single {@link ComponentWithText}
   *
   * @param comp The {@link ComponentWithText}
   */
  private updateVisibilityOf(comp: ComponentWithText) {
    this.visibility.set(
      comp,
      this.domComputationService.isIntoView(
        comp.getElement(),
        this.bufferFactorHeight,
        this.bufferFactorWidth
      )
    );
  }
 
  /** Update the visibility of all {@link ComponentWithText} */
  updateVisibility() {
    for (const comp of this.subscribers) {
      this.updateVisibilityOf(comp);
    }
  }
 
  /**
   * Indicates that a {@link ComponentWithText} has finished loading the texts.
   *
   * @param comp The {@link ComponentWithText}
   */
  textLoaded(comp: ComponentWithText) {
    if (!this.toReload.get(comp)) {
      this.loaded.set(comp, true);
      this.loading.set(comp, false);
      setTimeout(() => {
        this.loadNewTexts();
      }, 0);
    } else {
      this.toReload.set(comp, false);
      this.loaded.set(comp, false);
      comp.updateTexts();
      this.loading.set(comp, true);
    }
  }
 
  /**
   * Wheter or not the text of a {@link ComponentWithText} has loaded.
   *
   * @param comp The {@link ComponentWithText}
   * @returns Whether or not the text as loaded
   */
  hasTextLoaded(comp: ComponentWithText) {
    return this.loaded.get(comp);
  }
 
  /**
   * Load new texts of a single {@link ComponentWithText} when appropriate.
   *
   * @param comp The {@link ComponentWithText}
   */
  loadNewTextsOf(comp: ComponentWithText) {
    if (
      (this.loading.get(comp) || this.loaded.get(comp)) &&
      this.onlyOnce.get(comp)
    )
      return;
    this.updateVisibilityOf(comp);
    if (
      this.visibility.get(comp) &&
      !this.loaded.get(comp) &&
      !this.loading.get(comp)
    ) {
      comp.updateTexts();
      this.loading.set(comp, true);
    }
  }
 
  /** Load new texts for all {@link ComponentWithText} when appropriate. */
  @debounce()
  loadNewTexts() {
    for (const comp of this.subscribers) {
      this.loadNewTextsOf(comp);
    }
  }
 
  /**
   * On language change, reset the loaded and loading status of the
   * {@link ComponentWithText} since everything has to be loaded again, and then
   * load the new texts.
   */
  languageChange() {
    for (const comp of this.subscribers) {
      // if the language change occurs during the loading of a component's text and
      // is not into view, the component should reload the texts with the correct language
      if (
        this.loading.get(comp) &&
        !this.loaded.get(comp) &&
        !this.onlyOnce.get(comp)
      ) {
        this.toReload.set(comp, true);
      }
      if (!this.onlyOnce.get(comp)) {
        this.loading.set(comp, false);
        this.loaded.set(comp, false);
      }
    }
    this.loadNewTexts();
  }
}