Zeptoのスタイル操作メソッドを深掘り

この記事では、DOM要素に対するCSSクラスやスタイルプロパティの操作に焦点を当てて解説します。

キャッシュ付き正規表現生成器

const patternCache = {}

function buildClassPattern(className) {
  return patternCache[className] ||
    (patternCache[className] = new RegExp('(^|\\s)' + className + '(\\s|$)'))
}

指定されたクラス名を含むかどうかを判定する正規表現を生成し、再利用のためにキャッシュします。パターンは「先頭・空白→クラス名→空白・末尾」の構造でマッチします。

自動単位付加ヘルパー

const unitlessProps = { 
  'column-count': 1, 'columns': 1, 'font-weight': 1, 
  'line-height': 1, 'opacity': 1, 'z-index': 1, 'zoom': 1 
}

function autoAppendUnit(propName, val) {
  return typeof val === "number" && !unitlessProps[toDashCase(propName)] 
    ? val + "px" 
    : val
}

数値が渡された場合、px単位が必要なプロパティかどうかを判定して自動的に単位を付加します。フォントサイズや幅などには適用されますが、opacityやz-indexには適用されません。

要素のデフォルト表示モード取得

const displayDefaults = {}

function getDefaultDisplay(tag) {
  if (!displayDefaults[tag]) {
    const tempEl = document.createElement(tag)
    document.body.appendChild(tempEl)
    let disp = window.getComputedStyle(tempEl).display
    tempEl.remove()
    if (disp === "none") disp = "block"
    displayDefaults[tag] = disp
  }
  return displayDefaults[tag]
}

要素のネイティブなdisplay値を取得します。headやstyleタグなどはデフォルトでnoneになるため、その場合はblockにフォールバックします。show()メソッドで正しい表示モードを復元するために使用されます。

関数型引数ハンドラ

function resolveArg(context, arg, index, original) {
  return typeof arg === 'function' 
    ? arg.call(context, index, original) 
    : arg
}

引数が関数の場合、現在の要素とインデックスを渡して実行結果を返します。固定値の場合はそのまま返します。addClassやtoggleClassなどで柔軟な値設定を可能にします。

SVG対応クラス名操作

function getClassNames(el) {
  const klass = el.className || ''
  return klass.baseVal !== undefined ? klass.baseVal : klass
}

function setClassNames(el, names) {
  if (el.className && el.className.baseVal !== undefined) {
    el.className.baseVal = names
  } else {
    el.className = names
  }
}

SVG要素と通常のHTML要素の両方に対応したクラス名の取得・設定を行います。SVGではbaseValプロパティ経由での操作が必要です。

CSSプロパティ操作

css: function(prop, val) {
  if (arguments.length < 2) {
    const el = this[0]
    if (!el) return
    if (typeof prop === 'string') {
      return el.style[toCamelCase(prop)] || 
        getComputedStyle(el).getPropertyValue(prop)
    } else if (Array.isArray(prop)) {
      const result = {}
      const computed = getComputedStyle(el)
      prop.forEach(p => {
        result[p] = el.style[toCamelCase(p)] || computed.getPropertyValue(p)
      })
      return result
    }
  }

  let styleStr = ''
  if (typeof prop === 'string') {
    if (val == null && val !== 0) {
      this.forEach(el => el.style.removeProperty(toDashCase(prop)))
    } else {
      styleStr = toDashCase(prop) + ':' + autoAppendUnit(prop, val)
    }
  } else {
    for (let key in prop) {
      if (prop[key] == null && prop[key] !== 0) {
        this.forEach(el => el.style.removeProperty(toDashCase(key)))
      } else {
        styleStr += toDashCase(key) + ':' + autoAppendUnit(key, prop[key]) + ';'
      }
    }
  }
  
  return this.forEach(el => el.style.cssText += ';' + styleStr)
}

単一または複数のCSSプロパティの取得・設定が可能です。数値には自動でpx単位を付加し、null値を指定するとプロパティを削除します。

表示制御メソッド

hide: function() {
  return this.css("display", "none")
},

show: function() {
  return this.forEach(function(el) {
    if (el.style.display === "none") el.style.display = ''
    if (getComputedStyle(el).display === "none") {
      el.style.display = getDefaultDisplay(el.tagName.toLowerCase())
    }
  })
},

toggle: function(force) {
  return this.forEach(function(el) {
    const $el = $(el)
    const shouldShow = force === undefined 
      ? $el.css("display") === "none" 
      : force
    shouldShow ? $el.show() : $el.hide()
  })
}

hide()は単純にdisplay:noneを設定します。show()は元の表示モードを復元するために計算スタイルを確認し、必要ならデフォルト値を設定します。toggle()は現在の状態に基づいて切り替えます。

クラス操作メソッド

hasClass: function(name) {
  if (!name) return false
  return this.some(el => buildClassPattern(name).test(getClassNames(el)))
},

addClass: function(names) {
  if (!names) return this
  return this.forEach(function(el, idx) {
    if (!('className' in el)) return
    const current = getClassNames(el)
    const toAdd = resolveArg(el, names, idx, current)
    const newClasses = []
    
    toAdd.split(/\s+/).forEach(cls => {
      if (!$.prototype.hasClass.call([el], cls)) {
        newClasses.push(cls)
      }
    })
    
    if (newClasses.length) {
      setClassNames(el, current + (current ? ' ' : '') + newClasses.join(' '))
    }
  })
},

removeClass: function(names) {
  return this.forEach(function(el, idx) {
    if (!('className' in el)) return
    if (names === undefined) {
      setClassNames(el, '')
      return
    }
    
    let current = getClassNames(el)
    const toRemove = resolveArg(el, names, idx, current)
    
    toRemove.split(/\s+/).forEach(cls => {
      current = current.replace(buildClassPattern(cls), ' ')
    })
    
    setClassNames(el, current.trim())
  })
},

toggleClass: function(names, force) {
  if (!names) return this
  return this.forEach(function(el, idx) {
    const $el = $(el)
    const targetClasses = resolveArg(el, names, idx, getClassNames(el))
    
    targetClasses.split(/\s+/).forEach(cls => {
      const shouldAdd = force === undefined ? !$el.hasClass(cls) : force
      shouldAdd ? $el.addClass(cls) : $el.removeClass(cls)
    })
  })
}

クラスの存在確認、追加、削除、切り替えを行う一連のメソッドです。重複追加を防ぎ、不要な空白を除去するなどの配慮がされています。

タグ: zepto javascript CSS dom-manipulation

5月25日 14:09 投稿