<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const React = require('react')
const urlParse = require('url').parse
const windowActions = require('../actions/windowActions')
const appActions = require('../actions/appActions')
const ImmutableComponent = require('./immutableComponent')
const Immutable = require('immutable')
const cx = require('../lib/classSet.js')
const siteUtil = require('../state/siteUtil')
const UrlUtil = require('../lib/urlutil')
const messages = require('../constants/messages.js')
const remote = global.require('electron').remote
const path = require('path')
const contextMenus = require('../contextMenus')
const config = require('../constants/config.js')
const siteHacks = require('../data/siteHacks')
const ipc = global.require('electron').ipcRenderer

import adInfo from '../data/adInfo.js'
import FindBar from './findbar.js'
const { isSourceAboutUrl, getTargetAboutUrl } = require('../lib/appUrlUtil')

class Frame extends ImmutableComponent {
  constructor () {
    super()
  }

  updateWebview () {
    let src = this.props.frame.get('src')
    let location = this.props.frame.get('location')
    let appRoot = window.baseHref
      ? 'file://' + path.resolve(__dirname, '..', '..', 'app') + '/'
      : ''

    let contentScripts = [appRoot + 'content/scripts/webviewPreload.js']
    if (['about:preferences', 'about:bookmarks', 'about:certerror'].includes(location)) {
      contentScripts.push(appRoot + 'content/scripts/aboutPreload.js')
    }

    contentScripts = contentScripts.join(',')
    const contentScriptsChanged =
      this.webview && contentScripts !== this.webview.getAttribute('contentScripts')

    // Create the webview dynamically because React doesn't whitelist all
    // of the attributes we need.  Clear out old webviews if the contentScripts
    // change because they cannot change after being added to the DOM.
    if (contentScriptsChanged) {
      while (this.webviewContainer.firstChild) {
        this.webviewContainer.removeChild(this.webviewContainer.firstChild)
      }
    }
    this.webview = !contentScriptsChanged && this.webview || document.createElement('webview')
    this.webview.setAttribute('allowDisplayingInsecureContent', true)
    this.webview.setAttribute('data-frame-key', this.props.frame.get('key'))
    this.webview.setAttribute('contentScripts', contentScripts)
    if (this.props.frame.get('isPrivate')) {
      this.webview.setAttribute('partition', 'private-1')
    } else if (this.props.frame.get('partitionNumber')) {
      let partition = `persist:partition-${this.props.frame.get('partitionNumber')}`
      ipc.send(messages.INITIALIZE_PARTITION, partition)
      this.webview.setAttribute('partition', partition)
    }
    if (this.props.frame.get('guestInstanceId')) {
      this.webview.setAttribute('data-guest-instance-id', this.props.frame.get('guestInstanceId'))
    }

    const hack = siteHacks[urlParse(location).hostname]
    if (hack && hack.userAgent) {
      this.webview.setAttribute('useragent', hack.userAgent)
    }
    this.webview.setAttribute('src',
                              isSourceAboutUrl(src) ? getTargetAboutUrl(src) : src)
    if (!this.webviewContainer.firstChild) {
      this.webviewContainer.appendChild(this.webview)
      this.addEventListeners()
    }
  }

  componentDidMount () {
    this.updateWebview()
  }

  componentDidUpdate (prevProps, prevState) {
    const didSrcChange = this.props.frame.get('src') !== prevProps.frame.get('src')
    const didLocationChange = this.props.frame.get('location') !== prevProps.frame.get('location')
    // When auto-redirecting to about:certerror, the frame location change and
    // frame src change are emitted separately. Make sure updateWebview is
    // called when the location changes.
    if (didSrcChange || (didLocationChange && this.props.frame.get('location') === 'about:certerror')) {
      this.updateWebview()
    }
    // give focus when switching tabs
    if (this.props.isActive && !prevProps.isActive) {
      this.webview.focus()
    }
    const activeShortcut = this.props.frame.get('activeShortcut')
    switch (activeShortcut) {
      case 'stop':
        this.webview.stop()
        break
      case 'reload':
        if (this.props.frame.get('location') === 'about:preferences') {
          break
        }
        this.webview.reload()
        break
      case 'clean-reload':
        this.webview.reloadIgnoringCache()
        break
      case 'zoom-in':
        windowActions.zoomIn(this.props.frame)
        break
      case 'zoom-out':
        windowActions.zoomOut(this.props.frame)
        break
      case 'zoom-reset':
        windowActions.zoomReset(this.props.frame)
        break
      case 'toggle-dev-tools':
        if (this.webview.isDevToolsOpened()) {
          this.webview.closeDevTools()
        } else {
          this.webview.openDevTools()
        }
        break
      case 'view-source':
        const location = UrlUtil.getViewSourceUrlFromUrl(this.webview.getURL())
        windowActions.newFrame({location}, true)
        // TODO: Make the URL bar show the view-source: prefix
        break
      case 'save':
        // TODO: Sometimes this tries to save in a non-existent directory
        this.webview.downloadURL(this.webview.getURL())
        break
      case 'print':
        this.webview.print()
        break
      case 'show-findbar':
        windowActions.setFindbarShown(this.props.frame, true)
        break
    }
    if (activeShortcut) {
      windowActions.setActiveFrameShortcut(this.props.frame, null)
    }

    if (this.props.frame.get('location') === 'about:preferences') {
      this.webview.send(messages.SETTINGS_UPDATED, this.props.settings.toJS())
    } else if (this.props.frame.get('location') === 'about:bookmarks') {
      this.webview.send(messages.BOOKMARKS_UPDATED, {
        bookmarks: this.props.bookmarks.toJS(),
        bookmarkFolders: this.props.bookmarkFolders.toJS()
      })
    }
  }

  addEventListeners () {
    this.webview.addEventListener('focus', this.onFocus.bind(this))
    // @see new-window event
    this.webview.addEventListener('new-window', (e, url, frameName, disposition, options) => {
      e.preventDefault()

      let guestInstanceId = e.options && e.options.webPreferences && e.options.webPreferences.guestInstanceId
      let windowOpts = e.options && e.options.windowOptions || {}
      windowOpts.parentWindowKey = remote.getCurrentWindow().id
      windowOpts.disposition = e.disposition
      let delayedLoadUrl = e.options && e.options.delayedLoadUrl

      let frameOpts = {
        location: e.url,
        parentFrameKey: this.props.frame.get('key'),
        isPrivate: this.props.frame.get('isPrivate'),
        partitionNumber: this.props.frame.get('partitionNumber'),
        // use the delayed load url for the temporary title
        delayedLoadUrl,
        guestInstanceId
      }

      if (e.disposition === 'new-window' || e.disposition === 'new-popup') {
        appActions.newWindow(frameOpts, windowOpts)
      } else {
        let openInForeground = this.props.prefOpenInForeground === true ||
          e.disposition !== 'background-tab'
        windowActions.newFrame(frameOpts, openInForeground)
      }
    })
    this.webview.addEventListener('destroyed', (e) => {
      this.props.onCloseFrame(this.props.frame)
    })
    this.webview.addEventListener('close', () => {
      this.props.onCloseFrame(this.props.frame)
    })
    this.webview.addEventListener('enter-html-full-screen', () => {
    })
    this.webview.addEventListener('leave-html-full-screen', () => {
    })
    this.webview.addEventListener('page-favicon-updated', (e) => {
      if (e.favicons && e.favicons.length > 0) {
        windowActions.setFavicon(this.props.frame, e.favicons[0])
      }
    })
    this.webview.addEventListener('page-title-updated', ({title}) => {
      windowActions.setFrameTitle(this.props.frame, title)
    })
    this.webview.addEventListener('ipc-message', (e) => {
      let method = () => {}
      switch (e.channel) {
        case messages.THEME_COLOR_COMPUTED:
          method = (computedThemeColor) =>
            windowActions.setThemeColor(this.props.frame, undefined, computedThemeColor || null)
          break
        case messages.CONTEXT_MENU_OPENED:
          method = (nodeProps, contextMenuType) => {
            contextMenus.onMainContextMenu(nodeProps, this.props.frame, contextMenuType)
          }
          break
        case messages.STOP_LOAD:
          method = () => this.webview.stop()
          break
        case messages.LINK_HOVERED:
          method = (href, position) => {
            position = position || {}
            let nearBottom = position.y > (window.innerHeight - 150) // todo: magic number
            let mouseOnLeft = position.x < (window.innerWidth / 2)
            let showOnRight = nearBottom && mouseOnLeft
            windowActions.setLinkHoverPreview(href, showOnRight)
          }
          break
        case messages.NEW_FRAME:
          method = (frameOpts, openInForeground) => {
            windowActions.newFrame(frameOpts, openInForeground)
          }
      }
      method.apply(this, e.args)
    })

    const loadStart = (event) => {
      if (event.isMainFrame && !event.isErrorPage && !event.isFrameSrcDoc) {
        // Temporary workaround for https://github.com/brave/browser-laptop/issues/787
        this.webview.insertCSS('input[type="search"]::-webkit-search-results-decoration { -webkit-appearance: none; }')
        // TODO: These 3 events should be combined into one
        windowActions.onWebviewLoadStart(
          this.props.frame)
        const key = this.props.frame.get('key')
        windowActions.setLocation(event.url, key)
        windowActions.setSecurityState(this.props.frame, {
          secure: urlParse(event.url).protocol === 'https:'
        })
      }
      windowActions.updateBackForwardState(
        this.props.frame,
        this.webview.canGoBack(),
        this.webview.canGoForward())
    }
    const loadEnd = () => {
      windowActions.onWebviewLoadEnd(
        this.props.frame,
        this.webview.getURL())
      if (this.props.enableAds) {
        this.insertAds(this.webview.getURL())
      }
      this.webview.send(messages.POST_PAGE_LOAD_RUN)
      let security = this.props.frame.get('security')
      if (this.props.frame.get('location') === 'about:certerror' &&
          security && security.get('certDetails')) {
        // Don't send certDetails.cert since it is big and crashes the page
        this.webview.send(messages.CERT_DETAILS_UPDATED, {
          url: security.get('certDetails').url,
          error: security.get('certDetails').error
        })
      }
      const protocol = urlParse(this.props.frame.get('location')).protocol
      if (!this.props.frame.get('isPrivate') && (protocol === 'http:' || protocol === 'https:')) {
        // Register the site for recent history for navigation bar
        appActions.addSite(siteUtil.getDetailFromFrame(this.props.frame))
      }
    }
    this.webview.addEventListener('load-commit', (event) => {
      loadStart(event)
    })
    this.webview.addEventListener('load-start', (event) => {
      loadStart(event)
    })
    this.webview.addEventListener('did-navigate', (e) => {
      // only give focus focus is this is not the initial default page load
      if (this.props.isActive && this.webview.canGoBack() && document.activeElement !== this.webview) {
        this.webview.focus()
      }
    })
    this.webview.addEventListener('did-fail-load', () => {
      windowActions.onWebviewLoadEnd(
        this.props.frame,
        this.webview.getURL())
    })
    this.webview.addEventListener('did-finish-load', () => {
      loadEnd()
    })
    this.webview.addEventListener('did-navigate-in-page', () => {
      loadEnd()
    })
    this.webview.addEventListener('media-started-playing', ({title}) => {
      windowActions.setAudioPlaybackActive(this.props.frame, true)
    })
    this.webview.addEventListener('media-paused', ({title}) => {
      windowActions.setAudioPlaybackActive(this.props.frame, false)
    })
    this.webview.addEventListener('did-change-theme-color', ({themeColor}) => {
      // Due to a bug in Electron, after navigating to a page with a theme color
      // to a page without a theme color, the background is sent to us as black
      // even know there is no background. To work around this we just ignore
      // the theme color in that case and let the computed theme color take over.
      windowActions.setThemeColor(this.props.frame, themeColor !== '#000000' ? themeColor : null)
    })
    this.webview.addEventListener('found-in-page', (e) => {
      if (e.result !== undefined && e.result.matches !== undefined) {
        windowActions.setFindDetail(this.props.frame, Immutable.fromJS({
          numberOfMatches: e.result.matches
        }))
      }
    })

    // Ensure we mute appropriately, the initial value could be set
    // from persisted state.
    if (this.props.frame.get('audioMuted')) {
      this.webview.setAudioMuted(true)
    }
  }

  insertAds (currentLocation) {
    const host = new window.URL(currentLocation).hostname.replace('www.', '')
    const adDivCandidates = adInfo[host] || []
    // Call this even when there are no matches because we have some logic
    // to replace common divs.
    this.webview.send(messages.SET_AD_DIV_CANDIDATES, adDivCandidates, config.vault.replacementUrl)
  }

  goBack () {
    this.webview.goBack()
  }

  goForward () {
    this.webview.goForward()
  }

  onFocus () {
    windowActions.setTabPageIndexByFrame(this.props.frame)
    windowActions.setUrlBarActive(false)
    windowActions.setContextMenuDetail()
  }

  onFindHide () {
    windowActions.setFindbarShown(this.props.frame, false)
    this.onClearMatch()
  }

  onFind (searchString, caseSensitivity, forward) {
    if (searchString) {
      this.webview.findInPage(searchString, {
        matchCase: caseSensitivity,
        forward: forward !== undefined ? forward : true,
        findNext: forward !== undefined
      })
    } else {
      this.onClearMatch()
    }
  }

  onClearMatch () {
    this.webview.stopFindInPage('clearSelection')
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.frame.get('audioMuted') &&
      this.props.frame.get('audioMuted') !== true) {
      this.webview.setAudioMuted(true)
    } else if (!nextProps.frame.get('audioMuted') &&
      this.props.frame.get('audioMuted') === true) {
      this.webview.setAudioMuted(false)
    }

    let zoomLevel = nextProps.frame.get('zoomLevel')
    if (zoomLevel !== this.props.frame.get('zoomLevel')) {
      this.webview.setZoomLevel(zoomLevel)
    }
  }

  render () {
    return

      { this.props.frame.get('findbarShown')
      ? <FindBar
        onFind={this.onFind.bind(this)}
        onFindHide={this.onFindHide.bind(this)}
        frame={this.props.frame}
        selected={this.props.frame.get('findbarSelected')}
        findDetail={this.props.frame.get('findDetail')}
      /> : null }

 this.webviewContainer = node}
        className={cx({
          webviewContainer: true,
          isPreview: this.props.isPreview
        })}/>
      { this.props.frame.get('hrefPreview')
        ?

          {this.props.frame.get('hrefPreview')}

 : null
      }

  }
}

module.exports = Frame
<commit_msg>Disable drop for webviews with aboutPreload<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const React = require('react')
const urlParse = require('url').parse
const windowActions = require('../actions/windowActions')
const appActions = require('../actions/appActions')
const ImmutableComponent = require('./immutableComponent')
const Immutable = require('immutable')
const cx = require('../lib/classSet.js')
const siteUtil = require('../state/siteUtil')
const UrlUtil = require('../lib/urlutil')
const messages = require('../constants/messages.js')
const remote = global.require('electron').remote
const path = require('path')
const contextMenus = require('../contextMenus')
const config = require('../constants/config.js')
const siteHacks = require('../data/siteHacks')
const ipc = global.require('electron').ipcRenderer

import adInfo from '../data/adInfo.js'
import FindBar from './findbar.js'
const { isSourceAboutUrl, getTargetAboutUrl } = require('../lib/appUrlUtil')

class Frame extends ImmutableComponent {
  constructor () {
    super()
  }

  updateWebview () {
    let src = this.props.frame.get('src')
    let location = this.props.frame.get('location')
    let appRoot = window.baseHref
      ? 'file://' + path.resolve(__dirname, '..', '..', 'app') + '/'
      : ''

    let contentScripts = [appRoot + 'content/scripts/webviewPreload.js']
    let aboutPreload = false
    if (['about:preferences', 'about:bookmarks', 'about:certerror'].includes(location)) {
      contentScripts.push(appRoot + 'content/scripts/aboutPreload.js')
      aboutPreload = true
    }

    contentScripts = contentScripts.join(',')
    const contentScriptsChanged =
      this.webview && contentScripts !== this.webview.getAttribute('contentScripts')

    // Create the webview dynamically because React doesn't whitelist all
    // of the attributes we need.  Clear out old webviews if the contentScripts
    // change because they cannot change after being added to the DOM.
    if (contentScriptsChanged) {
      while (this.webviewContainer.firstChild) {
        this.webviewContainer.removeChild(this.webviewContainer.firstChild)
      }
    }
    this.webview = !contentScriptsChanged && this.webview || document.createElement('webview')
    this.webview.setAttribute('allowDisplayingInsecureContent', true)
    this.webview.setAttribute('data-frame-key', this.props.frame.get('key'))
    this.webview.setAttribute('contentScripts', contentScripts)
    // Don't allow dropping on webviews with aboutPreload since they navigate within the same process
    // automatically while keeping the content script loaded.
    if (aboutPreload) {
      this.webview.addEventListener('drop', (e) => {
        e.preventDefault()
      })
    }
    if (this.props.frame.get('isPrivate')) {
      this.webview.setAttribute('partition', 'private-1')
    } else if (this.props.frame.get('partitionNumber')) {
      let partition = `persist:partition-${this.props.frame.get('partitionNumber')}`
      ipc.send(messages.INITIALIZE_PARTITION, partition)
      this.webview.setAttribute('partition', partition)
    }
    if (this.props.frame.get('guestInstanceId')) {
      this.webview.setAttribute('data-guest-instance-id', this.props.frame.get('guestInstanceId'))
    }

    const hack = siteHacks[urlParse(location).hostname]
    if (hack && hack.userAgent) {
      this.webview.setAttribute('useragent', hack.userAgent)
    }
    this.webview.setAttribute('src',
                              isSourceAboutUrl(src) ? getTargetAboutUrl(src) : src)
    if (!this.webviewContainer.firstChild) {
      this.webviewContainer.appendChild(this.webview)
      this.addEventListeners()
    }
  }

  componentDidMount () {
    this.updateWebview()
  }

  componentDidUpdate (prevProps, prevState) {
    const didSrcChange = this.props.frame.get('src') !== prevProps.frame.get('src')
    const didLocationChange = this.props.frame.get('location') !== prevProps.frame.get('location')
    // When auto-redirecting to about:certerror, the frame location change and
    // frame src change are emitted separately. Make sure updateWebview is
    // called when the location changes.
    if (didSrcChange || (didLocationChange && this.props.frame.get('location') === 'about:certerror')) {
      this.updateWebview()
    }
    // give focus when switching tabs
    if (this.props.isActive && !prevProps.isActive) {
      this.webview.focus()
    }
    const activeShortcut = this.props.frame.get('activeShortcut')
    switch (activeShortcut) {
      case 'stop':
        this.webview.stop()
        break
      case 'reload':
        if (this.props.frame.get('location') === 'about:preferences') {
          break
        }
        this.webview.reload()
        break
      case 'clean-reload':
        this.webview.reloadIgnoringCache()
        break
      case 'zoom-in':
        windowActions.zoomIn(this.props.frame)
        break
      case 'zoom-out':
        windowActions.zoomOut(this.props.frame)
        break
      case 'zoom-reset':
        windowActions.zoomReset(this.props.frame)
        break
      case 'toggle-dev-tools':
        if (this.webview.isDevToolsOpened()) {
          this.webview.closeDevTools()
        } else {
          this.webview.openDevTools()
        }
        break
      case 'view-source':
        const location = UrlUtil.getViewSourceUrlFromUrl(this.webview.getURL())
        windowActions.newFrame({location}, true)
        // TODO: Make the URL bar show the view-source: prefix
        break
      case 'save':
        // TODO: Sometimes this tries to save in a non-existent directory
        this.webview.downloadURL(this.webview.getURL())
        break
      case 'print':
        this.webview.print()
        break
      case 'show-findbar':
        windowActions.setFindbarShown(this.props.frame, true)
        break
    }
    if (activeShortcut) {
      windowActions.setActiveFrameShortcut(this.props.frame, null)
    }

    if (this.props.frame.get('location') === 'about:preferences') {
      this.webview.send(messages.SETTINGS_UPDATED, this.props.settings.toJS())
    } else if (this.props.frame.get('location') === 'about:bookmarks') {
      this.webview.send(messages.BOOKMARKS_UPDATED, {
        bookmarks: this.props.bookmarks.toJS(),
        bookmarkFolders: this.props.bookmarkFolders.toJS()
      })
    }
  }

  addEventListeners () {
    this.webview.addEventListener('focus', this.onFocus.bind(this))
    // @see new-window event
    this.webview.addEventListener('new-window', (e, url, frameName, disposition, options) => {
      e.preventDefault()

      let guestInstanceId = e.options && e.options.webPreferences && e.options.webPreferences.guestInstanceId
      let windowOpts = e.options && e.options.windowOptions || {}
      windowOpts.parentWindowKey = remote.getCurrentWindow().id
      windowOpts.disposition = e.disposition
      let delayedLoadUrl = e.options && e.options.delayedLoadUrl

      let frameOpts = {
        location: e.url,
        parentFrameKey: this.props.frame.get('key'),
        isPrivate: this.props.frame.get('isPrivate'),
        partitionNumber: this.props.frame.get('partitionNumber'),
        // use the delayed load url for the temporary title
        delayedLoadUrl,
        guestInstanceId
      }

      if (e.disposition === 'new-window' || e.disposition === 'new-popup') {
        appActions.newWindow(frameOpts, windowOpts)
      } else {
        let openInForeground = this.props.prefOpenInForeground === true ||
          e.disposition !== 'background-tab'
        windowActions.newFrame(frameOpts, openInForeground)
      }
    })
    this.webview.addEventListener('destroyed', (e) => {
      this.props.onCloseFrame(this.props.frame)
    })
    this.webview.addEventListener('close', () => {
      this.props.onCloseFrame(this.props.frame)
    })
    this.webview.addEventListener('enter-html-full-screen', () => {
    })
    this.webview.addEventListener('leave-html-full-screen', () => {
    })
    this.webview.addEventListener('page-favicon-updated', (e) => {
      if (e.favicons && e.favicons.length > 0) {
        windowActions.setFavicon(this.props.frame, e.favicons[0])
      }
    })
    this.webview.addEventListener('page-title-updated', ({title}) => {
      windowActions.setFrameTitle(this.props.frame, title)
    })
    this.webview.addEventListener('ipc-message', (e) => {
      let method = () => {}
      switch (e.channel) {
        case messages.THEME_COLOR_COMPUTED:
          method = (computedThemeColor) =>
            windowActions.setThemeColor(this.props.frame, undefined, computedThemeColor || null)
          break
        case messages.CONTEXT_MENU_OPENED:
          method = (nodeProps, contextMenuType) => {
            contextMenus.onMainContextMenu(nodeProps, this.props.frame, contextMenuType)
          }
          break
        case messages.STOP_LOAD:
          method = () => this.webview.stop()
          break
        case messages.LINK_HOVERED:
          method = (href, position) => {
            position = position || {}
            let nearBottom = position.y > (window.innerHeight - 150) // todo: magic number
            let mouseOnLeft = position.x < (window.innerWidth / 2)
            let showOnRight = nearBottom && mouseOnLeft
            windowActions.setLinkHoverPreview(href, showOnRight)
          }
          break
        case messages.NEW_FRAME:
          method = (frameOpts, openInForeground) => {
            windowActions.newFrame(frameOpts, openInForeground)
          }
      }
      method.apply(this, e.args)
    })

    const loadStart = (event) => {
      if (event.isMainFrame && !event.isErrorPage && !event.isFrameSrcDoc) {
        // Temporary workaround for https://github.com/brave/browser-laptop/issues/787
        this.webview.insertCSS('input[type="search"]::-webkit-search-results-decoration { -webkit-appearance: none; }')
        // TODO: These 3 events should be combined into one
        windowActions.onWebviewLoadStart(
          this.props.frame)
        const key = this.props.frame.get('key')
        windowActions.setLocation(event.url, key)
        windowActions.setSecurityState(this.props.frame, {
          secure: urlParse(event.url).protocol === 'https:'
        })
      }
      windowActions.updateBackForwardState(
        this.props.frame,
        this.webview.canGoBack(),
        this.webview.canGoForward())
    }
    const loadEnd = () => {
      windowActions.onWebviewLoadEnd(
        this.props.frame,
        this.webview.getURL())
      if (this.props.enableAds) {
        this.insertAds(this.webview.getURL())
      }
      this.webview.send(messages.POST_PAGE_LOAD_RUN)
      let security = this.props.frame.get('security')
      if (this.props.frame.get('location') === 'about:certerror' &&
          security && security.get('certDetails')) {
        // Don't send certDetails.cert since it is big and crashes the page
        this.webview.send(messages.CERT_DETAILS_UPDATED, {
          url: security.get('certDetails').url,
          error: security.get('certDetails').error
        })
      }
      const protocol = urlParse(this.props.frame.get('location')).protocol
      if (!this.props.frame.get('isPrivate') && (protocol === 'http:' || protocol === 'https:')) {
        // Register the site for recent history for navigation bar
        appActions.addSite(siteUtil.getDetailFromFrame(this.props.frame))
      }
    }
    this.webview.addEventListener('load-commit', (event) => {
      loadStart(event)
    })
    this.webview.addEventListener('load-start', (event) => {
      loadStart(event)
    })
    this.webview.addEventListener('did-navigate', (e) => {
      // only give focus focus is this is not the initial default page load
      if (this.props.isActive && this.webview.canGoBack() && document.activeElement !== this.webview) {
        this.webview.focus()
      }
    })
    this.webview.addEventListener('did-fail-load', () => {
      windowActions.onWebviewLoadEnd(
        this.props.frame,
        this.webview.getURL())
    })
    this.webview.addEventListener('did-finish-load', () => {
      loadEnd()
    })
    this.webview.addEventListener('did-navigate-in-page', () => {
      loadEnd()
    })
    this.webview.addEventListener('media-started-playing', ({title}) => {
      windowActions.setAudioPlaybackActive(this.props.frame, true)
    })
    this.webview.addEventListener('media-paused', ({title}) => {
      windowActions.setAudioPlaybackActive(this.props.frame, false)
    })
    this.webview.addEventListener('did-change-theme-color', ({themeColor}) => {
      // Due to a bug in Electron, after navigating to a page with a theme color
      // to a page without a theme color, the background is sent to us as black
      // even know there is no background. To work around this we just ignore
      // the theme color in that case and let the computed theme color take over.
      windowActions.setThemeColor(this.props.frame, themeColor !== '#000000' ? themeColor : null)
    })
    this.webview.addEventListener('found-in-page', (e) => {
      if (e.result !== undefined && e.result.matches !== undefined) {
        windowActions.setFindDetail(this.props.frame, Immutable.fromJS({
          numberOfMatches: e.result.matches
        }))
      }
    })

    // Ensure we mute appropriately, the initial value could be set
    // from persisted state.
    if (this.props.frame.get('audioMuted')) {
      this.webview.setAudioMuted(true)
    }
  }

  insertAds (currentLocation) {
    const host = new window.URL(currentLocation).hostname.replace('www.', '')
    const adDivCandidates = adInfo[host] || []
    // Call this even when there are no matches because we have some logic
    // to replace common divs.
    this.webview.send(messages.SET_AD_DIV_CANDIDATES, adDivCandidates, config.vault.replacementUrl)
  }

  goBack () {
    this.webview.goBack()
  }

  goForward () {
    this.webview.goForward()
  }

  onFocus () {
    windowActions.setTabPageIndexByFrame(this.props.frame)
    windowActions.setUrlBarActive(false)
    windowActions.setContextMenuDetail()
  }

  onFindHide () {
    windowActions.setFindbarShown(this.props.frame, false)
    this.onClearMatch()
  }

  onFind (searchString, caseSensitivity, forward) {
    if (searchString) {
      this.webview.findInPage(searchString, {
        matchCase: caseSensitivity,
        forward: forward !== undefined ? forward : true,
        findNext: forward !== undefined
      })
    } else {
      this.onClearMatch()
    }
  }

  onClearMatch () {
    this.webview.stopFindInPage('clearSelection')
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.frame.get('audioMuted') &&
      this.props.frame.get('audioMuted') !== true) {
      this.webview.setAudioMuted(true)
    } else if (!nextProps.frame.get('audioMuted') &&
      this.props.frame.get('audioMuted') === true) {
      this.webview.setAudioMuted(false)
    }

    let zoomLevel = nextProps.frame.get('zoomLevel')
    if (zoomLevel !== this.props.frame.get('zoomLevel')) {
      this.webview.setZoomLevel(zoomLevel)
    }
  }

  render () {
    return

      { this.props.frame.get('findbarShown')
      ? <FindBar
        onFind={this.onFind.bind(this)}
        onFindHide={this.onFindHide.bind(this)}
        frame={this.props.frame}
        selected={this.props.frame.get('findbarSelected')}
        findDetail={this.props.frame.get('findDetail')}
      /> : null }

 this.webviewContainer = node}
        className={cx({
          webviewContainer: true,
          isPreview: this.props.isPreview
        })}/>
      { this.props.frame.get('hrefPreview')
        ?

          {this.props.frame.get('hrefPreview')}

 : null
      }

  }
}

module.exports = Frame
<|endoftext|>


<commit_before>// Copyright 2013-2016, University of Colorado Boulder

/**
 * Handles a visual SVG layer of drawables.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Poolable = require( 'PHET_CORE/Poolable' );
  var cleanArray = require( 'PHET_CORE/cleanArray' );
  var scenery = require( 'SCENERY/scenery' );
  var FittedBlock = require( 'SCENERY/display/FittedBlock' );
  var SVGGroup = require( 'SCENERY/display/SVGGroup' );
  var Util = require( 'SCENERY/util/Util' );

  /**
   * @constructor
   * @extends FittedBlock
   * @mixes Poolable
   *
   * @param {Display} display - the scenery Display this SVGBlock will appear in
   * @param {number} renderer - the bitmask for the renderer, see Renderer.js
   * @param {Instance} transformRootInstance - TODO: Documentation
   * @param {Instance} filterRootInstance - TODO: Documentation
   * @constructor
   */
  function SVGBlock( display, renderer, transformRootInstance, filterRootInstance ) {
    this.initialize( display, renderer, transformRootInstance, filterRootInstance );
  }

  scenery.register( 'SVGBlock', SVGBlock );

  inherit( FittedBlock, SVGBlock, {

    /**
     * Initialize function, which is required since SVGBlock instances are pooled by scenery.
     *
     * @param {Display} display - the scenery Display this SVGBlock will appear in
     * @param {number} renderer - the bitmask for the renderer, see Renderer.js
     * @param {Instance} transformRootInstance - TODO: Documentation
     * @param {Instance} filterRootInstance - TODO: Documentation
     * @returns {FittedBlock}
     */
    initialize: function( display, renderer, transformRootInstance, filterRootInstance ) {
      this.initializeFittedBlock( display, renderer, transformRootInstance, FittedBlock.COMMON_ANCESTOR );

      this.filterRootInstance = filterRootInstance;

      this.dirtyGroups = cleanArray( this.dirtyGroups );
      this.dirtyDrawables = cleanArray( this.dirtyDrawables );

      // Keep track of how many times each Paint is used in this SVGBlock so that when all usages have been eliminated
      // we can remove the SVG def from our SVG tree to prevent memory leaks, etc.
      // maps {string} paint.id => { count: {number}, paint: {Paint}, def: {SVGElement} }
      // @private
      this.paintMap = {};

      if ( !this.domElement ) {

        // main SVG element
        this.svg = document.createElementNS( scenery.svgns, 'svg' );
        this.svg.style.position = 'absolute';
        this.svg.style.left = '0';
        this.svg.style.top = '0';

        //OHTWO TODO: why would we clip the individual layers also? Seems like a potentially useless performance loss
        // this.svg.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
        this.svg.style[ 'pointer-events' ] = 'none';

        // the <defs> block that we will be stuffing gradients and patterns into
        this.defs = document.createElementNS( scenery.svgns, 'defs' );
        this.svg.appendChild( this.defs );

        this.baseTransformGroup = document.createElementNS( scenery.svgns, 'g' );
        this.svg.appendChild( this.baseTransformGroup );

        this.domElement = this.svg;
      }

      // reset what layer fitting can do (this.forceAcceleration set in fitted block initialization)
      Util.prepareForTransform( this.svg, this.forceAcceleration ); // Apply CSS needed for future CSS transforms to work properly.

      Util.unsetTransform( this.svg ); // clear out any transforms that could have been previously applied
      this.baseTransformGroup.setAttribute( 'transform', '' ); // no base transform

      var instanceClosestToRoot = transformRootInstance.trail.nodes.length > filterRootInstance.trail.nodes.length ?
                                  filterRootInstance : transformRootInstance;

      this.rootGroup = SVGGroup.createFromPool( this, instanceClosestToRoot, null );
      this.baseTransformGroup.appendChild( this.rootGroup.svgGroup );

      // TODO: dirty list of nodes (each should go dirty only once, easier than scanning all?)

      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'initialized #' + this.id );

      return this;
    },

    /*
     * Increases our reference count for the specified {Paint}. If it didn't exist before, we'll add the SVG def to the
     * paint can be referenced by SVG id.
     *
     * @param {Paint} paint
     */
    incrementPaint: function( paint ) {
      assert && assert( paint.isPaint );

      sceneryLog && sceneryLog.Paints && sceneryLog.Paints( 'incrementPaint ' + this.toString() + ' ' + paint.id );

      if ( this.paintMap.hasOwnProperty( paint.id ) ) {
        this.paintMap[ paint.id ].count++;
      }
      else {
        var def = paint.getSVGDefinition();
        def.setAttribute( 'id', paint.id + '-' + this.id );

        // TODO: reduce allocations?
        this.paintMap[ paint.id ] = {
          count: 1,
          paint: paint,
          def: def
        };

        this.defs.appendChild( def );
      }
    },

    /*
     * Decreases our reference count for the specified {Paint}. If this was the last reference, we'll remove the SVG def
     * from our SVG tree to prevent memory leaks, etc.
     *
     * @param {Paint} paint
     */
    decrementPaint: function( paint ) {
      assert && assert( paint.isPaint );

      sceneryLog && sceneryLog.Paints && sceneryLog.Paints( 'decrementPaint ' + this.toString() + ' ' + paint.id );

      // since the block may have been disposed (yikes!), we have a defensive set-up here
      if ( this.paintMap.hasOwnProperty( paint.id ) ) {
        var entry = this.paintMap[ paint.id ];
        assert && assert( entry.count >= 1 );

        if ( entry.count === 1 ) {
          this.defs.removeChild( entry.def );
          delete this.paintMap[ paint.id ]; // delete, so we don't memory leak if we run through MANY paints
        }
        else {
          entry.count--;
        }
      }
    },

    markDirtyGroup: function( block ) {
      this.dirtyGroups.push( block );
      this.markDirty();
    },

    markDirtyDrawable: function( drawable ) {
      sceneryLog && sceneryLog.dirty && sceneryLog.dirty( 'markDirtyDrawable on SVGBlock#' + this.id + ' with ' + drawable.toString() );
      this.dirtyDrawables.push( drawable );
      this.markDirty();
    },

    setSizeFullDisplay: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'setSizeFullDisplay #' + this.id );

      var size = this.display.getSize();
      this.svg.setAttribute( 'width', size.width );
      this.svg.setAttribute( 'height', size.height );
    },

    setSizeFitBounds: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'setSizeFitBounds #' + this.id + ' with ' + this.fitBounds.toString() );

      var x = this.fitBounds.minX;
      var y = this.fitBounds.minY;

      assert && assert( isFinite( x ) && isFinite( y ), 'Invalid SVG transform for SVGBlock' );
      assert && assert( this.fitBounds.isValid(), 'Invalid fitBounds' );

      this.baseTransformGroup.setAttribute( 'transform', 'translate(' + (-x) + ',' + (-y) + ')' ); // subtract off so we have a tight fit
      Util.setTransform( 'matrix(1,0,0,1,' + x + ',' + y + ')', this.svg, this.forceAcceleration ); // reapply the translation as a CSS transform
      this.svg.setAttribute( 'width', this.fitBounds.width );
      this.svg.setAttribute( 'height', this.fitBounds.height );
    },

    /**
     * Update the SVGBlock as part of a render step.  Called from Display.updateDisplay => BackboneDrawable.updateDisplay
     */
    update: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'update #' + this.id );

      // TODO: Shouldn't calling update on a disposed SVGBlock be an assertion error?
      if ( this.dirty && !this.disposed ) {
        this.dirty = false;

        //OHTWO TODO: call here!
        // TODO: What does the above TODO mean?
        while ( this.dirtyGroups.length ) {
          var group = this.dirtyGroups.pop();

          // if this group has been disposed or moved to another block, don't mess with it
          if ( group.block === this ) {
            group.update();
          }
        }
        while ( this.dirtyDrawables.length ) {
          var drawable = this.dirtyDrawables.pop();

          // if this drawable has been disposed or moved to another block, don't mess with it
          // TODO: If it was moved to another block, why might it still appear in our list?  Shouldn't that be an assertion check?
          if ( drawable.parentDrawable === this ) {
            drawable.update();
          }
        }

        // checks will be done in updateFit() to see whether it is needed
        this.updateFit();
      }
    },

    dispose: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'dispose #' + this.id );

      // make it take up zero area, so that we don't use up excess memory
      this.svg.setAttribute( 'width', 0 );
      this.svg.setAttribute( 'height', 0 );

      // clear references
      this.filterRootInstance = null;
      cleanArray( this.dirtyGroups );
      cleanArray( this.dirtyDrawables );
      this.paintMap = {};

      this.baseTransformGroup.removeChild( this.rootGroup.svgGroup );
      this.rootGroup.dispose();
      this.rootGroup = null;

      // since we may not properly remove all defs yet
      while ( this.defs.childNodes.length ) {
        this.defs.removeChild( this.defs.childNodes[ 0 ] );
      }

      FittedBlock.prototype.dispose.call( this );
    },

    addDrawable: function( drawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.addDrawable ' + drawable.toString() );

      FittedBlock.prototype.addDrawable.call( this, drawable );

      SVGGroup.addDrawable( this, drawable );
      drawable.updateSVGBlock( this );
    },

    removeDrawable: function( drawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.removeDrawable ' + drawable.toString() );

      SVGGroup.removeDrawable( this, drawable );

      FittedBlock.prototype.removeDrawable.call( this, drawable );

      // NOTE: we don't unset the drawable's defs here, since it will either be disposed (will clear it)
      // or will be added to another SVGBlock (which will overwrite it)
    },

    onIntervalChange: function( firstDrawable, lastDrawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.onIntervalChange ' + firstDrawable.toString() + ' to ' + lastDrawable.toString() );

      FittedBlock.prototype.onIntervalChange.call( this, firstDrawable, lastDrawable );
    },

    toString: function() {
      return 'SVGBlock#' + this.id + '-' + FittedBlock.fitString[ this.fit ];
    }
  } );

  Poolable.mixin( SVGBlock, {
    constructorDuplicateFactory: function( pool ) {
      return function( display, renderer, transformRootInstance, filterRootInstance ) {
        if ( pool.length ) {
          sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'new from pool' );
          return pool.pop().initialize( display, renderer, transformRootInstance, filterRootInstance );
        }
        else {
          sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'new from constructor' );
          return new SVGBlock( display, renderer, transformRootInstance, filterRootInstance );
        }
      };
    }
  } );

  return SVGBlock;
} );
<commit_msg>remove svg from tab order in IE11 full screen mode, see phetsims/john-travoltage#220<commit_after>// Copyright 2013-2016, University of Colorado Boulder

/**
 * Handles a visual SVG layer of drawables.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var Poolable = require( 'PHET_CORE/Poolable' );
  var cleanArray = require( 'PHET_CORE/cleanArray' );
  var scenery = require( 'SCENERY/scenery' );
  var FittedBlock = require( 'SCENERY/display/FittedBlock' );
  var SVGGroup = require( 'SCENERY/display/SVGGroup' );
  var Util = require( 'SCENERY/util/Util' );

  /**
   * @constructor
   * @extends FittedBlock
   * @mixes Poolable
   *
   * @param {Display} display - the scenery Display this SVGBlock will appear in
   * @param {number} renderer - the bitmask for the renderer, see Renderer.js
   * @param {Instance} transformRootInstance - TODO: Documentation
   * @param {Instance} filterRootInstance - TODO: Documentation
   * @constructor
   */
  function SVGBlock( display, renderer, transformRootInstance, filterRootInstance ) {
    this.initialize( display, renderer, transformRootInstance, filterRootInstance );
  }

  scenery.register( 'SVGBlock', SVGBlock );

  inherit( FittedBlock, SVGBlock, {

    /**
     * Initialize function, which is required since SVGBlock instances are pooled by scenery.
     *
     * @param {Display} display - the scenery Display this SVGBlock will appear in
     * @param {number} renderer - the bitmask for the renderer, see Renderer.js
     * @param {Instance} transformRootInstance - TODO: Documentation
     * @param {Instance} filterRootInstance - TODO: Documentation
     * @returns {FittedBlock}
     */
    initialize: function( display, renderer, transformRootInstance, filterRootInstance ) {
      this.initializeFittedBlock( display, renderer, transformRootInstance, FittedBlock.COMMON_ANCESTOR );

      this.filterRootInstance = filterRootInstance;

      this.dirtyGroups = cleanArray( this.dirtyGroups );
      this.dirtyDrawables = cleanArray( this.dirtyDrawables );

      // Keep track of how many times each Paint is used in this SVGBlock so that when all usages have been eliminated
      // we can remove the SVG def from our SVG tree to prevent memory leaks, etc.
      // maps {string} paint.id => { count: {number}, paint: {Paint}, def: {SVGElement} }
      // @private
      this.paintMap = {};

      if ( !this.domElement ) {

        // main SVG element
        this.svg = document.createElementNS( scenery.svgns, 'svg' );
        this.svg.style.position = 'absolute';
        this.svg.style.left = '0';
        this.svg.style.top = '0';

        // a11y - make sure the element is not focusable (it is focusable by default in IE11 full screen mode)
        this.svg.setAttribute( 'focusable', false );

        //OHTWO TODO: why would we clip the individual layers also? Seems like a potentially useless performance loss
        // this.svg.style.clip = 'rect(0px,' + width + 'px,' + height + 'px,0px)';
        this.svg.style[ 'pointer-events' ] = 'none';

        // the <defs> block that we will be stuffing gradients and patterns into
        this.defs = document.createElementNS( scenery.svgns, 'defs' );
        this.svg.appendChild( this.defs );

        this.baseTransformGroup = document.createElementNS( scenery.svgns, 'g' );
        this.svg.appendChild( this.baseTransformGroup );

        this.domElement = this.svg;
      }

      // reset what layer fitting can do (this.forceAcceleration set in fitted block initialization)
      Util.prepareForTransform( this.svg, this.forceAcceleration ); // Apply CSS needed for future CSS transforms to work properly.

      Util.unsetTransform( this.svg ); // clear out any transforms that could have been previously applied
      this.baseTransformGroup.setAttribute( 'transform', '' ); // no base transform

      var instanceClosestToRoot = transformRootInstance.trail.nodes.length > filterRootInstance.trail.nodes.length ?
                                  filterRootInstance : transformRootInstance;

      this.rootGroup = SVGGroup.createFromPool( this, instanceClosestToRoot, null );
      this.baseTransformGroup.appendChild( this.rootGroup.svgGroup );

      // TODO: dirty list of nodes (each should go dirty only once, easier than scanning all?)

      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'initialized #' + this.id );

      return this;
    },

    /*
     * Increases our reference count for the specified {Paint}. If it didn't exist before, we'll add the SVG def to the
     * paint can be referenced by SVG id.
     *
     * @param {Paint} paint
     */
    incrementPaint: function( paint ) {
      assert && assert( paint.isPaint );

      sceneryLog && sceneryLog.Paints && sceneryLog.Paints( 'incrementPaint ' + this.toString() + ' ' + paint.id );

      if ( this.paintMap.hasOwnProperty( paint.id ) ) {
        this.paintMap[ paint.id ].count++;
      }
      else {
        var def = paint.getSVGDefinition();
        def.setAttribute( 'id', paint.id + '-' + this.id );

        // TODO: reduce allocations?
        this.paintMap[ paint.id ] = {
          count: 1,
          paint: paint,
          def: def
        };

        this.defs.appendChild( def );
      }
    },

    /*
     * Decreases our reference count for the specified {Paint}. If this was the last reference, we'll remove the SVG def
     * from our SVG tree to prevent memory leaks, etc.
     *
     * @param {Paint} paint
     */
    decrementPaint: function( paint ) {
      assert && assert( paint.isPaint );

      sceneryLog && sceneryLog.Paints && sceneryLog.Paints( 'decrementPaint ' + this.toString() + ' ' + paint.id );

      // since the block may have been disposed (yikes!), we have a defensive set-up here
      if ( this.paintMap.hasOwnProperty( paint.id ) ) {
        var entry = this.paintMap[ paint.id ];
        assert && assert( entry.count >= 1 );

        if ( entry.count === 1 ) {
          this.defs.removeChild( entry.def );
          delete this.paintMap[ paint.id ]; // delete, so we don't memory leak if we run through MANY paints
        }
        else {
          entry.count--;
        }
      }
    },

    markDirtyGroup: function( block ) {
      this.dirtyGroups.push( block );
      this.markDirty();
    },

    markDirtyDrawable: function( drawable ) {
      sceneryLog && sceneryLog.dirty && sceneryLog.dirty( 'markDirtyDrawable on SVGBlock#' + this.id + ' with ' + drawable.toString() );
      this.dirtyDrawables.push( drawable );
      this.markDirty();
    },

    setSizeFullDisplay: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'setSizeFullDisplay #' + this.id );

      var size = this.display.getSize();
      this.svg.setAttribute( 'width', size.width );
      this.svg.setAttribute( 'height', size.height );
    },

    setSizeFitBounds: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'setSizeFitBounds #' + this.id + ' with ' + this.fitBounds.toString() );

      var x = this.fitBounds.minX;
      var y = this.fitBounds.minY;

      assert && assert( isFinite( x ) && isFinite( y ), 'Invalid SVG transform for SVGBlock' );
      assert && assert( this.fitBounds.isValid(), 'Invalid fitBounds' );

      this.baseTransformGroup.setAttribute( 'transform', 'translate(' + (-x) + ',' + (-y) + ')' ); // subtract off so we have a tight fit
      Util.setTransform( 'matrix(1,0,0,1,' + x + ',' + y + ')', this.svg, this.forceAcceleration ); // reapply the translation as a CSS transform
      this.svg.setAttribute( 'width', this.fitBounds.width );
      this.svg.setAttribute( 'height', this.fitBounds.height );
    },

    /**
     * Update the SVGBlock as part of a render step.  Called from Display.updateDisplay => BackboneDrawable.updateDisplay
     */
    update: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'update #' + this.id );

      // TODO: Shouldn't calling update on a disposed SVGBlock be an assertion error?
      if ( this.dirty && !this.disposed ) {
        this.dirty = false;

        //OHTWO TODO: call here!
        // TODO: What does the above TODO mean?
        while ( this.dirtyGroups.length ) {
          var group = this.dirtyGroups.pop();

          // if this group has been disposed or moved to another block, don't mess with it
          if ( group.block === this ) {
            group.update();
          }
        }
        while ( this.dirtyDrawables.length ) {
          var drawable = this.dirtyDrawables.pop();

          // if this drawable has been disposed or moved to another block, don't mess with it
          // TODO: If it was moved to another block, why might it still appear in our list?  Shouldn't that be an assertion check?
          if ( drawable.parentDrawable === this ) {
            drawable.update();
          }
        }

        // checks will be done in updateFit() to see whether it is needed
        this.updateFit();
      }
    },

    dispose: function() {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'dispose #' + this.id );

      // make it take up zero area, so that we don't use up excess memory
      this.svg.setAttribute( 'width', 0 );
      this.svg.setAttribute( 'height', 0 );

      // clear references
      this.filterRootInstance = null;
      cleanArray( this.dirtyGroups );
      cleanArray( this.dirtyDrawables );
      this.paintMap = {};

      this.baseTransformGroup.removeChild( this.rootGroup.svgGroup );
      this.rootGroup.dispose();
      this.rootGroup = null;

      // since we may not properly remove all defs yet
      while ( this.defs.childNodes.length ) {
        this.defs.removeChild( this.defs.childNodes[ 0 ] );
      }

      FittedBlock.prototype.dispose.call( this );
    },

    addDrawable: function( drawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.addDrawable ' + drawable.toString() );

      FittedBlock.prototype.addDrawable.call( this, drawable );

      SVGGroup.addDrawable( this, drawable );
      drawable.updateSVGBlock( this );
    },

    removeDrawable: function( drawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.removeDrawable ' + drawable.toString() );

      SVGGroup.removeDrawable( this, drawable );

      FittedBlock.prototype.removeDrawable.call( this, drawable );

      // NOTE: we don't unset the drawable's defs here, since it will either be disposed (will clear it)
      // or will be added to another SVGBlock (which will overwrite it)
    },

    onIntervalChange: function( firstDrawable, lastDrawable ) {
      sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( '#' + this.id + '.onIntervalChange ' + firstDrawable.toString() + ' to ' + lastDrawable.toString() );

      FittedBlock.prototype.onIntervalChange.call( this, firstDrawable, lastDrawable );
    },

    toString: function() {
      return 'SVGBlock#' + this.id + '-' + FittedBlock.fitString[ this.fit ];
    }
  } );

  Poolable.mixin( SVGBlock, {
    constructorDuplicateFactory: function( pool ) {
      return function( display, renderer, transformRootInstance, filterRootInstance ) {
        if ( pool.length ) {
          sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'new from pool' );
          return pool.pop().initialize( display, renderer, transformRootInstance, filterRootInstance );
        }
        else {
          sceneryLog && sceneryLog.SVGBlock && sceneryLog.SVGBlock( 'new from constructor' );
          return new SVGBlock( display, renderer, transformRootInstance, filterRootInstance );
        }
      };
    }
  } );

  return SVGBlock;
} );
<|endoftext|>


<commit_before>/*!
 * jQuery Accordion 0.0.1
 * (c) 2014 Victor Fernandez <victor@vctrfrnndz.com>
 * MIT Licensed.
 */

;(function ( $, window, document, undefined ) {

    var pluginName = 'accordion',
        defaults = {
            transitionSpeed: 300,
            transitionEasing: 'ease',
            controlElement: '[data-control]',
            contentElement: '[data-content]',
            groupElement: '[data-accordion-group]',
            singleOpen: true
        };

    function Accordion(element, options) {
        this.element = element;
        this.options = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }

    Accordion.prototype.init = function () {
        var self = this,
            opts = self.options;

        var $accordion = $(self.element),
            $controls = $accordion.find(opts.controlElement),
            $content =  $accordion.find('> ' + opts.contentElement);

        var accordionParentsQty = $accordion.parents('[data-accordion]').length,
            accordionHasParent = accordionParentsQty > 0;

        var closedCSS = { 'max-height': 0, 'overflow': 'hidden' };

        var CSStransitions = supportsTransitions();

        function debounce(func, threshold, execAsap) {
            var timeout;

            return function debounced() {
                var obj = this,
                    args = arguments;

                function delayed() {
                    if (!execAsap) func.apply(obj, args);
                    timeout = null;
                };

                if (timeout) clearTimeout(timeout);
                else if (execAsap) func.apply(obj, args);

                timeout = setTimeout(delayed, threshold || 100);
            };
        }

        function supportsTransitions() {
            var b = document.body || document.documentElement,
                s = b.style,
                p = 'transition';

            if (typeof s[p] == 'string') {
                return true;
            }

            var v = ['Moz', 'webkit', 'Webkit', 'Khtml', 'O', 'ms'];

            p = 'Transition';

            for (var i=0; i<v.length; i++) {
                if (typeof s[v[i] + p] == 'string') {
                    return true;
                }
            }

            return false;
        }

        function requestAnimFrame(cb) {
            if(window.requestAnimationFrame){
                requestAnimationFrame(cb);
            } else if (window.webkitRequestAnimationFrame) {
                webkitRequestAnimationFrame(cb);
            } else if (window.mozRequestAnimationFrame) {
                mozRequestAnimationFrame(cb);
            } else {
                setTimeout(cb, 1000 / 60);
            }
        }

        function toggleTransition($el, remove) {
            if(!remove) {
                $content.css({
                    '-webkit-transition': 'max-height ' + opts.transitionSpeed + 'ms ' + opts.transitionEasing,
                    'transition': 'max-height ' + opts.transitionSpeed + 'ms ' + opts.transitionEasing
                });
            } else {
                $content.css({
                    '-webkit-transition': '',
                    'transition': ''
                });
            }
        }

        function calculateHeight($el) {
            var height = 0;

            $el.children().each(function() {
                height = height + $(this).outerHeight(true);
            });

            $el.data('oHeight', height);
        }

        function updateParentHeight($parentAccordion, $currentAccordion, qty, operation) {
            var $content = $parentAccordion.filter('.open').find('> [data-content]'),
                $childs = $content.find('[data-accordion].open > [data-content]'),
                $matched;

            if(!opts.singleOpen) {
                $childs = $childs.not($currentAccordion.siblings('[data-accordion].open').find('> [data-content]'));
            }

            $matched = $content.add($childs);

            if($parentAccordion.hasClass('open')) {
                $matched.each(function() {
                    var currentHeight = $(this).data('oHeight');

                    switch (operation) {
                        case '+':
                            $(this).data('oHeight', currentHeight + qty);
                            break;
                        case '-':
                            $(this).data('oHeight', currentHeight - qty);
                            break;
                        default:
                            throw 'updateParentHeight method needs an operation';
                    }

                    $(this).css('max-height', $(this).data('oHeight'));
                });
            }
        }

        function refreshHeight($accordion) {
            if($accordion.hasClass('open')) {
                var $content = $accordion.find('> [data-content]'),
                    $childs = $content.find('[data-accordion].open > [data-content]'),
                    $matched = $content.add($childs);

                calculateHeight($matched);

                $matched.css('max-height', $matched.data('oHeight'));
            }
        }

        function closeAccordion($accordion, $content) {
            $accordion.trigger('accordion.close');

            if(CSStransitions) {
                if(accordionHasParent) {
                    var $parentAccordions = $accordion.parents('[data-accordion]');

                    updateParentHeight($parentAccordions, $accordion, $content.data('oHeight'), '-');
                }

                $content.css(closedCSS);

                $accordion.removeClass('open');
            } else {
                $content.css('max-height', $content.data('oHeight'));

                $content.animate(closedCSS, opts.transitionSpeed);

                $accordion.removeClass('open');
            }
        }

        function openAccordion($accordion, $content) {
            $accordion.trigger('accordion.open');
            if(CSStransitions) {
                toggleTransition($content);

                if(accordionHasParent) {
                    var $parentAccordions = $accordion.parents('[data-accordion]');

                    updateParentHeight($parentAccordions, $accordion, $content.data('oHeight'), '+');
                }

                requestAnimFrame(function() {
                    $content.css('max-height', $content.data('oHeight'));
                });

                $accordion.addClass('open');
            } else {
                $content.animate({
                    'max-height': $content.data('oHeight')
                }, opts.transitionSpeed, function() {
                    $content.css({'max-height': 'none'});
                });

                $accordion.addClass('open');
            }
        }

        function closeSiblingAccordions($accordion) {
            var $accordionGroup = $accordion.closest(opts.groupElement);

            var $siblings = $accordion.siblings('[data-accordion]').filter('.open'),
                $siblingsChildren = $siblings.find('[data-accordion]').filter('.open');

            var $otherAccordions = $siblings.add($siblingsChildren);

            $otherAccordions.each(function() {
                var $accordion = $(this),
                    $content = $accordion.find(opts.contentElement);

                closeAccordion($accordion, $content);
            });

            $otherAccordions.removeClass('open');
        }

        function toggleAccordion() {
            var isAccordionGroup = (opts.singleOpen) ? $accordion.parents(opts.groupElement).length > 0 : false;

            calculateHeight($content);

            if(isAccordionGroup) {
                closeSiblingAccordions($accordion);
            }

            if($accordion.hasClass('open')) {
                closeAccordion($accordion, $content);
            } else {
                openAccordion($accordion, $content);
            }
        }

        function addEventListeners() {
            $controls.on('click', toggleAccordion);

            $controls.on('accordion.toggle', function() {
                if(opts.singleOpen && $controls.length > 1) {
                    return false;
                }

                toggleAccordion();
            });

            $controls.on('accordion.refresh', function() {
                refreshHeight($accordion);
            });

            $(window).on('resize', debounce(function() {
                refreshHeight($accordion);
            }));
        }

        function setup() {
            $content.each(function() {
                var $curr = $(this);

                if($curr.css('max-height') != 0) {
                    if(!$curr.closest('[data-accordion]').hasClass('open')) {
                        $curr.css({ 'max-height': 0, 'overflow': 'hidden' });
                    } else {
                        toggleTransition($curr);
                        calculateHeight($curr);

                        $curr.css('max-height', $curr.data('oHeight'));
                    }
                }
            });

            if(!$accordion.attr('data-accordion')) {
                $accordion.attr('data-accordion', '');
                $accordion.find(opts.controlElement).attr('data-control', '');
                $accordion.find(opts.contentElement).attr('data-content', '');
            }
        }

        setup();
        addEventListeners();
    };

    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName,
                new Accordion( this, options ));
            }
        });
    }

})( jQuery, window, document );
<commit_msg>Revert bad change<commit_after>/*!
 * jQuery Accordion 0.0.1
 * (c) 2014 Victor Fernandez <victor@vctrfrnndz.com>
 * MIT Licensed.
 */

;(function ( $, window, document, undefined ) {

    var pluginName = 'accordion',
        defaults = {
            transitionSpeed: 300,
            transitionEasing: 'ease',
            controlElement: '[data-control]',
            contentElement: '[data-content]',
            groupElement: '[data-accordion-group]',
            singleOpen: true
        };

    function Accordion(element, options) {
        this.element = element;
        this.options = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }

    Accordion.prototype.init = function () {
        var self = this,
            opts = self.options;

        var $accordion = $(self.element),
            $controls = $accordion.find('> ' + opts.controlElement),
            $content =  $accordion.find('> ' + opts.contentElement);

        var accordionParentsQty = $accordion.parents('[data-accordion]').length,
            accordionHasParent = accordionParentsQty > 0;

        var closedCSS = { 'max-height': 0, 'overflow': 'hidden' };

        var CSStransitions = supportsTransitions();

        function debounce(func, threshold, execAsap) {
            var timeout;

            return function debounced() {
                var obj = this,
                    args = arguments;

                function delayed() {
                    if (!execAsap) func.apply(obj, args);
                    timeout = null;
                };

                if (timeout) clearTimeout(timeout);
                else if (execAsap) func.apply(obj, args);

                timeout = setTimeout(delayed, threshold || 100);
            };
        }

        function supportsTransitions() {
            var b = document.body || document.documentElement,
                s = b.style,
                p = 'transition';

            if (typeof s[p] == 'string') {
                return true;
            }

            var v = ['Moz', 'webkit', 'Webkit', 'Khtml', 'O', 'ms'];

            p = 'Transition';

            for (var i=0; i<v.length; i++) {
                if (typeof s[v[i] + p] == 'string') {
                    return true;
                }
            }

            return false;
        }

        function requestAnimFrame(cb) {
            if(window.requestAnimationFrame){
                requestAnimationFrame(cb);
            } else if (window.webkitRequestAnimationFrame) {
                webkitRequestAnimationFrame(cb);
            } else if (window.mozRequestAnimationFrame) {
                mozRequestAnimationFrame(cb);
            } else {
                setTimeout(cb, 1000 / 60);
            }
        }

        function toggleTransition($el, remove) {
            if(!remove) {
                $content.css({
                    '-webkit-transition': 'max-height ' + opts.transitionSpeed + 'ms ' + opts.transitionEasing,
                    'transition': 'max-height ' + opts.transitionSpeed + 'ms ' + opts.transitionEasing
                });
            } else {
                $content.css({
                    '-webkit-transition': '',
                    'transition': ''
                });
            }
        }

        function calculateHeight($el) {
            var height = 0;

            $el.children().each(function() {
                height = height + $(this).outerHeight(true);
            });

            $el.data('oHeight', height);
        }

        function updateParentHeight($parentAccordion, $currentAccordion, qty, operation) {
            var $content = $parentAccordion.filter('.open').find('> [data-content]'),
                $childs = $content.find('[data-accordion].open > [data-content]'),
                $matched;

            if(!opts.singleOpen) {
                $childs = $childs.not($currentAccordion.siblings('[data-accordion].open').find('> [data-content]'));
            }

            $matched = $content.add($childs);

            if($parentAccordion.hasClass('open')) {
                $matched.each(function() {
                    var currentHeight = $(this).data('oHeight');

                    switch (operation) {
                        case '+':
                            $(this).data('oHeight', currentHeight + qty);
                            break;
                        case '-':
                            $(this).data('oHeight', currentHeight - qty);
                            break;
                        default:
                            throw 'updateParentHeight method needs an operation';
                    }

                    $(this).css('max-height', $(this).data('oHeight'));
                });
            }
        }

        function refreshHeight($accordion) {
            if($accordion.hasClass('open')) {
                var $content = $accordion.find('> [data-content]'),
                    $childs = $content.find('[data-accordion].open > [data-content]'),
                    $matched = $content.add($childs);

                calculateHeight($matched);

                $matched.css('max-height', $matched.data('oHeight'));
            }
        }

        function closeAccordion($accordion, $content) {
            $accordion.trigger('accordion.close');

            if(CSStransitions) {
                if(accordionHasParent) {
                    var $parentAccordions = $accordion.parents('[data-accordion]');

                    updateParentHeight($parentAccordions, $accordion, $content.data('oHeight'), '-');
                }

                $content.css(closedCSS);

                $accordion.removeClass('open');
            } else {
                $content.css('max-height', $content.data('oHeight'));

                $content.animate(closedCSS, opts.transitionSpeed);

                $accordion.removeClass('open');
            }
        }

        function openAccordion($accordion, $content) {
            $accordion.trigger('accordion.open');
            if(CSStransitions) {
                toggleTransition($content);

                if(accordionHasParent) {
                    var $parentAccordions = $accordion.parents('[data-accordion]');

                    updateParentHeight($parentAccordions, $accordion, $content.data('oHeight'), '+');
                }

                requestAnimFrame(function() {
                    $content.css('max-height', $content.data('oHeight'));
                });

                $accordion.addClass('open');
            } else {
                $content.animate({
                    'max-height': $content.data('oHeight')
                }, opts.transitionSpeed, function() {
                    $content.css({'max-height': 'none'});
                });

                $accordion.addClass('open');
            }
        }

        function closeSiblingAccordions($accordion) {
            var $accordionGroup = $accordion.closest(opts.groupElement);

            var $siblings = $accordion.siblings('[data-accordion]').filter('.open'),
                $siblingsChildren = $siblings.find('[data-accordion]').filter('.open');

            var $otherAccordions = $siblings.add($siblingsChildren);

            $otherAccordions.each(function() {
                var $accordion = $(this),
                    $content = $accordion.find(opts.contentElement);

                closeAccordion($accordion, $content);
            });

            $otherAccordions.removeClass('open');
        }

        function toggleAccordion() {
            var isAccordionGroup = (opts.singleOpen) ? $accordion.parents(opts.groupElement).length > 0 : false;

            calculateHeight($content);

            if(isAccordionGroup) {
                closeSiblingAccordions($accordion);
            }

            if($accordion.hasClass('open')) {
                closeAccordion($accordion, $content);
            } else {
                openAccordion($accordion, $content);
            }
        }

        function addEventListeners() {
            $controls.on('click', toggleAccordion);

            $controls.on('accordion.toggle', function() {
                if(opts.singleOpen && $controls.length > 1) {
                    return false;
                }

                toggleAccordion();
            });

            $controls.on('accordion.refresh', function() {
                refreshHeight($accordion);
            });

            $(window).on('resize', debounce(function() {
                refreshHeight($accordion);
            }));
        }

        function setup() {
            $content.each(function() {
                var $curr = $(this);

                if($curr.css('max-height') != 0) {
                    if(!$curr.closest('[data-accordion]').hasClass('open')) {
                        $curr.css({ 'max-height': 0, 'overflow': 'hidden' });
                    } else {
                        toggleTransition($curr);
                        calculateHeight($curr);

                        $curr.css('max-height', $curr.data('oHeight'));
                    }
                }
            });

            if(!$accordion.attr('data-accordion')) {
                $accordion.attr('data-accordion', '');
                $accordion.find(opts.controlElement).attr('data-control', '');
                $accordion.find(opts.contentElement).attr('data-content', '');
            }
        }

        setup();
        addEventListeners();
    };

    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName,
                new Accordion( this, options ));
            }
        });
    }

})( jQuery, window, document );
<|endoftext|>