<commit_before>
// TODO: write a test that imports public interfaces in every possible
// permutation to detect circular dependency errors.
// See: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem

import Class from 'lowclass'
import documentReady from '@awaitbox/document-ready'

import Mixin from './Mixin'
import Motor from './Motor'
import {getWebGLRendererThree, destroyWebGLRendererThree} from './WebGLRendererThree'
import {getCSS3DRendererThree, destroyCSS3DRendererThree} from './CSS3DRendererThree'
import ImperativeBase, {initImperativeBase} from './ImperativeBase'
import XYZSizeModeValues from './XYZSizeModeValues'
import XYZNonNegativeValues from './XYZNonNegativeValues'
import { default as HTMLInterface } from '../html/HTMLScene'
import { props } from './props'

import {
    Scene as ThreeScene, // so as not to confuse with Infamous Scene.
    PerspectiveCamera,
    //AmbientLight,
    Color,
} from 'three'

initImperativeBase()

let Scene = Mixin(Base => {

    const Parent = ImperativeBase.mixin( Base )

    return Class('Scene').extends( Parent, ({ Super, Public, Protected, Private }) => ({

        static: {
            defaultElementName: 'i-scene',

            props: {
                ...Parent.props,
                backgroundColor: props.THREE.Color,
                backgroundOpacity: props.number,
                shadowmapType: props.string,
                vr: props.boolean,
                experimentalWebgl: props.boolean,
                disableCss: props.boolean,
            },
        },

        constructor(options = {}) {
            const self = Super(this).constructor(options)

            // Used by the `scene` getter in ImperativeBase
            self._scene = self

            self._mounted = false

            // TODO get default camera values from somewhere.
            self._perspective = 1000

            // size of the element where the Scene is mounted
            // NOTE: z size is always 0, since native DOM elements are always flat.
            self._elementParentSize = {x:0, y:0, z:0}

            self.cameraSetup()

            self._calcSize()
            self._needsToBeRendered()

            return self
        },

        _onElementParentSizeChange(newSize) {
            this._elementParentSize = newSize
            this._calcSize()
            this._needsToBeRendered()
        },

        cameraSetup() {
            // this.threeCamera holds the active camera. There can be many
            // cameras in the scene tree, but the last one with active="true"
            // will be the one referenced here.
            // If there are no cameras in the tree, a virtual default camera is
            // referenced here, who's perspective is that of the scene's
            // perspective attribute.
            this.threeCamera = null
            this._createDefaultCamera()

            // holds active cameras found in the DOM tree (if this is empty, it
            // means no camera elements are in the DOM, but this.threeCamera
            // will still have a reference to the default camera that scenes
            // are rendered with when no camera elements exist).
            this._activeCameras = new Set
        },

        // For now, use the same program (with shaders) for all objects.
        // Basically it has position, frag colors, point light, directional
        // light, and ambient light.
        loadGL() {
            if (Protected(this).__glLoaded) return

            // THREE
            // maybe keep this in sceneState in WebGLRendererThree
            Super(this).loadGL()

            // We don't let Three update any matrices, we supply our own world
            // matrices.
            this.three.autoUpdate = false

            // TODO: default ambient light when no AmbientLight elements are
            // present in the Scene.
            //const ambientLight = new AmbientLight( 0x353535 )
            //this.three.add( ambientLight )

            // a default orange background color. Use the backgroundColor and
            // backgroundOpacity attributes to customize.
            this._glBackgroundColor = new Color( 0xff6600 )
            this._glBackgroundOpacity = 0

            Private(this).__glRenderer = Private(this).__getRenderer('three')

            // set default colors
            Private(this).__glRenderer.setClearColor( this, this._glBackgroundColor, this._glBackgroundOpacity )

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.__loadGL()
            })
        },

        unloadGL() {
            if (!Protected(this).__glLoaded) return

            Super(this).unloadGL()

            if (Private(this).__glRenderer) {
                Private(this).__glRenderer.uninitialize(this)
                Private(this).__glRenderer = null
            }

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.__unloadGL()
            })
        },

        makeThreeObject3d() {
            return new ThreeScene
        },

        loadCSS() {
            if (Protected(this).__cssLoaded) return

            Super(this).loadCSS()

            Private(this).__cssRenderer = Private(this).__getCSSRenderer('three')

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.loadCSS()
            })
        },

        unloadCSS() {
            if (!Protected(this).__cssLoaded) return

            Super(this).unloadCSS()

            if (Private(this).__cssRenderer) {
                Private(this).__cssRenderer.uninitialize(this)
                Private(this).__cssRenderer = null
            }

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.unloadCSS()
            })
        },

        makeThreeCSSObject() {
            return new ThreeScene
        },

        drawScene() {
            Private(this).__glRenderer && Private(this).__glRenderer.drawScene(this)
            Private(this).__cssRenderer && Private(this).__cssRenderer.drawScene(this)
        },

        private: {
            __glRenderer: null,
            __cssRenderer: null,

            // The idea here is that in the future we might have "babylon",
            // "playcanvas", etc, on a per scene basis.
            __getRenderer(type) {
                const scene = Public(this)

                if (this.__glRenderer) return this.__glRenderer

                let rendererGetter = null

                if (type === "three")
                    rendererGetter = getWebGLRendererThree
                else throw new Error('invalid WebGL renderer')

                const renderer = rendererGetter(scene)
                renderer.initialize(scene)

                return renderer
            },

            __getCSSRenderer(type) {
                const scene = Public(this)

                if (this.__cssRenderer) return this.__cssRenderer

                let rendererGetter = null

                if (type === "three")
                    rendererGetter = getCSS3DRendererThree
                else throw new Error('invalid WebGL renderer')

                const renderer = rendererGetter(scene)
                renderer.initialize(scene)

                return renderer
            },
        },

        // TODO PERFORMANCE: make this static for better performance.
        _setDefaultProperties() {
            Super(this)._setDefaultProperties()

            Object.assign(this._properties, {
                sizeMode: new XYZSizeModeValues('proportional', 'proportional', 'proportional'),
                size: new XYZNonNegativeValues(1, 1, 1),
            })
        },

        // TODO FIXME: manual camera doesn't work after we've added the
        // default-camera feature.
        _setCamera( camera ) {
            if ( !camera ) {
                this._createDefaultCamera()
            }
            else {
                // TODO?: implement an changecamera event/method and emit/call
                // that here, then move this logic to the renderer
                // handler/method?
                this.threeCamera = camera.three
                this._updateCameraAspect()
                this._updateCameraProjection()
                this._needsToBeRendered()
            }
        },

        _createDefaultCamera() {
            const size = this._calculatedSize
            // THREE-COORDS-TO-DOM-COORDS
            // We apply Three perspective the same way as CSS3D perspective here.
            // TODO CAMERA-DEFAULTS, get defaults from somewhere common.
            // TODO the "far" arg will be auto-calculated to encompass the furthest objects (like CSS3D).
            this.threeCamera = new PerspectiveCamera( 45, size.x / size.y || 1, 0.1, 10000 )
            this.perspective = 1000
        },

        // TODO can this be moved to a render task like _calcSize? It depends
        // on size values.
        _updateCameraPerspective() {
            const perspective = this._perspective
            this.threeCamera.fov = 180 * ( 2 * Math.atan( this._calculatedSize.y / 2 / perspective ) ) / Math.PI
            this.threeCamera.position.z = perspective
        },

        // TODO perspective SkateJS prop
        set perspective(value) {
            this._perspective = value
            this._updateCameraPerspective()
            this._updateCameraProjection()
            this._needsToBeRendered()
        },
        get perspective() {
            return this._perspective
        },

        _updateCameraAspect() {
            this.threeCamera.aspect = this._calculatedSize.x / this._calculatedSize.y || 1
        },

        _updateCameraProjection() {
            this.threeCamera.updateProjectionMatrix()
        },

        _addCamera( camera ) {
            this._activeCameras.add( camera )
            this._setCamera( camera )
        },

        _removeCamera( camera ) {
            this._activeCameras.delete( camera )

            if ( this._activeCameras.size ) {
                // get the last camera in the Set
                this._activeCameras.forEach(c => camera = c)
            }
            else camera = null

            this._setCamera( camera )
        },

        /** @override */
        _getParentSize() {
            return this.parent ? this.parent._calculatedSize : this._elementParentSize
        },

        /**
         * Mount the scene into the given target.
         * Resolves the Scene's mountPromise, which can be use to do something once
         * the scene is mounted.
         *
         * @param {string|HTMLElement} [mountPoint=document.body] If a string selector is provided,
         * the mount point will be selected from the DOM. If an HTMLElement is
         * provided, that will be the mount point. If no mount point is provided,
         * the scene will be mounted into document.body.
         */
        async mount(mountPoint) {
            // if no mountPoint was provided, just mount onto the <body> element.
            if (!mountPoint) {
                if (!document.body) await documentBody()
                mountPoint = document.body
            }

            // if the user supplied a selector, mount there.
            else if (typeof mountPoint === 'string') {
                mountPoint = document.querySelector(mountPoint)
                if (!mountPoint && document.readyState === 'loading') {
                    // maybe the element wasn't parsed yet, check again when the
                    // document is ready.
                    await documentReady()
                    mountPoint = document.querySelector(mountPoint)
                }
            }

            // if we have an actual mount point (the user may have supplied one)
            if (!(mountPoint instanceof HTMLElement)) {
                throw new Error(`
                    Invalid mount point specified in Scene.mount() call. Pass a
                    selector, an actual HTMLElement, or don\'t pass anything to
                    mount to <body>.
                `)
            }

            // The user can mount to a new location without calling unmount
            // first. Call it automatically in that case.
            if (this._mounted) this.unmount()

            if (mountPoint !== this.parentNode)
                mountPoint.appendChild(this)

            this._mounted = true

            this._startOrStopSizePolling()
        },

        /**
         * Unmount the scene from it's mount point. Resets the Scene's
         * mountPromise.
         */
        unmount() {
            if (!this._mounted) return

            this._stopSizePolling()

            if (this.parentNode)
                this.parentNode.removeChild(this)

            this._mounted = false
        },

        updated(oldProps, moddedProps) {
            if (!this.isConnected) return

            if (moddedProps.experimentalWebgl) {
                if (this.experimentalWebgl) this.__loadGL()
                else this.__unloadGL()
            }

            if (moddedProps.disableCss) {
                if (!this.disableCss) this.__loadCSS()
                else this.__unloadCSS()
            }

            // call super.updated() after the above loadGL() so that WebGL
            // stuff will be ready in super.updated()
            Super(this).updated(oldProps, moddedProps)

            if (this.experimentalWebgl) {
                if (moddedProps.backgroundColor) {
                    Private(this).__glRenderer.setClearColor( this, this.backgroundColor, this.backgroundOpacity )
                    this._needsToBeRendered()
                }
                if (moddedProps.backgroundOpacity) {
                    Private(this).__glRenderer.setClearAlpha( this, this.backgroundOpacity )
                    this._needsToBeRendered()
                }
                if (moddedProps.shadowmapType) {
                    Private(this).__glRenderer.setShadowMapType( this, this.shadowmapType )
                    this._needsToBeRendered()
                }
                if (moddedProps.vr) {
                    Private(this).__glRenderer.enableVR( this, this.vr)

                    if ( this.vr ) {
                        Motor.setFrameRequester( fn => Private(this).__glRenderer.requestFrame( this, fn ) )
                        Private(this).__glRenderer.createDefaultWebVREntryUI( this )
                    }
                    else {
                        // TODO else return back to normal requestAnimationFrame
                    }
                }
            }

            if (moddedProps.sizeMode) {
                this._startOrStopSizePolling()
            }
        },

    }))

})

// TODO for now, hard-mixin the HTMLInterface class. We'll do this automatically later.
Scene = Scene.mixin(HTMLInterface)

export {Scene as default}

function documentBody() {
    return new Promise(resolve => {
        if (document.body) return resolve()

        const observer = new MutationObserver(() => {
            if (document.body) {
                resolve()
                observer.disconnect()
            }
        })

        observer.observe(document.documentElement, {childList: true})
    })
}
<commit_msg>make the default Z-size of Scene (zero) more obvious<commit_after>
// TODO: write a test that imports public interfaces in every possible
// permutation to detect circular dependency errors.
// See: https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem

import Class from 'lowclass'
import documentReady from '@awaitbox/document-ready'

import Mixin from './Mixin'
import Motor from './Motor'
import {getWebGLRendererThree, destroyWebGLRendererThree} from './WebGLRendererThree'
import {getCSS3DRendererThree, destroyCSS3DRendererThree} from './CSS3DRendererThree'
import ImperativeBase, {initImperativeBase} from './ImperativeBase'
import XYZSizeModeValues from './XYZSizeModeValues'
import XYZNonNegativeValues from './XYZNonNegativeValues'
import { default as HTMLInterface } from '../html/HTMLScene'
import { props } from './props'

import {
    Scene as ThreeScene, // so as not to confuse with Infamous Scene.
    PerspectiveCamera,
    //AmbientLight,
    Color,
} from 'three'

initImperativeBase()

let Scene = Mixin(Base => {

    const Parent = ImperativeBase.mixin( Base )

    return Class('Scene').extends( Parent, ({ Super, Public, Protected, Private }) => ({

        static: {
            defaultElementName: 'i-scene',

            props: {
                ...Parent.props,
                backgroundColor: props.THREE.Color,
                backgroundOpacity: props.number,
                shadowmapType: props.string,
                vr: props.boolean,
                experimentalWebgl: props.boolean,
                disableCss: props.boolean,
            },
        },

        constructor(options = {}) {
            const self = Super(this).constructor(options)

            // Used by the `scene` getter in ImperativeBase
            self._scene = self

            self._mounted = false

            // TODO get default camera values from somewhere.
            self._perspective = 1000

            // size of the element where the Scene is mounted
            // NOTE: z size is always 0, since native DOM elements are always flat.
            self._elementParentSize = {x:0, y:0, z:0}

            self.cameraSetup()

            self._calcSize()
            self._needsToBeRendered()

            return self
        },

        _onElementParentSizeChange(newSize) {
            this._elementParentSize = newSize
            this._calcSize()
            this._needsToBeRendered()
        },

        cameraSetup() {
            // this.threeCamera holds the active camera. There can be many
            // cameras in the scene tree, but the last one with active="true"
            // will be the one referenced here.
            // If there are no cameras in the tree, a virtual default camera is
            // referenced here, who's perspective is that of the scene's
            // perspective attribute.
            this.threeCamera = null
            this._createDefaultCamera()

            // holds active cameras found in the DOM tree (if this is empty, it
            // means no camera elements are in the DOM, but this.threeCamera
            // will still have a reference to the default camera that scenes
            // are rendered with when no camera elements exist).
            this._activeCameras = new Set
        },

        // For now, use the same program (with shaders) for all objects.
        // Basically it has position, frag colors, point light, directional
        // light, and ambient light.
        loadGL() {
            if (Protected(this).__glLoaded) return

            // THREE
            // maybe keep this in sceneState in WebGLRendererThree
            Super(this).loadGL()

            // We don't let Three update any matrices, we supply our own world
            // matrices.
            this.three.autoUpdate = false

            // TODO: default ambient light when no AmbientLight elements are
            // present in the Scene.
            //const ambientLight = new AmbientLight( 0x353535 )
            //this.three.add( ambientLight )

            // a default orange background color. Use the backgroundColor and
            // backgroundOpacity attributes to customize.
            this._glBackgroundColor = new Color( 0xff6600 )
            this._glBackgroundOpacity = 0

            Private(this).__glRenderer = Private(this).__getRenderer('three')

            // set default colors
            Private(this).__glRenderer.setClearColor( this, this._glBackgroundColor, this._glBackgroundOpacity )

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.__loadGL()
            })
        },

        unloadGL() {
            if (!Protected(this).__glLoaded) return

            Super(this).unloadGL()

            if (Private(this).__glRenderer) {
                Private(this).__glRenderer.uninitialize(this)
                Private(this).__glRenderer = null
            }

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.__unloadGL()
            })
        },

        makeThreeObject3d() {
            return new ThreeScene
        },

        loadCSS() {
            if (Protected(this).__cssLoaded) return

            Super(this).loadCSS()

            Private(this).__cssRenderer = Private(this).__getCSSRenderer('three')

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.loadCSS()
            })
        },

        unloadCSS() {
            if (!Protected(this).__cssLoaded) return

            Super(this).unloadCSS()

            if (Private(this).__cssRenderer) {
                Private(this).__cssRenderer.uninitialize(this)
                Private(this).__cssRenderer = null
            }

            this.traverse((node) => {
                // skip `this`, we already handled it above
                if (node === this) return

                node.unloadCSS()
            })
        },

        makeThreeCSSObject() {
            return new ThreeScene
        },

        drawScene() {
            Private(this).__glRenderer && Private(this).__glRenderer.drawScene(this)
            Private(this).__cssRenderer && Private(this).__cssRenderer.drawScene(this)
        },

        private: {
            __glRenderer: null,
            __cssRenderer: null,

            // The idea here is that in the future we might have "babylon",
            // "playcanvas", etc, on a per scene basis.
            __getRenderer(type) {
                const scene = Public(this)

                if (this.__glRenderer) return this.__glRenderer

                let rendererGetter = null

                if (type === "three")
                    rendererGetter = getWebGLRendererThree
                else throw new Error('invalid WebGL renderer')

                const renderer = rendererGetter(scene)
                renderer.initialize(scene)

                return renderer
            },

            __getCSSRenderer(type) {
                const scene = Public(this)

                if (this.__cssRenderer) return this.__cssRenderer

                let rendererGetter = null

                if (type === "three")
                    rendererGetter = getCSS3DRendererThree
                else throw new Error('invalid WebGL renderer')

                const renderer = rendererGetter(scene)
                renderer.initialize(scene)

                return renderer
            },
        },

        // TODO PERFORMANCE: make this static for better performance.
        _setDefaultProperties() {
            Super(this)._setDefaultProperties()

            Object.assign(this._properties, {
                sizeMode: new XYZSizeModeValues('proportional', 'proportional', 'literal'),
                size: new XYZNonNegativeValues(1, 1, 0),
            })
        },

        // TODO FIXME: manual camera doesn't work after we've added the
        // default-camera feature.
        _setCamera( camera ) {
            if ( !camera ) {
                this._createDefaultCamera()
            }
            else {
                // TODO?: implement an changecamera event/method and emit/call
                // that here, then move this logic to the renderer
                // handler/method?
                this.threeCamera = camera.three
                this._updateCameraAspect()
                this._updateCameraProjection()
                this._needsToBeRendered()
            }
        },

        _createDefaultCamera() {
            const size = this._calculatedSize
            // THREE-COORDS-TO-DOM-COORDS
            // We apply Three perspective the same way as CSS3D perspective here.
            // TODO CAMERA-DEFAULTS, get defaults from somewhere common.
            // TODO the "far" arg will be auto-calculated to encompass the furthest objects (like CSS3D).
            this.threeCamera = new PerspectiveCamera( 45, size.x / size.y || 1, 0.1, 10000 )
            this.perspective = 1000
        },

        // TODO can this be moved to a render task like _calcSize? It depends
        // on size values.
        _updateCameraPerspective() {
            const perspective = this._perspective
            this.threeCamera.fov = 180 * ( 2 * Math.atan( this._calculatedSize.y / 2 / perspective ) ) / Math.PI
            this.threeCamera.position.z = perspective
        },

        // TODO perspective SkateJS prop
        set perspective(value) {
            this._perspective = value
            this._updateCameraPerspective()
            this._updateCameraProjection()
            this._needsToBeRendered()
        },
        get perspective() {
            return this._perspective
        },

        _updateCameraAspect() {
            this.threeCamera.aspect = this._calculatedSize.x / this._calculatedSize.y || 1
        },

        _updateCameraProjection() {
            this.threeCamera.updateProjectionMatrix()
        },

        _addCamera( camera ) {
            this._activeCameras.add( camera )
            this._setCamera( camera )
        },

        _removeCamera( camera ) {
            this._activeCameras.delete( camera )

            if ( this._activeCameras.size ) {
                // get the last camera in the Set
                this._activeCameras.forEach(c => camera = c)
            }
            else camera = null

            this._setCamera( camera )
        },

        /** @override */
        _getParentSize() {
            return this.parent ? this.parent._calculatedSize : this._elementParentSize
        },

        /**
         * Mount the scene into the given target.
         * Resolves the Scene's mountPromise, which can be use to do something once
         * the scene is mounted.
         *
         * @param {string|HTMLElement} [mountPoint=document.body] If a string selector is provided,
         * the mount point will be selected from the DOM. If an HTMLElement is
         * provided, that will be the mount point. If no mount point is provided,
         * the scene will be mounted into document.body.
         */
        async mount(mountPoint) {
            // if no mountPoint was provided, just mount onto the <body> element.
            if (!mountPoint) {
                if (!document.body) await documentBody()
                mountPoint = document.body
            }

            // if the user supplied a selector, mount there.
            else if (typeof mountPoint === 'string') {
                mountPoint = document.querySelector(mountPoint)
                if (!mountPoint && document.readyState === 'loading') {
                    // maybe the element wasn't parsed yet, check again when the
                    // document is ready.
                    await documentReady()
                    mountPoint = document.querySelector(mountPoint)
                }
            }

            // if we have an actual mount point (the user may have supplied one)
            if (!(mountPoint instanceof HTMLElement)) {
                throw new Error(`
                    Invalid mount point specified in Scene.mount() call. Pass a
                    selector, an actual HTMLElement, or don\'t pass anything to
                    mount to <body>.
                `)
            }

            // The user can mount to a new location without calling unmount
            // first. Call it automatically in that case.
            if (this._mounted) this.unmount()

            if (mountPoint !== this.parentNode)
                mountPoint.appendChild(this)

            this._mounted = true

            this._startOrStopSizePolling()
        },

        /**
         * Unmount the scene from it's mount point. Resets the Scene's
         * mountPromise.
         */
        unmount() {
            if (!this._mounted) return

            this._stopSizePolling()

            if (this.parentNode)
                this.parentNode.removeChild(this)

            this._mounted = false
        },

        updated(oldProps, moddedProps) {
            if (!this.isConnected) return

            if (moddedProps.experimentalWebgl) {
                if (this.experimentalWebgl) this.__loadGL()
                else this.__unloadGL()
            }

            if (moddedProps.disableCss) {
                if (!this.disableCss) this.__loadCSS()
                else this.__unloadCSS()
            }

            // call super.updated() after the above loadGL() so that WebGL
            // stuff will be ready in super.updated()
            Super(this).updated(oldProps, moddedProps)

            if (this.experimentalWebgl) {
                if (moddedProps.backgroundColor) {
                    Private(this).__glRenderer.setClearColor( this, this.backgroundColor, this.backgroundOpacity )
                    this._needsToBeRendered()
                }
                if (moddedProps.backgroundOpacity) {
                    Private(this).__glRenderer.setClearAlpha( this, this.backgroundOpacity )
                    this._needsToBeRendered()
                }
                if (moddedProps.shadowmapType) {
                    Private(this).__glRenderer.setShadowMapType( this, this.shadowmapType )
                    this._needsToBeRendered()
                }
                if (moddedProps.vr) {
                    Private(this).__glRenderer.enableVR( this, this.vr)

                    if ( this.vr ) {
                        Motor.setFrameRequester( fn => Private(this).__glRenderer.requestFrame( this, fn ) )
                        Private(this).__glRenderer.createDefaultWebVREntryUI( this )
                    }
                    else {
                        // TODO else return back to normal requestAnimationFrame
                    }
                }
            }

            if (moddedProps.sizeMode) {
                this._startOrStopSizePolling()
            }
        },

    }))

})

// TODO for now, hard-mixin the HTMLInterface class. We'll do this automatically later.
Scene = Scene.mixin(HTMLInterface)

export {Scene as default}

function documentBody() {
    return new Promise(resolve => {
        if (document.body) return resolve()

        const observer = new MutationObserver(() => {
            if (document.body) {
                resolve()
                observer.disconnect()
            }
        })

        observer.observe(document.documentElement, {childList: true})
    })
}
<|endoftext|>


<commit_before>const throttle = require('lodash.throttle')
// we inline file-type module, as opposed to using the NPM version,
// because of this https://github.com/sindresorhus/file-type/issues/78
// and https://github.com/sindresorhus/copy-text-to-clipboard/issues/5
const fileType = require('../vendor/file-type')
const html = require('yo-yo')

/**
 * A collection of small utility functions that help with dom manipulation, adding listeners,
 * promises and other good things.
 *
 * @module Utils
 */

/**
 * Shallow flatten nested arrays.
 */
function flatten (arr) {
  return [].concat.apply([], arr)
}

function isTouchDevice () {
  return 'ontouchstart' in window || // works on most browsers
          navigator.maxTouchPoints   // works on IE10/11 and Surface
}

// /**
//  * Shorter and fast way to select a single node in the DOM
//  * @param   { String } selector - unique dom selector
//  * @param   { Object } ctx - DOM node where the target of our search will is located
//  * @returns { Object } dom node found
//  */
// function $ (selector, ctx) {
//   return (ctx || document).querySelector(selector)
// }

// /**
//  * Shorter and fast way to select multiple nodes in the DOM
//  * @param   { String|Array } selector - DOM selector or nodes list
//  * @param   { Object } ctx - DOM node where the targets of our search will is located
//  * @returns { Object } dom nodes found
//  */
// function $$ (selector, ctx) {
//   var els
//   if (typeof selector === 'string') {
//     els = (ctx || document).querySelectorAll(selector)
//   } else {
//     els = selector
//     return Array.prototype.slice.call(els)
//   }
// }

function truncateString (str, length) {
  if (str.length > length) {
    return str.substr(0, length / 2) + '...' + str.substr(str.length - length / 4, str.length)
  }
  return str

  // more precise version if needed
  // http://stackoverflow.com/a/831583
}

function secondsToTime (rawSeconds) {
  const hours = Math.floor(rawSeconds / 3600) % 24
  const minutes = Math.floor(rawSeconds / 60) % 60
  const seconds = Math.floor(rawSeconds % 60)

  return { hours, minutes, seconds }
}

/**
 * Partition array by a grouping function.
 * @param  {[type]} array      Input array
 * @param  {[type]} groupingFn Grouping function
 * @return {[type]}            Array of arrays
 */
function groupBy (array, groupingFn) {
  return array.reduce((result, item) => {
    let key = groupingFn(item)
    let xs = result.get(key) || []
    xs.push(item)
    result.set(key, xs)
    return result
  }, new Map())
}

/**
 * Tests if every array element passes predicate
 * @param  {Array}  array       Input array
 * @param  {Object} predicateFn Predicate
 * @return {bool}               Every element pass
 */
function every (array, predicateFn) {
  return array.reduce((result, item) => {
    if (!result) {
      return false
    }

    return predicateFn(item)
  }, true)
}

/**
 * Converts list into array
*/
function toArray (list) {
  return Array.prototype.slice.call(list || [], 0)
}

/**
 * Takes a fileName and turns it into fileID, by converting to lowercase,
 * removing extra characters and adding unix timestamp
 *
 * @param {String} fileName
 *
 */
function generateFileID (file) {
  console.log(file)
  let fileID = file.name.toLowerCase()
  fileID = fileID.replace(/[^A-Z0-9]/ig, '')
  fileID = fileID + file.data.lastModified
  return fileID
}

function extend (...objs) {
  return Object.assign.apply(this, [{}].concat(objs))
}

/**
 * Runs an array of promise-returning functions in sequence.
 */
function runPromiseSequence (functions, ...args) {
  let promise = Promise.resolve()
  functions.forEach((func) => {
    promise = promise.then(() => func(...args))
  })
  return promise
}

/**
 * Takes function or class, returns its name.
 * Because IE doesn’t support `constructor.name`.
 * https://gist.github.com/dfkaye/6384439, http://stackoverflow.com/a/15714445
 *
 * @param {Object} fn — function
 *
 */
// function getFnName (fn) {
//   var f = typeof fn === 'function'
//   var s = f && ((fn.name && ['', fn.name]) || fn.toString().match(/function ([^\(]+)/))
//   return (!f && 'not a function') || (s && s[1] || 'anonymous')
// }

function isPreviewSupported (fileTypeSpecific) {
  // list of images that browsers can preview
  if (/^(jpeg|gif|png|svg|svg\+xml|bmp)$/.test(fileTypeSpecific)) {
    return true
  }
  return false
}

function getArrayBuffer (chunk) {
  return new Promise(function (resolve, reject) {
    var reader = new FileReader()
    reader.addEventListener('load', function (e) {
      // e.target.result is an ArrayBuffer
      resolve(e.target.result)
    })
    reader.addEventListener('error', function (err) {
      console.error('FileReader error' + err)
      reject(err)
    })
    // file-type only needs the first 4100 bytes
    reader.readAsArrayBuffer(chunk)
  })
}

function getFileType (file) {
  const emptyFileType = ['', '']
  const extensionsToMime = {
    'md': 'text/markdown',
    'markdown': 'text/markdown',
    'mp4': 'video/mp4',
    'mp3': 'audio/mp3'
  }

  // no smart detection for remote files, just trust the provider
  if (file.isRemote) {
    return Promise.resolve(file.type.split('/'))
  }

  const fileExtension = getFileNameAndExtension(file.name)[1]

  // 1. try to determine file type from magic bytes with file-type module
  // this should be the most trustworthy way
  const chunk = file.data.slice(0, 4100)
  return getArrayBuffer(chunk)
    .then((buffer) => {
      const type = fileType(buffer)
      if (type && type.mime) {
        return type.mime.split('/')
      }

      // 2. if that’s no good, check if mime type is set in the file object
      if (file.type) {
        return file.type.split('/')
      }

      // 3. if that’s no good, see if we can map extension to a mime type
      if (extensionsToMime[fileExtension]) {
        return extensionsToMime[fileExtension].split('/')
      }

      // if all fails, well, return empty
      return emptyFileType
    })
    .catch(() => {
      return emptyFileType
    })

    // if (file.type) {
    //   return Promise.resolve(file.type.split('/'))
    // }
    // return mime.lookup(file.name)
    // return file.type ? file.type.split('/') : ['', '']
}

// TODO Check which types are actually supported in browsers. Chrome likes webm
// from my testing, but we may need more.
// We could use a library but they tend to contain dozens of KBs of mappings,
// most of which will go unused, so not sure if that's worth it.
const mimeToExtensions = {
  'video/ogg': 'ogv',
  'audio/ogg': 'ogg',
  'video/webm': 'webm',
  'audio/webm': 'webm',
  'video/mp4': 'mp4',
  'audio/mp3': 'mp3'
}

function getFileTypeExtension (mimeType) {
  return mimeToExtensions[mimeType] || null
}

// returns [fileName, fileExt]
function getFileNameAndExtension (fullFileName) {
  var re = /(?:\.([^.]+))?$/
  var fileExt = re.exec(fullFileName)[1]
  var fileName = fullFileName.replace('.' + fileExt, '')
  return [fileName, fileExt]
}

function supportsMediaRecorder () {
  return typeof MediaRecorder === 'function' && !!MediaRecorder.prototype &&
    typeof MediaRecorder.prototype.start === 'function'
}

/**
 * Check if a URL string is an object URL from `URL.createObjectURL`.
 *
 * @param {string} url
 * @return {boolean}
 */
function isObjectURL (url) {
  return url.indexOf('blob:') === 0
}

function getProportionalHeight (img, width) {
  const aspect = img.width / img.height
  return Math.round(width / aspect)
}

/**
 * Create a thumbnail for the given Uppy file object.
 *
 * @param {{data: Blob}} file
 * @param {number} width
 * @return {Promise}
 */
function createThumbnail (file, width) {
  const originalUrl = URL.createObjectURL(file.data)

  const onload = new Promise((resolve, reject) => {
    const image = new Image()
    image.src = originalUrl
    image.onload = () => {
      URL.revokeObjectURL(originalUrl)
      resolve(image)
    }
    image.onerror = () => {
      // The onerror event is totally useless unfortunately, as far as I know
      URL.revokeObjectURL(originalUrl)
      reject(new Error('Could not create thumbnail'))
    }
  })

  return onload.then((image) => {
    const height = getProportionalHeight(image, width)

    const canvas = html`<canvas></canvas>`
    canvas.width = width
    canvas.height = height

    const context = canvas.getContext('2d')
    context.drawImage(image, 0, 0, width, height)

    return canvasToBlob(canvas)
  }).then((blob) => {
    return URL.createObjectURL(blob)
  })
}

/**
 * Save a <canvas> element's content to a Blob object.
 *
 * @param {HTMLCanvasElement} canvas
 * @return {Promise}
 */
function canvasToBlob (canvas) {
  if (canvas.toBlob) {
    return new Promise((resolve) => {
      canvas.toBlob(resolve)
    })
  }
  return Promise.resolve().then(() => {
    return dataURItoBlob(canvas.toDataURL(), {})
  })
}

function dataURItoBlob (dataURI, opts, toFile) {
  // get the base64 data
  var data = dataURI.split(',')[1]

  // user may provide mime type, if not get it from data URI
  var mimeType = opts.mimeType || dataURI.split(',')[0].split(':')[1].split(';')[0]

  // default to plain/text if data URI has no mimeType
  if (mimeType == null) {
    mimeType = 'plain/text'
  }

  var binary = atob(data)
  var array = []
  for (var i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i))
  }

  // Convert to a File?
  if (toFile) {
    return new File([new Uint8Array(array)], opts.name || '', {type: mimeType})
  }

  return new Blob([new Uint8Array(array)], {type: mimeType})
}

function dataURItoFile (dataURI, opts) {
  return dataURItoBlob(dataURI, opts, true)
}

/**
 * Copies text to clipboard by creating an almost invisible textarea,
 * adding text there, then running execCommand('copy').
 * Falls back to prompt() when the easy way fails (hello, Safari!)
 * From http://stackoverflow.com/a/30810322
 *
 * @param {String} textToCopy
 * @param {String} fallbackString
 * @return {Promise}
 */
function copyToClipboard (textToCopy, fallbackString) {
  fallbackString = fallbackString || 'Copy the URL below'

  return new Promise((resolve, reject) => {
    const textArea = document.createElement('textarea')
    textArea.setAttribute('style', {
      position: 'fixed',
      top: 0,
      left: 0,
      width: '2em',
      height: '2em',
      padding: 0,
      border: 'none',
      outline: 'none',
      boxShadow: 'none',
      background: 'transparent'
    })

    textArea.value = textToCopy
    document.body.appendChild(textArea)
    textArea.select()

    const magicCopyFailed = (err) => {
      document.body.removeChild(textArea)
      window.prompt(fallbackString, textToCopy)
      return reject('Oops, unable to copy displayed fallback prompt: ' + err)
    }

    try {
      const successful = document.execCommand('copy')
      if (!successful) {
        return magicCopyFailed('copy command unavailable')
      }
      document.body.removeChild(textArea)
      return resolve()
    } catch (err) {
      document.body.removeChild(textArea)
      return magicCopyFailed(err)
    }
  })
}

function getSpeed (fileProgress) {
  if (!fileProgress.bytesUploaded) return 0

  const timeElapsed = (new Date()) - fileProgress.uploadStarted
  const uploadSpeed = fileProgress.bytesUploaded / (timeElapsed / 1000)
  return uploadSpeed
}

function getBytesRemaining (fileProgress) {
  return fileProgress.bytesTotal - fileProgress.bytesUploaded
}

function getETA (fileProgress) {
  if (!fileProgress.bytesUploaded) return 0

  const uploadSpeed = getSpeed(fileProgress)
  const bytesRemaining = getBytesRemaining(fileProgress)
  const secondsRemaining = Math.round(bytesRemaining / uploadSpeed * 10) / 10

  return secondsRemaining
}

function prettyETA (seconds) {
  const time = secondsToTime(seconds)

  // Only display hours and minutes if they are greater than 0 but always
  // display minutes if hours is being displayed
  // Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
  const hoursStr = time.hours ? time.hours + 'h ' : ''
  const minutesVal = time.hours ? ('0' + time.minutes).substr(-2) : time.minutes
  const minutesStr = minutesVal ? minutesVal + 'm ' : ''
  const secondsVal = minutesVal ? ('0' + time.seconds).substr(-2) : time.seconds
  const secondsStr = secondsVal + 's'

  return `${hoursStr}${minutesStr}${secondsStr}`
}

/**
 * Check if an object is a DOM element. Duck-typing based on `nodeType`.
 *
 * @param {*} obj
 */
function isDOMElement (obj) {
  return obj && typeof obj === 'object' && obj.nodeType === Node.ELEMENT_NODE
}

/**
 * Find a DOM element.
 *
 * @param {Node|string} element
 * @return {Node|null}
 */
function findDOMElement (element) {
  if (typeof element === 'string') {
    return document.querySelector(element)
  }

  if (typeof element === 'object' && isDOMElement(element)) {
    return element
  }
}

function getSocketHost (url) {
  // get the host domain
  var regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/
  var host = regex.exec(url)[1]
  var socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws'

  return `${socketProtocol}://${host}`
}

function _emitSocketProgress (uploader, progressData, file) {
  const {progress, bytesUploaded, bytesTotal} = progressData
  if (progress) {
    uploader.core.log(`Upload progress: ${progress}`)
    uploader.core.emitter.emit('core:upload-progress', {
      uploader,
      id: file.id,
      bytesUploaded: bytesUploaded,
      bytesTotal: bytesTotal
    })
  }
}

const emitSocketProgress = throttle(_emitSocketProgress, 300, {leading: true, trailing: true})

module.exports = {
  generateFileID,
  toArray,
  every,
  flatten,
  groupBy,
  extend,
  runPromiseSequence,
  supportsMediaRecorder,
  isTouchDevice,
  getFileNameAndExtension,
  truncateString,
  getFileTypeExtension,
  getFileType,
  getArrayBuffer,
  isPreviewSupported,
  isObjectURL,
  createThumbnail,
  secondsToTime,
  dataURItoBlob,
  dataURItoFile,
  getSpeed,
  getBytesRemaining,
  getETA,
  copyToClipboard,
  prettyETA,
  findDOMElement,
  getSocketHost,
  emitSocketProgress
}
<commit_msg>Use `document.createElement` for the resize canvas.<commit_after>const throttle = require('lodash.throttle')
// we inline file-type module, as opposed to using the NPM version,
// because of this https://github.com/sindresorhus/file-type/issues/78
// and https://github.com/sindresorhus/copy-text-to-clipboard/issues/5
const fileType = require('../vendor/file-type')

/**
 * A collection of small utility functions that help with dom manipulation, adding listeners,
 * promises and other good things.
 *
 * @module Utils
 */

/**
 * Shallow flatten nested arrays.
 */
function flatten (arr) {
  return [].concat.apply([], arr)
}

function isTouchDevice () {
  return 'ontouchstart' in window || // works on most browsers
          navigator.maxTouchPoints   // works on IE10/11 and Surface
}

// /**
//  * Shorter and fast way to select a single node in the DOM
//  * @param   { String } selector - unique dom selector
//  * @param   { Object } ctx - DOM node where the target of our search will is located
//  * @returns { Object } dom node found
//  */
// function $ (selector, ctx) {
//   return (ctx || document).querySelector(selector)
// }

// /**
//  * Shorter and fast way to select multiple nodes in the DOM
//  * @param   { String|Array } selector - DOM selector or nodes list
//  * @param   { Object } ctx - DOM node where the targets of our search will is located
//  * @returns { Object } dom nodes found
//  */
// function $$ (selector, ctx) {
//   var els
//   if (typeof selector === 'string') {
//     els = (ctx || document).querySelectorAll(selector)
//   } else {
//     els = selector
//     return Array.prototype.slice.call(els)
//   }
// }

function truncateString (str, length) {
  if (str.length > length) {
    return str.substr(0, length / 2) + '...' + str.substr(str.length - length / 4, str.length)
  }
  return str

  // more precise version if needed
  // http://stackoverflow.com/a/831583
}

function secondsToTime (rawSeconds) {
  const hours = Math.floor(rawSeconds / 3600) % 24
  const minutes = Math.floor(rawSeconds / 60) % 60
  const seconds = Math.floor(rawSeconds % 60)

  return { hours, minutes, seconds }
}

/**
 * Partition array by a grouping function.
 * @param  {[type]} array      Input array
 * @param  {[type]} groupingFn Grouping function
 * @return {[type]}            Array of arrays
 */
function groupBy (array, groupingFn) {
  return array.reduce((result, item) => {
    let key = groupingFn(item)
    let xs = result.get(key) || []
    xs.push(item)
    result.set(key, xs)
    return result
  }, new Map())
}

/**
 * Tests if every array element passes predicate
 * @param  {Array}  array       Input array
 * @param  {Object} predicateFn Predicate
 * @return {bool}               Every element pass
 */
function every (array, predicateFn) {
  return array.reduce((result, item) => {
    if (!result) {
      return false
    }

    return predicateFn(item)
  }, true)
}

/**
 * Converts list into array
*/
function toArray (list) {
  return Array.prototype.slice.call(list || [], 0)
}

/**
 * Takes a fileName and turns it into fileID, by converting to lowercase,
 * removing extra characters and adding unix timestamp
 *
 * @param {String} fileName
 *
 */
function generateFileID (file) {
  console.log(file)
  let fileID = file.name.toLowerCase()
  fileID = fileID.replace(/[^A-Z0-9]/ig, '')
  fileID = fileID + file.data.lastModified
  return fileID
}

function extend (...objs) {
  return Object.assign.apply(this, [{}].concat(objs))
}

/**
 * Runs an array of promise-returning functions in sequence.
 */
function runPromiseSequence (functions, ...args) {
  let promise = Promise.resolve()
  functions.forEach((func) => {
    promise = promise.then(() => func(...args))
  })
  return promise
}

/**
 * Takes function or class, returns its name.
 * Because IE doesn’t support `constructor.name`.
 * https://gist.github.com/dfkaye/6384439, http://stackoverflow.com/a/15714445
 *
 * @param {Object} fn — function
 *
 */
// function getFnName (fn) {
//   var f = typeof fn === 'function'
//   var s = f && ((fn.name && ['', fn.name]) || fn.toString().match(/function ([^\(]+)/))
//   return (!f && 'not a function') || (s && s[1] || 'anonymous')
// }

function isPreviewSupported (fileTypeSpecific) {
  // list of images that browsers can preview
  if (/^(jpeg|gif|png|svg|svg\+xml|bmp)$/.test(fileTypeSpecific)) {
    return true
  }
  return false
}

function getArrayBuffer (chunk) {
  return new Promise(function (resolve, reject) {
    var reader = new FileReader()
    reader.addEventListener('load', function (e) {
      // e.target.result is an ArrayBuffer
      resolve(e.target.result)
    })
    reader.addEventListener('error', function (err) {
      console.error('FileReader error' + err)
      reject(err)
    })
    // file-type only needs the first 4100 bytes
    reader.readAsArrayBuffer(chunk)
  })
}

function getFileType (file) {
  const emptyFileType = ['', '']
  const extensionsToMime = {
    'md': 'text/markdown',
    'markdown': 'text/markdown',
    'mp4': 'video/mp4',
    'mp3': 'audio/mp3'
  }

  // no smart detection for remote files, just trust the provider
  if (file.isRemote) {
    return Promise.resolve(file.type.split('/'))
  }

  const fileExtension = getFileNameAndExtension(file.name)[1]

  // 1. try to determine file type from magic bytes with file-type module
  // this should be the most trustworthy way
  const chunk = file.data.slice(0, 4100)
  return getArrayBuffer(chunk)
    .then((buffer) => {
      const type = fileType(buffer)
      if (type && type.mime) {
        return type.mime.split('/')
      }

      // 2. if that’s no good, check if mime type is set in the file object
      if (file.type) {
        return file.type.split('/')
      }

      // 3. if that’s no good, see if we can map extension to a mime type
      if (extensionsToMime[fileExtension]) {
        return extensionsToMime[fileExtension].split('/')
      }

      // if all fails, well, return empty
      return emptyFileType
    })
    .catch(() => {
      return emptyFileType
    })

    // if (file.type) {
    //   return Promise.resolve(file.type.split('/'))
    // }
    // return mime.lookup(file.name)
    // return file.type ? file.type.split('/') : ['', '']
}

// TODO Check which types are actually supported in browsers. Chrome likes webm
// from my testing, but we may need more.
// We could use a library but they tend to contain dozens of KBs of mappings,
// most of which will go unused, so not sure if that's worth it.
const mimeToExtensions = {
  'video/ogg': 'ogv',
  'audio/ogg': 'ogg',
  'video/webm': 'webm',
  'audio/webm': 'webm',
  'video/mp4': 'mp4',
  'audio/mp3': 'mp3'
}

function getFileTypeExtension (mimeType) {
  return mimeToExtensions[mimeType] || null
}

// returns [fileName, fileExt]
function getFileNameAndExtension (fullFileName) {
  var re = /(?:\.([^.]+))?$/
  var fileExt = re.exec(fullFileName)[1]
  var fileName = fullFileName.replace('.' + fileExt, '')
  return [fileName, fileExt]
}

function supportsMediaRecorder () {
  return typeof MediaRecorder === 'function' && !!MediaRecorder.prototype &&
    typeof MediaRecorder.prototype.start === 'function'
}

/**
 * Check if a URL string is an object URL from `URL.createObjectURL`.
 *
 * @param {string} url
 * @return {boolean}
 */
function isObjectURL (url) {
  return url.indexOf('blob:') === 0
}

function getProportionalHeight (img, width) {
  const aspect = img.width / img.height
  return Math.round(width / aspect)
}

/**
 * Create a thumbnail for the given Uppy file object.
 *
 * @param {{data: Blob}} file
 * @param {number} width
 * @return {Promise}
 */
function createThumbnail (file, width) {
  const originalUrl = URL.createObjectURL(file.data)

  const onload = new Promise((resolve, reject) => {
    const image = new Image()
    image.src = originalUrl
    image.onload = () => {
      URL.revokeObjectURL(originalUrl)
      resolve(image)
    }
    image.onerror = () => {
      // The onerror event is totally useless unfortunately, as far as I know
      URL.revokeObjectURL(originalUrl)
      reject(new Error('Could not create thumbnail'))
    }
  })

  return onload.then((image) => {
    const height = getProportionalHeight(image, width)

    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height

    const context = canvas.getContext('2d')
    context.drawImage(image, 0, 0, width, height)

    return canvasToBlob(canvas)
  }).then((blob) => {
    return URL.createObjectURL(blob)
  })
}

/**
 * Save a <canvas> element's content to a Blob object.
 *
 * @param {HTMLCanvasElement} canvas
 * @return {Promise}
 */
function canvasToBlob (canvas) {
  if (canvas.toBlob) {
    return new Promise((resolve) => {
      canvas.toBlob(resolve)
    })
  }
  return Promise.resolve().then(() => {
    return dataURItoBlob(canvas.toDataURL(), {})
  })
}

function dataURItoBlob (dataURI, opts, toFile) {
  // get the base64 data
  var data = dataURI.split(',')[1]

  // user may provide mime type, if not get it from data URI
  var mimeType = opts.mimeType || dataURI.split(',')[0].split(':')[1].split(';')[0]

  // default to plain/text if data URI has no mimeType
  if (mimeType == null) {
    mimeType = 'plain/text'
  }

  var binary = atob(data)
  var array = []
  for (var i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i))
  }

  // Convert to a File?
  if (toFile) {
    return new File([new Uint8Array(array)], opts.name || '', {type: mimeType})
  }

  return new Blob([new Uint8Array(array)], {type: mimeType})
}

function dataURItoFile (dataURI, opts) {
  return dataURItoBlob(dataURI, opts, true)
}

/**
 * Copies text to clipboard by creating an almost invisible textarea,
 * adding text there, then running execCommand('copy').
 * Falls back to prompt() when the easy way fails (hello, Safari!)
 * From http://stackoverflow.com/a/30810322
 *
 * @param {String} textToCopy
 * @param {String} fallbackString
 * @return {Promise}
 */
function copyToClipboard (textToCopy, fallbackString) {
  fallbackString = fallbackString || 'Copy the URL below'

  return new Promise((resolve, reject) => {
    const textArea = document.createElement('textarea')
    textArea.setAttribute('style', {
      position: 'fixed',
      top: 0,
      left: 0,
      width: '2em',
      height: '2em',
      padding: 0,
      border: 'none',
      outline: 'none',
      boxShadow: 'none',
      background: 'transparent'
    })

    textArea.value = textToCopy
    document.body.appendChild(textArea)
    textArea.select()

    const magicCopyFailed = (err) => {
      document.body.removeChild(textArea)
      window.prompt(fallbackString, textToCopy)
      return reject('Oops, unable to copy displayed fallback prompt: ' + err)
    }

    try {
      const successful = document.execCommand('copy')
      if (!successful) {
        return magicCopyFailed('copy command unavailable')
      }
      document.body.removeChild(textArea)
      return resolve()
    } catch (err) {
      document.body.removeChild(textArea)
      return magicCopyFailed(err)
    }
  })
}

function getSpeed (fileProgress) {
  if (!fileProgress.bytesUploaded) return 0

  const timeElapsed = (new Date()) - fileProgress.uploadStarted
  const uploadSpeed = fileProgress.bytesUploaded / (timeElapsed / 1000)
  return uploadSpeed
}

function getBytesRemaining (fileProgress) {
  return fileProgress.bytesTotal - fileProgress.bytesUploaded
}

function getETA (fileProgress) {
  if (!fileProgress.bytesUploaded) return 0

  const uploadSpeed = getSpeed(fileProgress)
  const bytesRemaining = getBytesRemaining(fileProgress)
  const secondsRemaining = Math.round(bytesRemaining / uploadSpeed * 10) / 10

  return secondsRemaining
}

function prettyETA (seconds) {
  const time = secondsToTime(seconds)

  // Only display hours and minutes if they are greater than 0 but always
  // display minutes if hours is being displayed
  // Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
  const hoursStr = time.hours ? time.hours + 'h ' : ''
  const minutesVal = time.hours ? ('0' + time.minutes).substr(-2) : time.minutes
  const minutesStr = minutesVal ? minutesVal + 'm ' : ''
  const secondsVal = minutesVal ? ('0' + time.seconds).substr(-2) : time.seconds
  const secondsStr = secondsVal + 's'

  return `${hoursStr}${minutesStr}${secondsStr}`
}

/**
 * Check if an object is a DOM element. Duck-typing based on `nodeType`.
 *
 * @param {*} obj
 */
function isDOMElement (obj) {
  return obj && typeof obj === 'object' && obj.nodeType === Node.ELEMENT_NODE
}

/**
 * Find a DOM element.
 *
 * @param {Node|string} element
 * @return {Node|null}
 */
function findDOMElement (element) {
  if (typeof element === 'string') {
    return document.querySelector(element)
  }

  if (typeof element === 'object' && isDOMElement(element)) {
    return element
  }
}

function getSocketHost (url) {
  // get the host domain
  var regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/
  var host = regex.exec(url)[1]
  var socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws'

  return `${socketProtocol}://${host}`
}

function _emitSocketProgress (uploader, progressData, file) {
  const {progress, bytesUploaded, bytesTotal} = progressData
  if (progress) {
    uploader.core.log(`Upload progress: ${progress}`)
    uploader.core.emitter.emit('core:upload-progress', {
      uploader,
      id: file.id,
      bytesUploaded: bytesUploaded,
      bytesTotal: bytesTotal
    })
  }
}

const emitSocketProgress = throttle(_emitSocketProgress, 300, {leading: true, trailing: true})

module.exports = {
  generateFileID,
  toArray,
  every,
  flatten,
  groupBy,
  extend,
  runPromiseSequence,
  supportsMediaRecorder,
  isTouchDevice,
  getFileNameAndExtension,
  truncateString,
  getFileTypeExtension,
  getFileType,
  getArrayBuffer,
  isPreviewSupported,
  isObjectURL,
  createThumbnail,
  secondsToTime,
  dataURItoBlob,
  dataURItoFile,
  getSpeed,
  getBytesRemaining,
  getETA,
  copyToClipboard,
  prettyETA,
  findDOMElement,
  getSocketHost,
  emitSocketProgress
}
<|endoftext|>


<commit_before>/**
 * @description 创建一个类
 * @param {String}    fullClassName  类全名，包括命名空间。
 * @param {Plain}     defines        要创建的类的特性
 *     defines.constructor  {Function}       类的构造函数，实例化的时候会被调用。
 *     defines.base         {String}         基类的名称。名称要使用全名。（因为base是javascript未来保留字，所以不用base）
 *     defines.mixin        {Array<String>}  要混合到新类的类集合
 *     defines.<method>     {Function}       其他类方法
 *
 * TODO:
 *     Mixin 构造函数调用支持
 */
define( function ( require, exports ) {

    // just to bind context
    Function.prototype.bind = Function.prototype.bind || function(thisObj) {
        var args = Array.prototype.slice.call(arguments, 1);
        return this.apply(thisObj, args);
    };

    var config = require( 'core/config' );

    // 方便调试查看
    if ( config.debug ) {
        var origin = Object.prototype.toString;
        Object.prototype.toString = function () {
            return this.__KityClassName || origin.call( this );
        };
    }

    // 所有类的基类
    function Class() {}
    Class.__KityClassName = 'Class';

    function getCallerClass( instance, caller ) {
        var currentClass = instance.constructor;
    }

    // 提供 base 调用支持
    Class.prototype.base = function ( name ) {
        var caller = arguments.callee.caller;
        var method = caller.__KityMethodClass.__KityBaseClass.prototype[ name ];
        return method.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    };

    // 直接调用 base 类的同名方法
    Class.prototype.callBase = function () {
        var caller = arguments.callee.caller;
        var method = caller.__KityMethodClass.__KityBaseClass.prototype[ caller.__KityMethodName ];
        return method.apply( this, arguments );
    };

    Class.prototype.mixin = function ( name ) {
        var caller = arguments.callee.caller;
        var mixins = caller.__KityMethodClass.__KityMixins;
        if(!mixins) {
            return this;
        }
        var method = mixins[ name ];
        return method.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    };

    Class.prototype.callMixin = function () {
        var caller = arguments.callee.caller;
        var methodName = caller.__KityMethodName;
        var mixins = caller.__KityMethodClass.__KityMixins;
        if(!mixins) {
            return this;
        }
        var method = mixins[ methodName ];
        if ( methodName == 'constructor' ) {
            for ( var i = 0, l = method.length; i < l; i++ ) {
                method[ i ].call( this );
            }
            return this;
        } else {
            return method.apply( this, arguments );
        }
    };

    Class.prototype.pipe = function(fn) {
        if(typeof(fn) =='function') {
            fn.call(this, this);
        }
        return this;
    };

    // 检查基类是否调用了父类的构造函数
    // 该检查是弱检查，假如调用的代码被注释了，同样能检查成功（这个特性可用于知道建议调用，但是出于某些原因不想调用的情况）
    function checkBaseConstructorCall( targetClass, classname ) {
        var code = targetClass.toString();
        if ( !/this\.callBase/.test( code ) ) {
            throw new Error( classname + ' : 类构造函数没有调用父类的构造函数！为了安全，请调用父类的构造函数' );
        }
    }

    var KITY_INHERIT_FLAG = '__KITY_INHERIT_FLAG_' + (+new Date());

    function inherit( constructor, BaseClass ) {

        var KityClass = function( __inherit__flag ) {
            if( __inherit__flag != KITY_INHERIT_FLAG ) {
                KityClass.__KityConstructor.apply(this, arguments);
            }
            this.__KityClassName = KityClass.__KityClassName;
        };
        KityClass.__KityConstructor = constructor;

        KityClass.prototype = new BaseClass(KITY_INHERIT_FLAG);

        for(var methodName in BaseClass.prototype) {
            if(BaseClass.prototype.hasOwnProperty(methodName) && methodName.indexOf('__Kity') !== 0) {
                KityClass.prototype[methodName] = BaseClass.prototype[methodName];
            }
        }

        KityClass.prototype.constructor = KityClass;

        return KityClass;
    }

    function mixin( NewClass, mixins ) {
        if ( false === mixins instanceof Array ) {
            return NewClass;
        }

        var i, length = mixins.length, proto, method;

        NewClass.__KityMixins = {
            constructor: []
        };

        for ( i = 0; i < length; i++ ) {
            proto = mixins[i].prototype;

            for( method in proto ) {
                if( false === proto.hasOwnProperty(method) || method.indexOf('__Kity') === 0) {
                    continue;
                }
                if( method === 'constructor' ) {
                    // constructor 特殊处理
                    NewClass.__KityMixins.constructor.push( proto[method] );
                } else {
                    NewClass.prototype[method] = NewClass.__KityMixins[method] = proto[method];
                }
            }
        }

        return NewClass;
    }

    function extend( BaseClass, extension ) {
        if(extension.__KityClassName) {
            extension = extension.prototype;
        }
        for(var methodName in extension) {
            if(extension.hasOwnProperty(methodName) &&
                methodName.indexOf('__Kity') &&
                methodName != 'constructor') {
                var method = BaseClass.prototype[ methodName ] = extension[ methodName ];
                method.__KityMethodClass = BaseClass;
                method.__KityMethodName = methodName;
            }
        }
        return BaseClass;
    }

    exports.createClass = function ( classname, defines ) {
        var constructor, NewClass, BaseClass;

        if(arguments.length === 1) {
            defines = arguments[0];
            classname = 'AnonymousClass';
        }

        BaseClass = defines.base || Class;

        if ( defines.hasOwnProperty( 'constructor' ) ) {
            constructor = defines.constructor;
            if ( BaseClass != Class ) {
                checkBaseConstructorCall( constructor, classname );
            }
        } else {
            constructor = function () {
                this.callBase.apply(this, arguments);
                this.callMixin.apply(this, arguments);
            };
        }

        NewClass = inherit( constructor, BaseClass );
        NewClass = mixin( NewClass, defines.mixins );

        NewClass.__KityClassName = constructor.__KityClassName = classname;
        NewClass.__KityBaseClass = constructor.__KityBaseClass = BaseClass;

        NewClass.__KityMethodName = constructor.__KityMethodName = 'constructor';
        NewClass.__KityMethodClass = constructor.__KityMethodClass = NewClass;

        // 下面这些不需要拷贝到原型链上
        delete defines.mixins;
        delete defines.constructor;
        delete defines.base;

        NewClass = extend( NewClass, defines );

        return NewClass;
    };

    exports.extendClass = extend;

} );<commit_msg>添加返回对象类型的方法<commit_after>/**
 * @description 创建一个类
 * @param {String}    fullClassName  类全名，包括命名空间。
 * @param {Plain}     defines        要创建的类的特性
 *     defines.constructor  {Function}       类的构造函数，实例化的时候会被调用。
 *     defines.base         {String}         基类的名称。名称要使用全名。（因为base是javascript未来保留字，所以不用base）
 *     defines.mixin        {Array<String>}  要混合到新类的类集合
 *     defines.<method>     {Function}       其他类方法
 *
 * TODO:
 *     Mixin 构造函数调用支持
 */
define( function ( require, exports ) {

    // just to bind context
    Function.prototype.bind = Function.prototype.bind || function(thisObj) {
        var args = Array.prototype.slice.call(arguments, 1);
        return this.apply(thisObj, args);
    };

    var config = require( 'core/config' );

    // 方便调试查看
    if ( config.debug ) {
        var origin = Object.prototype.toString;
        Object.prototype.toString = function () {
            return this.__KityClassName || origin.call( this );
        };
    }

    // 所有类的基类
    function Class() {}
    Class.__KityClassName = 'Class';

    function getCallerClass( instance, caller ) {
        var currentClass = instance.constructor;
    }

    // 提供 base 调用支持
    Class.prototype.base = function ( name ) {
        var caller = arguments.callee.caller;
        var method = caller.__KityMethodClass.__KityBaseClass.prototype[ name ];
        return method.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    };

    // 直接调用 base 类的同名方法
    Class.prototype.callBase = function () {
        var caller = arguments.callee.caller;
        var method = caller.__KityMethodClass.__KityBaseClass.prototype[ caller.__KityMethodName ];
        return method.apply( this, arguments );
    };

    Class.prototype.mixin = function ( name ) {
        var caller = arguments.callee.caller;
        var mixins = caller.__KityMethodClass.__KityMixins;
        if(!mixins) {
            return this;
        }
        var method = mixins[ name ];
        return method.apply( this, Array.prototype.slice.call( arguments, 1 ) );
    };

    Class.prototype.callMixin = function () {
        var caller = arguments.callee.caller;
        var methodName = caller.__KityMethodName;
        var mixins = caller.__KityMethodClass.__KityMixins;
        if(!mixins) {
            return this;
        }
        var method = mixins[ methodName ];
        if ( methodName == 'constructor' ) {
            for ( var i = 0, l = method.length; i < l; i++ ) {
                method[ i ].call( this );
            }
            return this;
        } else {
            return method.apply( this, arguments );
        }
    };

    Class.prototype.pipe = function(fn) {
        if(typeof(fn) =='function') {
            fn.call(this, this);
        }
        return this;
    };

    Class.prototype.getType = function() {
        return this.__KityClassName;
    };

    // 检查基类是否调用了父类的构造函数
    // 该检查是弱检查，假如调用的代码被注释了，同样能检查成功（这个特性可用于知道建议调用，但是出于某些原因不想调用的情况）
    function checkBaseConstructorCall( targetClass, classname ) {
        var code = targetClass.toString();
        if ( !/this\.callBase/.test( code ) ) {
            throw new Error( classname + ' : 类构造函数没有调用父类的构造函数！为了安全，请调用父类的构造函数' );
        }
    }

    var KITY_INHERIT_FLAG = '__KITY_INHERIT_FLAG_' + (+new Date());

    function inherit( constructor, BaseClass ) {

        var KityClass = function( __inherit__flag ) {
            if( __inherit__flag != KITY_INHERIT_FLAG ) {
                KityClass.__KityConstructor.apply(this, arguments);
            }
            this.__KityClassName = KityClass.__KityClassName;
        };
        KityClass.__KityConstructor = constructor;

        KityClass.prototype = new BaseClass(KITY_INHERIT_FLAG);

        for(var methodName in BaseClass.prototype) {
            if(BaseClass.prototype.hasOwnProperty(methodName) && methodName.indexOf('__Kity') !== 0) {
                KityClass.prototype[methodName] = BaseClass.prototype[methodName];
            }
        }

        KityClass.prototype.constructor = KityClass;

        return KityClass;
    }

    function mixin( NewClass, mixins ) {
        if ( false === mixins instanceof Array ) {
            return NewClass;
        }

        var i, length = mixins.length, proto, method;

        NewClass.__KityMixins = {
            constructor: []
        };

        for ( i = 0; i < length; i++ ) {
            proto = mixins[i].prototype;

            for( method in proto ) {
                if( false === proto.hasOwnProperty(method) || method.indexOf('__Kity') === 0) {
                    continue;
                }
                if( method === 'constructor' ) {
                    // constructor 特殊处理
                    NewClass.__KityMixins.constructor.push( proto[method] );
                } else {
                    NewClass.prototype[method] = NewClass.__KityMixins[method] = proto[method];
                }
            }
        }

        return NewClass;
    }

    function extend( BaseClass, extension ) {
        if(extension.__KityClassName) {
            extension = extension.prototype;
        }
        for(var methodName in extension) {
            if(extension.hasOwnProperty(methodName) &&
                methodName.indexOf('__Kity') &&
                methodName != 'constructor') {
                var method = BaseClass.prototype[ methodName ] = extension[ methodName ];
                method.__KityMethodClass = BaseClass;
                method.__KityMethodName = methodName;
            }
        }
        return BaseClass;
    }

    exports.createClass = function ( classname, defines ) {
        var constructor, NewClass, BaseClass;

        if(arguments.length === 1) {
            defines = arguments[0];
            classname = 'AnonymousClass';
        }

        BaseClass = defines.base || Class;

        if ( defines.hasOwnProperty( 'constructor' ) ) {
            constructor = defines.constructor;
            if ( BaseClass != Class ) {
                checkBaseConstructorCall( constructor, classname );
            }
        } else {
            constructor = function () {
                this.callBase.apply(this, arguments);
                this.callMixin.apply(this, arguments);
            };
        }

        NewClass = inherit( constructor, BaseClass );
        NewClass = mixin( NewClass, defines.mixins );

        NewClass.__KityClassName = constructor.__KityClassName = classname;
        NewClass.__KityBaseClass = constructor.__KityBaseClass = BaseClass;

        NewClass.__KityMethodName = constructor.__KityMethodName = 'constructor';
        NewClass.__KityMethodClass = constructor.__KityMethodClass = NewClass;

        // 下面这些不需要拷贝到原型链上
        delete defines.mixins;
        delete defines.constructor;
        delete defines.base;

        NewClass = extend( NewClass, defines );

        return NewClass;
    };

    exports.extendClass = extend;

} );<|endoftext|>


<commit_before>if ( typeof define !== "function" ) {
  var define = require( "amdefine" )( module );
}

define( function( require ) {

  var MulticastDelegate = require( "common/multicast-delegate" );

  var C_STARTED = 0,
  C_PAUSED = 1;

  var Clock = function( delegate ) {
    this.time = 0;
    this.delta = 0;
    this._timeScale = 1.0;
    this._idealFrameInterval = 1.0/30.0;

    this._clockState = undefined;
    this.signal = new MulticastDelegate();
    this._delegate = delegate || null;

    this.start();
  };

  function pause() {
    this._clockState = C_PAUSED;
    if( this._delegate ) {
      this._delegate.unsubscribe( this.update.bind( this ) );
    }
  }

  function start() {
    this._clockState = C_STARTED;
    if( this._delegate ) {
      this._delegate.subscribe( this.update.bind( this ) );
    }
  }

  function update( delta ) {
    if( C_PAUSED !== this._clockState ) {
      this.delta = delta * this._timeScale;
      this.time += this.delta;
      this.signal( this.delta ); // Dispatch time signal
    }
  }

  function step( count ) {
    count = undefined === count ? 1 : count;
    if( C_PAUSED === this._clockState ) {
      this.delta = count * this._idealFrameInterval * this._timeScale;
      this.time += this.delta;
      this.signal( this.delta );  // Dispatch time signal
    }
  }

  function isStarted() {
    return this._clockState === C_STARTED;
  }

  function reset() {
    this.time = 0;
    this.delta = 0;
  }

  function setTimeScale( scale ) {
    this._timeScale = scale;
  }

  function setIdealFrameInterval( interval ) {
    this._idealFrameInterval = interval;
  }

  Clock.prototype = {
     pause: pause,
     start: start,
     update: update,
     isStarted: isStarted,
     step: step,
     reset: reset,
     setTimeScale: setTimeScale,
     setIdealFrameInterval: setIdealFrameInterval
  };

  return Clock;

});<commit_msg>Fixed a bunch of bugs in Clock. Now suspends and resume properly.<commit_after>if ( typeof define !== "function" ) {
  var define = require( "amdefine" )( module );
}

define( function( require ) {

  var MulticastDelegate = require( "common/multicast-delegate" );

  var C_STARTED = 0,
  C_PAUSED = 1;

  var Clock = function( delegate ) {
    this.time = 0;
    this.delta = 0;
    this._timeScale = 1.0;
    this._idealFrameInterval = 1.0/30.0;

    this._clockState = undefined;
    this.signal = new MulticastDelegate();
    this._delegate = delegate || null;

    this._delegateHandler = this.update.bind( this );
    if( this._delegate ) {
      this._delegate.subscribe( this._delegateHandler );
    }

    this._stepCount = 0;

    this.start();
  };

  function pause() {
    this._clockState = C_PAUSED;
  }

  function start() {
    this._clockState = C_STARTED;
  }

  function update( delta ) {
    if( C_PAUSED !== this._clockState ) {
      this.delta = delta * this._timeScale;
    } else {
      this.delta = this._stepCount * this._idealFrameInterval * this._timeScale;
      this._stepCount = 0;
    }
    this.time += this.delta;
    this.signal( this.delta ); // Dispatch time signal
  }

  function step( count ) {
    if( C_PAUSED === this._clockState ) {
      this._stepCount += (undefined === count) ? 1 : count;
    }
  }

  function isStarted() {
    return this._clockState === C_STARTED;
  }

  function reset( delegate ) {
    if( delegate && delegate != this._delegate ) {
      this._delegate.unsubscribe( this._delegateHandler );
      this._delegate = delegate || null;
      this._delegate.subscribe( this._delegateHandler );
    }
    this.time = 0;
    this.delta = 0;
  }

  function setTimeScale( scale ) {
    this._timeScale = scale;
  }

  function setIdealFrameInterval( interval ) {
    this._idealFrameInterval = interval;
  }

  Clock.prototype = {
     pause: pause,
     start: start,
     update: update,
     isStarted: isStarted,
     step: step,
     reset: reset,
     setTimeScale: setTimeScale,
     setIdealFrameInterval: setIdealFrameInterval
  };

  return Clock;

});<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/* Add describe() support to classes. */
foam.AbstractClass.describe = function(opt_name) {
  console.log('CLASS:  ', this.name);
  console.log('extends:', this.model_.extends);
  console.log('Axiom Type           Source Class   Name');
  console.log('----------------------------------------------------');
  for ( var key in this.axiomMap_ ) {
    var a = this.axiomMap_[key];
    console.log(
      foam.string.pad(a.cls_ ? a.cls_.name : 'anonymous', 20),
      foam.string.pad(a.sourceCls_.name, 14),
      a.name);
  }
  console.log('\n');
};

/* Add describe() support to objects. */
foam.CLASS({
  refines: 'foam.core.FObject',

  methods: [
    function describe(opt_name) {
      console.log('Instance of', this.cls_.name);
      console.log('Axiom Type           Name           Value');
      console.log('----------------------------------------------------');
      var ps = this.cls_.getAxiomsByClass(foam.core.Property);
      for ( var i = 0 ; i < ps.length ; i++ ) {
        var p = ps[i];
        console.log(
          foam.string.pad(p.cls_ ? p.cls_.name : 'anonymous', 20),
          foam.string.pad(p.name, 14),
          this[p.name]);
      }
      console.log('\n');
    }
  ]
});

/* Add describe support to contexts. */
foam.sub().__proto__.describe = function() {
  console.log('Context:', this.hasOwnProperty('NAME') ? this.NAME : ('anonymous ' + this.$UID));
  console.log('KEY                  Type           Value');
  console.log('----------------------------------------------------');
  for ( var key in this ) {
    var value = this[key];
    var type = foam.core.FObject.isInstance(value) ? value.cls_.name : typeof value;
    console.log(
      foam.string.pad(key,  20),
      foam.string.pad(type, 14),
      typeof value === 'string' || typeof value === 'number' ? value : '');
  }
  console.log('\n');
};

foam.CLASS({
  package: 'foam.debug',
  name: 'Window',

  documentation: 'Decorated merged() and framed() to have debug friendly toString() methods.',

  exports: [ 'merged', 'framed' ],

  methods: [
    function merged(l, opt_delay) {
      var f = this.X.merged(l, opt_delay);
      f.toString = function() {
        return 'MERGED(' + delay + ', ' + listener.$UID + ', ' + listener + ')';
      };
      return f;
    },
    function framed(l) {
      var f = this.X.framed(l);
      f.toString = function() {
        return 'ANIMATE(' + l.$UID + ', ' + l + ')';
      };
      return f;
    }
  ]
});

foam.X = foam.debug.Window.create(null, foam.X).Y;

foam.CLASS({
  name: 'Argument',

  documentation: "Describes one argument of a function.",

  constants: {
    PREFIX: 'Argument',
  },

  properties: [
    {
      /** The name of the argument */
      name: 'name'
    },
    {
      /** The string name of the type (either a model name or javascript typeof name) */
      name: 'typeName'
    },
    {
      /** If set, this holds the actual Model represented by typeName. */
      name: 'type'
    },
    {
      /** If true, indicates that this argument is optional. */
      name: 'optional', defaultValue: false
    },
    {
      /** The index of the argument (the first argument is at index 0). */
      name: 'index', defaultValue: -1
    },
    {
      /** The documentation associated with the argument (denoted by a // ) */
      name: 'documentation', defaultValue: ''
    }
  ],

  methods: [
    /** Validates the given argument against this type information.
        If any type checks are failed, a TypeError is thrown.
         */
    function validate(/* any // the argument value to validate. */ arg) {
      i = ( this.index >= 0 ) ? ' '+this.index+', ' : ', ';
      // optional check
      if ( ( ! arg ) && typeof arg === 'undefined' ) {
        if ( ! this.optional ) {
          throw new TypeError(this.PREFIX + i + this.name+', is not optional, but was undefined in a function call');
        } else {
          return; // value is undefined, but ok with that
        }
      }
      // type this for non-modelled types (no model, but a type name specified)
      if ( ! this.type ) {
        if (   this.typeName
            && typeof arg !== this.typeName
            && ! ( this.typeName === 'array' && Array.isArray(arg) ) ) {
          throw new TypeError(this.PREFIX + i + this.name+', expected type '+this.typeName+' but passed '+(typeof arg));
        } // else no this: no type, no typeName
      } else {
        // have a modelled type
        if ( ! arg.cls_ || ! this.type.isInstance(arg) ) {   // TODO: .cls_ check in isInstance() instead?
          var gotType = (arg.cls_) ? arg.cls_.name : typeof arg;
          throw new TypeError(this.PREFIX + i + this.name+', expected type '+this.typeName+' but passed '+gotType);
        }
      }
    }
  ]
});

/** Describes a function return type. */
foam.CLASS({
  name: 'ReturnValue',
  extends: 'Argument',

  constants: {
    PREFIX: 'Return'
  }
});

/** The types library deals with type safety. */
foam.LIB({
  name: 'types',

  methods: [
    function getFunctionArgs(fn) {
      /** Extracts the arguments and their types from the given function.
        * @arg fn The function to extract from. The toString() of the function must be
        *     accurate.
        * @return An array of Argument objects.
        */
      // strip newlines and find the function(...) declaration
      var args = foam.fn.argsStr(fn);
      if ( ! args ) return [];
      args += ','; // easier matching

      var ret = [];
      // check each arg for types
      // Optional commented type(incl. dots for packages), argument name, optional commented return type
      // ws [/* ws package.type? ws */] ws argname ws [/* ws retType ws */]
      //console.log('-----------------');
      var argIdx = 0;
      var argMatcher = /(\s*\/\*\s*([\w._$]+)(\?)?\s*(\/\/\s*(.*?))?\s*\*\/)?\s*([\w_$]+)\s*(\/\*\s*([\w._$]+)\s*\*\/)?\s*\,+/g;
      var typeMatch;
      while ((typeMatch = argMatcher.exec(args)) !== null) {
        // if can't match from start of string, fail
        if ( argIdx == 0 && typeMatch.index > 0 ) break;

        ret.push(/*X.*/Argument.create({
          name: typeMatch[6],
          typeName: typeMatch[2],
          type: global[typeMatch[2]],
          optional: typeMatch[3] == '?',
          argIdx: argIdx++,
          documentation: typeMatch[5],
        }));
        // TODO: this is only valid on the last arg
        if ( typeMatch[6] ) {
          ret.returnType = /*X.*/ReturnValue.create({
            typeName: typeMatch[8],
            type: global[typeMatch[8]]
          });
        }
      }
      if ( argIdx == 0 ) {
        // check for bare return type with no args
        typeMatch = args.match(/^\s*\/\*\s*([\w._$]+)\s*\*\/\s*/);
        if ( typeMatch && typeMatch[1] ) {
          ret.returnType = /*X.*/ReturnValue.create({
            typeName: typeMatch[1],
            type: global[typeMatch[1]]
          });
        } else {
          throw "foam.types.getFunctionArgs argument parsing error: " + args.toString();
        }
      }

      return ret;
    },

    /** Decorates the given function with a runtime type checker.
      * Types should be denoted before each argument:
      * function(\/\*TypeA\*\/ argA, \/\*string\*\/ argB) { ... }
      * Types are either the names of Models (i.e. declared with CLASS), or
      * javascript primitives as returned by 'typeof'. In addition, 'array'
      * is supported as a special case, corresponding to an Array.isArray() check.
      * @fn The function to decorate. The toString() of the function must be
      *     accurate.
      * @return A new function that will throw errors if arguments
      *         doesn't match the declared types, run the original function,
      *         then type check the returned value.
      */
    function typeCheck(fn) {
      // parse out the arguments and their types
      var args = foam.types.getFunctionArgs(fn);
      var ret = function() {
        // check each declared argument, arguments[i] can be undefined for missing optional args,
        // extra arguments are ok
        for ( var i = 0 ; i < args.length ; ++i ) {
          args[i].validate(arguments[i]);
        }

        // If nothing threw an exception, we are free to run the function
        var retVal = fn.apply(this, arguments);

        // check the return value
        if ( args.returnType ) {
          args.returnType.validate(retVal);
        }

        return retVal;
      }

      // keep the old value of toString (hide the decorator)
      ret.toString = function() { return fn.toString(); }
      return ret;
    }
  ]
});

<commit_msg>Added comments to debug.js<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/*
 * Debug.js
 *
 * This file contains refinements and replacements designed to make
 * FOAM apps easier to debug. Things like more informative toString() methods
 * .describe() on various types of objects, extra type checking, warnings,
 * and asserts, etc. Many of these features may negatively affect performance,
 * so while this file should be loaded during your day-to-day development,
 * it should not be included in production.
 */

/* Add describe() support to classes. */
foam.AbstractClass.describe = function(opt_name) {
  console.log('CLASS:  ', this.name);
  console.log('extends:', this.model_.extends);
  console.log('Axiom Type           Source Class   Name');
  console.log('----------------------------------------------------');
  for ( var key in this.axiomMap_ ) {
    var a = this.axiomMap_[key];
    console.log(
      foam.string.pad(a.cls_ ? a.cls_.name : 'anonymous', 20),
      foam.string.pad(a.sourceCls_.name, 14),
      a.name);
  }
  console.log('\n');
};

/* Add describe() support to objects. */
foam.CLASS({
  refines: 'foam.core.FObject',

  methods: [
    function describe(opt_name) {
      console.log('Instance of', this.cls_.name);
      console.log('Axiom Type           Name           Value');
      console.log('----------------------------------------------------');
      var ps = this.cls_.getAxiomsByClass(foam.core.Property);
      for ( var i = 0 ; i < ps.length ; i++ ) {
        var p = ps[i];
        console.log(
          foam.string.pad(p.cls_ ? p.cls_.name : 'anonymous', 20),
          foam.string.pad(p.name, 14),
          this[p.name]);
      }
      console.log('\n');
    }
  ]
});

/* Add describe support to contexts. */
foam.sub().__proto__.describe = function() {
  console.log('Context:', this.hasOwnProperty('NAME') ? this.NAME : ('anonymous ' + this.$UID));
  console.log('KEY                  Type           Value');
  console.log('----------------------------------------------------');
  for ( var key in this ) {
    var value = this[key];
    var type = foam.core.FObject.isInstance(value) ? value.cls_.name : typeof value;
    console.log(
      foam.string.pad(key,  20),
      foam.string.pad(type, 14),
      typeof value === 'string' || typeof value === 'number' ? value : '');
  }
  console.log('\n');
};

foam.CLASS({
  package: 'foam.debug',
  name: 'Window',

  documentation: 'Decorated merged() and framed() to have debug friendly toString() methods.',

  exports: [ 'merged', 'framed' ],

  methods: [
    function merged(l, opt_delay) {
      var f = this.X.merged(l, opt_delay);
      f.toString = function() {
        return 'MERGED(' + delay + ', ' + listener.$UID + ', ' + listener + ')';
      };
      return f;
    },
    function framed(l) {
      var f = this.X.framed(l);
      f.toString = function() {
        return 'ANIMATE(' + l.$UID + ', ' + l + ')';
      };
      return f;
    }
  ]
});

foam.X = foam.debug.Window.create(null, foam.X).Y;

foam.CLASS({
  name: 'Argument',

  documentation: "Describes one argument of a function.",

  constants: {
    PREFIX: 'Argument',
  },

  properties: [
    {
      /** The name of the argument */
      name: 'name'
    },
    {
      /** The string name of the type (either a model name or javascript typeof name) */
      name: 'typeName'
    },
    {
      /** If set, this holds the actual Model represented by typeName. */
      name: 'type'
    },
    {
      /** If true, indicates that this argument is optional. */
      name: 'optional', defaultValue: false
    },
    {
      /** The index of the argument (the first argument is at index 0). */
      name: 'index', defaultValue: -1
    },
    {
      /** The documentation associated with the argument (denoted by a // ) */
      name: 'documentation', defaultValue: ''
    }
  ],

  methods: [
    /** Validates the given argument against this type information.
        If any type checks are failed, a TypeError is thrown.
         */
    function validate(/* any // the argument value to validate. */ arg) {
      i = ( this.index >= 0 ) ? ' '+this.index+', ' : ', ';
      // optional check
      if ( ( ! arg ) && typeof arg === 'undefined' ) {
        if ( ! this.optional ) {
          throw new TypeError(this.PREFIX + i + this.name+', is not optional, but was undefined in a function call');
        } else {
          return; // value is undefined, but ok with that
        }
      }
      // type this for non-modelled types (no model, but a type name specified)
      if ( ! this.type ) {
        if (   this.typeName
            && typeof arg !== this.typeName
            && ! ( this.typeName === 'array' && Array.isArray(arg) ) ) {
          throw new TypeError(this.PREFIX + i + this.name+', expected type '+this.typeName+' but passed '+(typeof arg));
        } // else no this: no type, no typeName
      } else {
        // have a modelled type
        if ( ! arg.cls_ || ! this.type.isInstance(arg) ) {   // TODO: .cls_ check in isInstance() instead?
          var gotType = (arg.cls_) ? arg.cls_.name : typeof arg;
          throw new TypeError(this.PREFIX + i + this.name+', expected type '+this.typeName+' but passed '+gotType);
        }
      }
    }
  ]
});

/** Describes a function return type. */
foam.CLASS({
  name: 'ReturnValue',
  extends: 'Argument',

  constants: {
    PREFIX: 'Return'
  }
});

/** The types library deals with type safety. */
foam.LIB({
  name: 'types',

  methods: [
    function getFunctionArgs(fn) {
      /** Extracts the arguments and their types from the given function.
        * @arg fn The function to extract from. The toString() of the function must be
        *     accurate.
        * @return An array of Argument objects.
        */
      // strip newlines and find the function(...) declaration
      var args = foam.fn.argsStr(fn);
      if ( ! args ) return [];
      args += ','; // easier matching

      var ret = [];
      // check each arg for types
      // Optional commented type(incl. dots for packages), argument name, optional commented return type
      // ws [/* ws package.type? ws */] ws argname ws [/* ws retType ws */]
      //console.log('-----------------');
      var argIdx = 0;
      var argMatcher = /(\s*\/\*\s*([\w._$]+)(\?)?\s*(\/\/\s*(.*?))?\s*\*\/)?\s*([\w_$]+)\s*(\/\*\s*([\w._$]+)\s*\*\/)?\s*\,+/g;
      var typeMatch;
      while ((typeMatch = argMatcher.exec(args)) !== null) {
        // if can't match from start of string, fail
        if ( argIdx == 0 && typeMatch.index > 0 ) break;

        ret.push(/*X.*/Argument.create({
          name: typeMatch[6],
          typeName: typeMatch[2],
          type: global[typeMatch[2]],
          optional: typeMatch[3] == '?',
          argIdx: argIdx++,
          documentation: typeMatch[5],
        }));
        // TODO: this is only valid on the last arg
        if ( typeMatch[6] ) {
          ret.returnType = /*X.*/ReturnValue.create({
            typeName: typeMatch[8],
            type: global[typeMatch[8]]
          });
        }
      }
      if ( argIdx == 0 ) {
        // check for bare return type with no args
        typeMatch = args.match(/^\s*\/\*\s*([\w._$]+)\s*\*\/\s*/);
        if ( typeMatch && typeMatch[1] ) {
          ret.returnType = /*X.*/ReturnValue.create({
            typeName: typeMatch[1],
            type: global[typeMatch[1]]
          });
        } else {
          throw "foam.types.getFunctionArgs argument parsing error: " + args.toString();
        }
      }

      return ret;
    },

    /** Decorates the given function with a runtime type checker.
      * Types should be denoted before each argument:
      * function(\/\*TypeA\*\/ argA, \/\*string\*\/ argB) { ... }
      * Types are either the names of Models (i.e. declared with CLASS), or
      * javascript primitives as returned by 'typeof'. In addition, 'array'
      * is supported as a special case, corresponding to an Array.isArray() check.
      * @fn The function to decorate. The toString() of the function must be
      *     accurate.
      * @return A new function that will throw errors if arguments
      *         doesn't match the declared types, run the original function,
      *         then type check the returned value.
      */
    function typeCheck(fn) {
      // parse out the arguments and their types
      var args = foam.types.getFunctionArgs(fn);
      var ret = function() {
        // check each declared argument, arguments[i] can be undefined for missing optional args,
        // extra arguments are ok
        for ( var i = 0 ; i < args.length ; ++i ) {
          args[i].validate(arguments[i]);
        }

        // If nothing threw an exception, we are free to run the function
        var retVal = fn.apply(this, arguments);

        // check the return value
        if ( args.returnType ) {
          args.returnType.validate(retVal);
        }

        return retVal;
      }

      // keep the old value of toString (hide the decorator)
      ret.toString = function() { return fn.toString(); }
      return ret;
    }
  ]
});
<|endoftext|>