<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This file provides browser-agnostic event listeners under the form of
 * RxJS Observables
 */

import {
  NEVER,
  Observable,
  fromEvent as observableFromEvent,
  merge as observableMerge,
} from "rxjs";
import config from "../config";
import log from "../log";
import { IEventEmitter } from "../utils/event_emitter";
import isNonEmptyString from "../utils/is_non_empty_string";
import isNullOrUndefined from "../utils/is_null_or_undefined";
import noop from "../utils/noop";
import createSharedReference, {
  IReadOnlySharedReference,
} from "../utils/reference";
import { CancellationSignal } from "../utils/task_canceller";
import {
  ICompatDocument,
  ICompatHTMLMediaElement,
  ICompatPictureInPictureWindow,
} from "./browser_compatibility_types";
import isNode from "./is_node";
import shouldFavourCustomSafariEME from "./should_favour_custom_safari_EME";

const BROWSER_PREFIXES = ["", "webkit", "moz", "ms"];

const pixelRatio = isNode ||
                   window.devicePixelRatio == null ||
                   window.devicePixelRatio === 0 ? 1 :
                                                   window.devicePixelRatio;

/**
 * Find the first supported event from the list given.
 * @param {HTMLElement} element
 * @param {string} eventNameSuffix
 * @returns {Boolean}
 */
function isEventSupported(
  element : HTMLElement,
  eventNameSuffix : string
) : boolean {
  const clone = document.createElement(element.tagName);
  const eventName = "on" + eventNameSuffix;
  if (eventName in clone) {
    return true;
  } else {
    clone.setAttribute(eventName, "return;");
    return typeof (
      clone as HTMLElement & Partial<Record<string, unknown>>
    )[eventName] === "function";
  }
}

/**
 * Find the first supported event from the list given.
 * @param {HTMLElement} element
 * @param {Array.<string>} eventNames
 * @returns {string|undefined}
 */
function findSupportedEvent(
  element : HTMLElement,
  eventNames : string[]
) : string|undefined {
  return eventNames
    .filter((name) => isEventSupported(element, name))[0];
}

/**
 * @param {Array.<string>} eventNames
 * @param {Array.<string>|undefined} prefixes
 * @returns {Array.<string>}
 */
function eventPrefixed(eventNames : string[], prefixes? : string[]) : string[] {
  return eventNames.reduce((parent : string[], name : string) =>
    parent.concat((prefixes == null ? BROWSER_PREFIXES :
                                      prefixes)
      .map((p) => p + name)), []);
}

export interface IEventEmitterLike {
  addEventListener : (eventName: string, handler: () => void) => void;
  removeEventListener: (eventName: string, handler: () => void) => void;
}

export type IEventTargetLike = HTMLElement |
                               IEventEmitterLike |
                               IEventEmitter<unknown>;

/**
 * @param {Array.<string>} eventNames
 * @param {Array.<string>|undefined} prefixes
 * @returns {Observable}
 */
function compatibleListener<T extends Event>(
  eventNames : string[],
  prefixes? : string[]
) : (element : IEventTargetLike) => Observable<T> {
  let mem : string|undefined;
  const prefixedEvents = eventPrefixed(eventNames, prefixes);
  return (element : IEventTargetLike) => {
    // if the element is a HTMLElement we can detect
    // the supported event, and memoize it in `mem`
    if (element instanceof HTMLElement) {
      if (typeof mem === "undefined") {
        mem = findSupportedEvent(element, prefixedEvents);
      }

      if (isNonEmptyString(mem)) {
        return observableFromEvent(element, mem) as Observable<T>;
      } else {
        if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.DEV as number) {
          log.warn(`compat: element ${element.tagName}` +
                   " does not support any of these events: " +
                   prefixedEvents.join(", "));
        }
        return NEVER;
      }
    }

    // otherwise, we need to listen to all the events
    // and merge them into one observable sequence
    return observableMerge(...prefixedEvents.map(eventName =>
      observableFromEvent(element, eventName)));
  };
}

/**
 * Returns a reference:
 *   - set to `true` when the document is visible
 *   - set to `false` when the document is hidden
 * @param {Object} stopListening - `CancellationSignal` allowing to free the
 * ressources allocated to update this value.
 * @returns {Object}
 */
function getDocumentVisibilityRef(
  stopListening : CancellationSignal
) : IReadOnlySharedReference<boolean> {
  let prefix : string|undefined;

  const doc = document as ICompatDocument;
  if (doc.hidden != null) {
    prefix = "";
  } else if (doc.mozHidden != null) {
    prefix = "moz";
  } else if (doc.msHidden != null) {
    prefix = "ms";
  } else if (doc.webkitHidden != null) {
    prefix = "webkit";
  }

  const hidden = isNonEmptyString(prefix) ? prefix + "Hidden" :
                                            "hidden";
  const visibilityChangeEvent = isNonEmptyString(prefix) ? prefix + "visibilitychange" :
                                                           "visibilitychange";

  const isHidden = document[hidden as "hidden"];
  const ref = createSharedReference(isHidden);

  addEventListener(document, visibilityChangeEvent, () => {
    const isVisible = !(document[hidden as "hidden"]);
    ref.setValueIfChanged(isVisible);
  }, stopListening);

  stopListening.register(() => {
    ref.finish();
  });

  return ref;
}

/**
 * Returns a reference:
 *   - Set to `true` when the current page is considered visible and active.
 *   - Set to `false` otherwise.<commit_msg>Fix document visibility being set to the opposite of what we want initially<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * This file provides browser-agnostic event listeners under the form of
 * RxJS Observables
 */

import {
  NEVER,
  Observable,
  fromEvent as observableFromEvent,
  merge as observableMerge,
} from "rxjs";
import config from "../config";
import log from "../log";
import { IEventEmitter } from "../utils/event_emitter";
import isNonEmptyString from "../utils/is_non_empty_string";
import isNullOrUndefined from "../utils/is_null_or_undefined";
import noop from "../utils/noop";
import createSharedReference, {
  IReadOnlySharedReference,
} from "../utils/reference";
import { CancellationSignal } from "../utils/task_canceller";
import {
  ICompatDocument,
  ICompatHTMLMediaElement,
  ICompatPictureInPictureWindow,
} from "./browser_compatibility_types";
import isNode from "./is_node";
import shouldFavourCustomSafariEME from "./should_favour_custom_safari_EME";

const BROWSER_PREFIXES = ["", "webkit", "moz", "ms"];

const pixelRatio = isNode ||
                   window.devicePixelRatio == null ||
                   window.devicePixelRatio === 0 ? 1 :
                                                   window.devicePixelRatio;

/**
 * Find the first supported event from the list given.
 * @param {HTMLElement} element
 * @param {string} eventNameSuffix
 * @returns {Boolean}
 */
function isEventSupported(
  element : HTMLElement,
  eventNameSuffix : string
) : boolean {
  const clone = document.createElement(element.tagName);
  const eventName = "on" + eventNameSuffix;
  if (eventName in clone) {
    return true;
  } else {
    clone.setAttribute(eventName, "return;");
    return typeof (
      clone as HTMLElement & Partial<Record<string, unknown>>
    )[eventName] === "function";
  }
}

/**
 * Find the first supported event from the list given.
 * @param {HTMLElement} element
 * @param {Array.<string>} eventNames
 * @returns {string|undefined}
 */
function findSupportedEvent(
  element : HTMLElement,
  eventNames : string[]
) : string|undefined {
  return eventNames
    .filter((name) => isEventSupported(element, name))[0];
}

/**
 * @param {Array.<string>} eventNames
 * @param {Array.<string>|undefined} prefixes
 * @returns {Array.<string>}
 */
function eventPrefixed(eventNames : string[], prefixes? : string[]) : string[] {
  return eventNames.reduce((parent : string[], name : string) =>
    parent.concat((prefixes == null ? BROWSER_PREFIXES :
                                      prefixes)
      .map((p) => p + name)), []);
}

export interface IEventEmitterLike {
  addEventListener : (eventName: string, handler: () => void) => void;
  removeEventListener: (eventName: string, handler: () => void) => void;
}

export type IEventTargetLike = HTMLElement |
                               IEventEmitterLike |
                               IEventEmitter<unknown>;

/**
 * @param {Array.<string>} eventNames
 * @param {Array.<string>|undefined} prefixes
 * @returns {Observable}
 */
function compatibleListener<T extends Event>(
  eventNames : string[],
  prefixes? : string[]
) : (element : IEventTargetLike) => Observable<T> {
  let mem : string|undefined;
  const prefixedEvents = eventPrefixed(eventNames, prefixes);
  return (element : IEventTargetLike) => {
    // if the element is a HTMLElement we can detect
    // the supported event, and memoize it in `mem`
    if (element instanceof HTMLElement) {
      if (typeof mem === "undefined") {
        mem = findSupportedEvent(element, prefixedEvents);
      }

      if (isNonEmptyString(mem)) {
        return observableFromEvent(element, mem) as Observable<T>;
      } else {
        if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.DEV as number) {
          log.warn(`compat: element ${element.tagName}` +
                   " does not support any of these events: " +
                   prefixedEvents.join(", "));
        }
        return NEVER;
      }
    }

    // otherwise, we need to listen to all the events
    // and merge them into one observable sequence
    return observableMerge(...prefixedEvents.map(eventName =>
      observableFromEvent(element, eventName)));
  };
}

/**
 * Returns a reference:
 *   - set to `true` when the document is visible
 *   - set to `false` when the document is hidden
 * @param {Object} stopListening - `CancellationSignal` allowing to free the
 * ressources allocated to update this value.
 * @returns {Object}
 */
function getDocumentVisibilityRef(
  stopListening : CancellationSignal
) : IReadOnlySharedReference<boolean> {
  let prefix : string|undefined;

  const doc = document as ICompatDocument;
  if (doc.hidden != null) {
    prefix = "";
  } else if (doc.mozHidden != null) {
    prefix = "moz";
  } else if (doc.msHidden != null) {
    prefix = "ms";
  } else if (doc.webkitHidden != null) {
    prefix = "webkit";
  }

  const hidden = isNonEmptyString(prefix) ? prefix + "Hidden" :
                                            "hidden";
  const visibilityChangeEvent = isNonEmptyString(prefix) ? prefix + "visibilitychange" :
                                                           "visibilitychange";

  const isHidden = document[hidden as "hidden"];
  const ref = createSharedReference(!isHidden);

  addEventListener(document, visibilityChangeEvent, () => {
    const isVisible = !(document[hidden as "hidden"]);
    ref.setValueIfChanged(isVisible);
  }, stopListening);

  stopListening.register(() => {
    ref.finish();
  });

  return ref;
}

/**
 * Returns a reference:
 *   - Set to `true` when the current page is considered visible and active.
 *   - Set to `false` otherwise.
{'fraction_non_alphanumeric': 0.11676188943429629, 'fraction_numerical': 0.0017703387784662428, 'mean_word_length': 3.105715229600264, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 14, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4486554', 'n_tokens_mistral': 3452, 'n_tokens_neox': 3419, 'n_words': 1045}