Source: dom/index.js

/**
 * This file is part of Sesatheque.
 *   Copyright 2014-2015, Association Sésamath
 *
 * Sesatheque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * Sesatheque is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Sesatheque (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE,
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */
'use strict'

var log = require('../utils/log')
var hasProp = require('../index').hasProp

/**
 * Ajoute une css dans le <head> de la page
 *
 * Déclaré par init (dès son chargement)
 * @param {string}  file Chemin du fichier css (mis dans href tel quel)
 */
function addCss (file) {
  var head = window.document.getElementsByTagName('head')[0]
  var links = head.getElementsByTagName('link')
  var dejala = false
  for (var i = 0; i < links.length; i++) {
    if (links[i].href === file) {
      dejala = true
      break
    }
  }

  if (dejala) {
    log(file + " était déjà présent, on ne l'ajoute pas")
  } else {
    var elt = window.document.createElement('link')
    elt.rel = 'stylesheet'
    elt.type = 'text/css'
    elt.href = file
    head.appendChild(elt)
  }
}

/**
 * Ajoute un js à la fin du body et appelle la callback quand il est chargé
 * @param {string}  file Chemin du fichier jss (mis dans src tel quel)
 */
function addJs (file, cb) {
  function callCb () {
    cb()
    elt.removeEventListener('load', callCb)
  }
  /**
   * @private
   * @type {HTMLElement}
   */
  var body = window.document.getElementsByTagName('body')[0]
  var elt = window.document.createElement('script')
  elt.type = 'text/javascript'
  // pour que ça marche mieux partout, il vaut mieux mettre le listener onload après avoir mis l'élément dans le dom
  body.appendChild(elt)
  elt.addEventListener('load', callCb)
  // et ensuite indiquer le fichier à charger
  elt.src = file
}

/**
 * Ajoute un élément html de type tag à parent
 * @param {HTMLElement} parent
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} content
 * @returns {HTMLElement} L'élément ajouté
 */
function addElement (parent, tag, attrs, content) {
  var elt = getElement(tag, attrs, content)
  parent.appendChild(elt)

  return elt
}

/**
 * Ajoute un élément html juste après element
 * @param {HTMLElement} element
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} content
 * @returns {HTMLElement} L'élément ajouté
 */
function addElementAfter (element, tag, attrs, content) {
  var newElt = getElement(tag, attrs, content)
  var parent = element.parentNode
  // pas de insertAfter, si nextSibling est null ça le mettra à la fin, cf https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore
  if (parent) parent.insertBefore(newElt, element.nextSibling)
  else log.error(new Error("Navigateur incompatible (pas de parentNode), impossible d'ajouter l'élément"))

  return newElt
}

/**
 * Ajoute un élément html juste avant element
 * @param {HTMLElement} element
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} content
 * @returns {HTMLElement} L'élément ajouté
 */
function addElementBefore (element, tag, attrs, content) {
  var newElt = getElement(tag, attrs, content)
  var parent = element.parentNode
  if (parent) parent.insertBefore(newElt, element)
  else log.error(new Error("Navigateur incompatible (pas de parentNode), impossible d'insérer l'élément"))

  return newElt
}

/**
 * Ajoute un élément html comme premier enfant de parent
 * @param {HTMLElement} parent
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} content
 * @returns {HTMLElement} L'élément ajouté
 */
function addElementFirstChild (parent, tag, attrs, content) {
  var newElt = getElement(tag, attrs, content)
  parent.insertBefore(newElt, parent.firstChild)

  return newElt
}

/**
 * Ajoute un élément html comme frère aîné de elementRef
 * @param {HTMLElement} elementRef
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} content
 * @returns {HTMLElement} L'élément ajouté
 */
function addElementFirstSibling (elementRef, tag, attrs, content) {
  var newElt = getElement(tag, attrs, content)
  elementRef.parentNode.insertBefore(newElt, elementRef.parentNode.firstChild)

  return newElt
}

/**
 * Ajoute du texte dans un élément
 *
 * Déclaré par init (dès son chargement)
 * @param {HTMLElement} elt
 * @param {string} text
 */
function addText (elt, text) {
  elt.appendChild(window.document.createTextNode(text))
}

/**
 * Vide un élément html de tous ses enfants
 *
 * Déclaré par init (dès son chargement)
 * @param {HTMLElement} element
 */
function empty (element) {
  if (element && element.firstChild) {
    while (element.firstChild) element.removeChild(element.firstChild)
  }
}

/**
 * Retourne un élément html de type tag (non inséré dans le dom)
 *
 * Déclaré par init (dès son chargement)
 * @param {string} tag
 * @param {Object=} attrs Les attributs
 * @param {string=} txtContent
 */
function getElement (tag, attrs, txtContent) {
  var elt = window.document.createElement(tag)
  var attr
  try {
    if (attrs) {
      for (attr in attrs) {
        if (hasProp(attrs, attr)) {
          if (attr === 'class') elt.className = attrs.class
          else if (attr === 'className') elt.className = attrs.className
          else if (attr === 'style') setStyles(elt, attrs.style)
          else elt.setAttribute(attr, attrs[attr])
        }
      }
    }
  } catch (error) {
    log('plantage dans getElement ' + tag + ' avec les attributs ', attrs, error)
  }

  if (txtContent) addText(elt, txtContent)

  return elt
}

/**
 * Retourne la taille de la fenêtre
 * @returns {{width: number, height: number}}
 */
function getSize () {
  // suivant que l'on est en mode standard ou quirk faut prendre l'un ou l'autre
  var body = (window.document.compatMode === 'CSS1Compat') ? window.document.documentElement : window.document.body
  // en cas de zoom, window.innerWidth est plus petit, et c'est la bonne valeur
  return {
    width: Math.floor(Math.min(body.clientWidth, window.innerWidth)),
    height: Math.floor(Math.min(body.clientHeight, window.innerHeight))
  }
}

/**
 * Retourne un id qui n'existe pas encore dans le dom (mais ne le créé pas)
 */
var getNewId = (function () {
  // au dela de 10000 id dans un dom y'a un pb !
  var max = 10000
  // une closure pour conserver la valeur de cette variable privée entre 2 appels
  var lastId = 0
  var id = 'sesa' + lastId
  return function getNewId () {
    while (window.document.getElementById(id) && lastId < max) {
      lastId++
      id = 'sesa' + lastId
    }
    if (lastId === max) throw Error('Max de ' + max + ' id générés atteint')

    return id
  }
})()

/**
 * Affecte des styles à un élément html (on peut pas affecter elt.style directement car read only, faut faire du elt.style.foo = bar)
 * sans planter en cas de pb (on le signale juste en console)
 *
 * Déclaré par init (dès son chargement)
 * @param {HTMLElement} elt
 * @param {string|object} styles
 */
function setStyles (elt, styles) {
  try {
    if (elt && elt.style) {
      if (typeof styles === 'string') {
        styles = styles.split(';')
        styles.forEach(function (paire) {
          paire = /([\w]+):(.+)/.exec(paire)
          if (paire && paire.length === 3) {
            var key = paire[1]
            elt.style[key] = paire[2]
          }
        })
      } else if (typeof styles === 'object') {
        for (var prop in styles) {
          if (hasProp(styles, prop)) {
            elt.style[prop] = styles[prop]
          }
        }
      }
    }
  } catch (error) {
    log.error(error)
  }
}

/**
 * Fonctions génériques pour manipuler le dom
 * @service sesajstools/dom
 */
module.exports = {
  // faut rester en es5 :-/ (uglifyJs aime pas si on répète pas `prop: prop`)
  addCss: addCss,
  addJs: addJs,
  addElement: addElement,
  addElementAfter: addElementAfter,
  addElementBefore: addElementBefore,
  addElementFirstChild: addElementFirstChild,
  addElementFirstSibling: addElementFirstSibling,
  addText: addText,
  empty: empty,
  getElement: getElement,
  getSize: getSize,
  getNewId: getNewId,
  setStyles: setStyles
}