<commit_before>/*#ifndef(UMD)*/
(function () { /* Begin of privacy scope */
    "use strict";
/*#endif*/

    var
        gpfI = gpf.interfaces,
        _BUFREADSTREAM_READ_SIZE        = 256,
        _BUFREADSTREAM_ISTATE_INIT        = 0,
        _BUFREADSTREAM_ISTATE_INPROGRESS  = 1,
        _BUFREADSTREAM_ISTATE_WAITING     = 2,
        _BUFREADSTREAM_ISTATE_EOS         = 3;

    /**
     * The Readable stream interface is the abstraction for a source of data
     * that you are reading from. In other words, data comes out of a Readable
     * stream.
     *
     * @class gpf.interfaces.IReadableStream
     * @extends gpf.interfaces.Interface
     */
    gpf._defIntrf("IReadableStream", {

        /**
         * Triggers the reading of data.
         * The expected behavior is:
         * - The callback is asynchronous
         * - One of the following callback must be called after a read
         *   - EVENT_ERROR: an error occurred.
         *     the stream can't be used after this.
         *   - EVENT_END_OF_STREAM: stream ended.
         *     the stream can't be used after this.
         *   - EVENT_DATA: a buffer is provided, it can't be empty.
         *
         * @param {Number} [size=0] size Number of bytes to read. Read
         * as much as possible if 0
         * @param {gpf.events.Handler} eventsHandler
         *
         * @event data Some data is ready to be ready
         * @eventParam {gpf.IReadOnlyArray} buffer Bytes buffer
         *
         * @event eos No more data can be read from the stream
         *
         */
        read: function (size, eventsHandler) {
            gpf.interfaces.ignoreParameter(size);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        static: {
            EVENT_ERROR: "error",
            EVENT_DATA: "data",
            EVENT_END_OF_STREAM: "eos",

            EXCEPTION_READ_IN_PROGRESS: {
                message: "Read in progress"
            }
        }

    });

    /**
     * The Writable stream interface is an abstraction for a destination that
     * you are writing data to.
     * The expected behavior is:
     * - The callback is asynchronous
     * - One of the following callback must be called after a read
     *   - EVENT_ERROR: an error occurred.
     *     the stream can't be used after this.
     *   - EVENT_READY: the write operation succeeded, the provided buffer has
     *     been fully written (otherwise an error is thrown)
     *
     * @class gpf.interfaces.IReadableStream
     * @extends gpf.interfaces.Interface
     */
    gpf._defIntrf("IWritableStream", {

        /**
         * Triggers the writing of data
         *
         * @param {IReadOnlyArray} int8buffer Buffer to write
         * @param {gpf.events.Handler} eventsHandler
         *
         * @event ready it is appropriate to begin writing more data to the
         * stream
         *
         */
        write: function (int8buffer, eventsHandler) {
            gpf.interfaces.ignoreParameter(int8buffer);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        static: {
            EVENT_ERROR: "error",
            EVENT_READY: "ready",

            EXCEPTION_WRITE_IN_PROGRESS: {
                message: "Read in progress"
            }
        }

    });

    /**
     * The stream combines both IReadableStream and IWritableStream
     */
    gpf._defIntrf("IStream", {

        /**
         * @inheritDoc gpf.interfaces.IReadableStream:read
         */
        read: function (size, eventsHandler) {
            gpf.interfaces.ignoreParameter(size);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        /**
         * @inheritDoc gpf.interfaces.IWritableStream:write
         */
        write: function (int8buffer, eventsHandler) {
            gpf.interfaces.ignoreParameter(int8buffer);
            gpf.interfaces.ignoreParameter(eventsHandler);
        }

    });

    /**
     * Text stream: instead of an int8 buffer, the interface handles strings
     *
     * @class gpf.interfaces.ITextStream
     * @extends gpf.interfaces.IStream
     *
     * @event data Some data is ready to be ready
     * @eventParam {String} buffer
     */
    gpf._defIntrf("ITextStream", gpfI.IStream, {
    });

    /**
     * Internal helper to implement the expected write behavior in all streams
     * @inheritDoc gpf.interfaces.ITextStream:write
     */
    gpfI.ITextStream._write = function () {
        var argIdx, arg;
        for (argIdx = 0; argIdx < arguments.length; ++argIdx) {
            arg = arguments[argIdx];
            if (null !== arg && "string" !== typeof arg) {
                arg = arg.toString();
            }
            this.write_(arg);
        }
        if (0 === argIdx) { // No parameter at all
            this.write_(null);
        }
    };

    //region Stream helpers

    gpf.stream = {};

    /**
     * Handles a buffered stream that depends on a read stream.
     * The way the underlying buffer is read and converted can be overridden
     * through the following protected APIs:
     * - _readSize
     * - _addToBuffer
     *
     * @class gpf.stream.BufferedOnRead
     * @abstract
     * @implements gpf.interfaces.IReadableStream
     */
    gpf.define("gpf.stream.BufferedOnRead", {

        "[Class]": [gpf.$InterfaceImplement(gpfI.IReadableStream)],

        //region Configurable part

        protected: {

            /**
             * Output buffer containing content to be read
             *
             * @type {*[]}
             * @protected
             */
            _buffer: [],

            /**
             * Length of the buffer (compared with read size)
             *
             * @type {Number}
             * @protected
             */
            _bufferLength: 0,

            /**
             * Underlying stream default read size
             *
             * @type {Number}
             * @protected
             */
            _readSize: _BUFREADSTREAM_READ_SIZE,

            /**
             * Process underlying stream buffer (this should grow the output
             * buffer)
             *
             * @param {Array} buffer
             * @abstract
             * @protected
             */
            _addToBuffer: function (buffer) {
                gpf.interfaces.ignoreParameter(buffer);
                gpf.Error.Abstract();
            },

            /**
             * Underlying stream reached its end (this may grow the output
             * buffer)
             *
             * @protected
             */
            _endOfInputStream: function () {
            },

            /**
             * Read buffer.
             * This default implementation checks the buffer type to switch
             * between string and byte array.
             *
             * @param {Number} size
             * @returns {String|Array}
             * @protected
             */
            _readFromBuffer: function (size) {
                gpf.ASSERT(0 !== this._buffer.length, "Buffer is not empty");
                if ("string" === this._buffer[0]) {
                    return this._readFromStringBuffer(size);
                } else {
                    return this._readFromByteBuffer(size);
                }
            },

            /**
             * Read string buffer.
             *
             * @param {Number} size
             * @returns {String}
             * @protected
             */
            _readFromStringBuffer: function (size) {
                var
                    result = gpf.stringExtractFromStringArray(this._buffer,
                        size);
                this._bufferLength -= result.length;
                return result;
            },

            /**
             * Read byte buffer.
             *
             * @param {Number} size
             * @returns {Array}
             * @protected
             */
            _readFromByteBuffer: function (size) {
                var
                    buffer = this._buffer,
                    len = buffer.length;
                if (size > len) {
                    size = len;
                }
                this._bufferLength -= size;
                return buffer.splice(0, size);
            }

        },

        //endregion

        //region Implementation

        public: {

            /**
             * @param {gpf.interfaces.IReadableStream} input
             * @constructor
             */
            constructor: function (input) {
                this._iStream = gpfI.query(input, gpfI.IReadableStream, true);
                this._buffer = [];
            },

            //region gpf.interfaces.IReadableStream

            /**
             * @implements gpf.interfaces.IReadableStream:read
             */
            read: function (size, eventsHandler) {
                var
                    iState = this._iState,
                    length = this._bufferLength;
                if (_BUFREADSTREAM_ISTATE_INPROGRESS === iState) {
                    // A read call is already in progress
                    throw gpfI.IReadableStream.EXCEPTION_READ_IN_PROGRESS;

                } else if (size < length
                    || length && _BUFREADSTREAM_ISTATE_EOS === iState) {
                    // Enough chars in the output buffer to do the read
                    // OR there won't be any more chars. Can output something.
                    gpf.events.fire.apply(this, [
                        gpfI.IReadableStream.EVENT_DATA,
                        {
                            buffer: this._readFromBuffer(size)
                        },
                        eventsHandler
                    ]);

                } else if (_BUFREADSTREAM_ISTATE_EOS === iState) {
                    // No more input and output buffer is empty
                    gpf.events.fire.apply(this, [
                        gpfI.IReadableStream.EVENT_END_OF_STREAM,
                        eventsHandler
                    ]);

                } else {
                    // Read input
                    if (_BUFREADSTREAM_ISTATE_INIT === this._iState) {
                        // Very first call, create callback for input reads
                        this._cbRead = new gpf.Callback(this._onRead, this);
                    }
                    this._iState = _BUFREADSTREAM_ISTATE_INPROGRESS;
                    // Backup parameters
                    this._size = size;
                    this._eventsHandler = eventsHandler;
                    this._iStream.read(this._readSize, this._cbRead);
                }
            }

            //endregion
        },

        private: {

            /**
             * Input stream
             * @type {gpf.interfaces.IReadableStream}
             */
            _iStream: null,

            /**
             * Input stream read callback (pointing to this:_onRead)
             * @type {gpf.Callback}
             */
            _cbRead: null,

            /**
             * Input state
             * @type {Number} see _BUFREADSTREAM_ISTATE_xxx
             */
            _iState: _BUFREADSTREAM_ISTATE_INIT,

            /**
             * Pending read call size
             * @type {Number}
             */
            _size: 0,

            /**
             * Pending read call event handlers
             * @type {gpf.events.Handler}
             */
            _eventsHandler: null,

            /**
             * Handles input stream read event
             *
             * @param {gpf.events.Event} event
             * @private
             */
            _onRead: function (event) {
                var
                    type = event.type();
                if (type === gpfI.IReadableStream.EVENT_END_OF_STREAM) {
                    this._iState = _BUFREADSTREAM_ISTATE_EOS;
                    this._endOfInputStream();
                    // Redirect to read with backed parameters
                    return this.read(this._size, this._eventsHandler);

                } else if (type === gpfI.IReadableStream.EVENT_ERROR) {
                    // Forward the event
                    gpf.events.fire.apply(this, [
                        event,
                        this._eventsHandler
                    ]);

                } else {
                    this._iState = _BUFREADSTREAM_ISTATE_WAITING;
                    this._addToBuffer(event.get("buffer"));
                    if (0 < this._bufferLength) {
                        // Redirect to read with backed parameters
                        return this.read(this._size, this._eventsHandler);
                    } else {
                        // Try to read source again
                        this._iStream.read(_BUFREADSTREAM_READ_SIZE,
                            this._cbRead);
                    }
                }
            }

        }

        //endregion
    });

    /**
     *
     * @param {gpf.interfaces.ITextStream} stream
     * @param {gpf.events.Handler} eventsHandler
     * @param {Function} concatMethod
     *
     * @forwardThis
     *
     * @event ready finished reading the stream
     * @eventParam {Array} array
     */
    gpf.stream.readAll = function (stream, concatMethod, eventsHandler) {
        stream = gpf.interfaces.query(stream, gpfI.IReadableStream,  true);
        (new StreamReader(this, eventsHandler, concatMethod)).read(stream);
    };

    function StreamReader(scope, eventsHandler, concatMethod) {
        this._scope = gpf.Callback.resolveScope(scope);
        this._eventsHandler = eventsHandler;
        this._concatMethod = concatMethod;
    }
    gpf.extend(StreamReader.prototype, {

        _scope: null,
        _eventsHandler: null,
        _concatMethod: null,
        _buffer: undefined,

        read: function (stream) {
            stream.read(0, gpf.Callback.bind(this, "callback"));
        },

        callback: function (event) {
            var
                type = event.type(),
                stream = event.scope();
            if (type === gpfI.IReadableStream.EVENT_END_OF_STREAM) {
                gpf.events.fire.apply(this._scope, [
                    gpfI.IReadableStream.EVENT_READY,
                    {
                        buffer: this._concatMethod(this._buffer)
                    },
                    this._eventsHandler
                ]);

            } else if (type === gpfI.IReadableStream.EVENT_ERROR) {
                // Forward the event
                gpf.events.fire.apply(this._scope, [
                    event,
                    this._eventsHandler
                ]);

            } else {
                this._buffer = this._concatMethod(this._buffer,
                    event.get("buffer"));
                this.read(stream);
            }
        }

    });

    //endregion

/*#ifndef(UMD)*/
}()); /* End of privacy scope */
/*#endif*/<commit_msg>Changed ReadAll interface<commit_after>/*#ifndef(UMD)*/
(function () { /* Begin of privacy scope */
    "use strict";
/*#endif*/

    var
        gpfI = gpf.interfaces,
        _BUFREADSTREAM_READ_SIZE        = 256,
        _BUFREADSTREAM_ISTATE_INIT        = 0,
        _BUFREADSTREAM_ISTATE_INPROGRESS  = 1,
        _BUFREADSTREAM_ISTATE_WAITING     = 2,
        _BUFREADSTREAM_ISTATE_EOS         = 3;

    /**
     * The Readable stream interface is the abstraction for a source of data
     * that you are reading from. In other words, data comes out of a Readable
     * stream.
     *
     * @class gpf.interfaces.IReadableStream
     * @extends gpf.interfaces.Interface
     */
    gpf._defIntrf("IReadableStream", {

        /**
         * Triggers the reading of data.
         * The expected behavior is:
         * - The callback is asynchronous
         * - One of the following callback must be called after a read
         *   - EVENT_ERROR: an error occurred.
         *     the stream can't be used after this.
         *   - EVENT_END_OF_STREAM: stream ended.
         *     the stream can't be used after this.
         *   - EVENT_DATA: a buffer is provided, it can't be empty.
         *
         * @param {Number} [size=0] size Number of bytes to read. Read
         * as much as possible if 0
         * @param {gpf.events.Handler} eventsHandler
         *
         * @event data Some data is ready to be used
         * @eventParam {gpf.IReadOnlyArray} buffer Bytes buffer
         *
         * @event eos No more data can be read from the stream
         *
         */
        read: function (size, eventsHandler) {
            gpf.interfaces.ignoreParameter(size);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        static: {
            EVENT_ERROR: "error",
            EVENT_DATA: "data",
            EVENT_END_OF_STREAM: "eos",

            EXCEPTION_READ_IN_PROGRESS: {
                message: "Read in progress"
            }
        }

    });

    /**
     * The Writable stream interface is an abstraction for a destination that
     * you are writing data to.
     * The expected behavior is:
     * - The callback is asynchronous
     * - One of the following callback must be called after a read
     *   - EVENT_ERROR: an error occurred.
     *     the stream can't be used after this.
     *   - EVENT_READY: the write operation succeeded, the provided buffer has
     *     been fully written (otherwise an error is thrown)
     *
     * @class gpf.interfaces.IReadableStream
     * @extends gpf.interfaces.Interface
     */
    gpf._defIntrf("IWritableStream", {

        /**
         * Triggers the writing of data
         *
         * @param {IReadOnlyArray} int8buffer Buffer to write
         * @param {gpf.events.Handler} eventsHandler
         *
         * @event ready it is appropriate to begin writing more data to the
         * stream
         *
         */
        write: function (int8buffer, eventsHandler) {
            gpf.interfaces.ignoreParameter(int8buffer);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        static: {
            EVENT_ERROR: "error",
            EVENT_READY: "ready",

            EXCEPTION_WRITE_IN_PROGRESS: {
                message: "Read in progress"
            }
        }

    });

    /**
     * The stream combines both IReadableStream and IWritableStream
     */
    gpf._defIntrf("IStream", {

        /**
         * @inheritDoc gpf.interfaces.IReadableStream:read
         */
        read: function (size, eventsHandler) {
            gpf.interfaces.ignoreParameter(size);
            gpf.interfaces.ignoreParameter(eventsHandler);
        },

        /**
         * @inheritDoc gpf.interfaces.IWritableStream:write
         */
        write: function (int8buffer, eventsHandler) {
            gpf.interfaces.ignoreParameter(int8buffer);
            gpf.interfaces.ignoreParameter(eventsHandler);
        }

    });

    /**
     * Text stream: instead of an int8 buffer, the interface handles strings
     *
     * @class gpf.interfaces.ITextStream
     * @extends gpf.interfaces.IStream
     *
     * @event data Some data is ready to be ready
     * @eventParam {String} buffer
     */
    gpf._defIntrf("ITextStream", gpfI.IStream, {
    });

    /**
     * Internal helper to implement the expected write behavior in all streams
     * @inheritDoc gpf.interfaces.ITextStream:write
     */
    gpfI.ITextStream._write = function () {
        var argIdx, arg;
        for (argIdx = 0; argIdx < arguments.length; ++argIdx) {
            arg = arguments[argIdx];
            if (null !== arg && "string" !== typeof arg) {
                arg = arg.toString();
            }
            this.write_(arg);
        }
        if (0 === argIdx) { // No parameter at all
            this.write_(null);
        }
    };

    //region Stream helpers

    gpf.stream = {};

    /**
     * Handles a buffered stream that depends on a read stream.
     * The way the underlying buffer is read and converted can be overridden
     * through the following protected APIs:
     * - _readSize
     * - _addToBuffer
     *
     * @class gpf.stream.BufferedOnRead
     * @abstract
     * @implements gpf.interfaces.IReadableStream
     */
    gpf.define("gpf.stream.BufferedOnRead", {

        "[Class]": [gpf.$InterfaceImplement(gpfI.IReadableStream)],

        //region Configurable part

        protected: {

            /**
             * Output buffer containing content to be read
             *
             * @type {*[]}
             * @protected
             */
            _buffer: [],

            /**
             * Length of the buffer (compared with read size)
             *
             * @type {Number}
             * @protected
             */
            _bufferLength: 0,

            /**
             * Underlying stream default read size
             *
             * @type {Number}
             * @protected
             */
            _readSize: _BUFREADSTREAM_READ_SIZE,

            /**
             * Process underlying stream buffer (this should grow the output
             * buffer)
             *
             * @param {Array} buffer
             * @abstract
             * @protected
             */
            _addToBuffer: function (buffer) {
                gpf.interfaces.ignoreParameter(buffer);
                gpf.Error.Abstract();
            },

            /**
             * Underlying stream reached its end (this may grow the output
             * buffer)
             *
             * @protected
             */
            _endOfInputStream: function () {
            },

            /**
             * Read buffer.
             * This default implementation checks the buffer type to switch
             * between string and byte array.
             *
             * @param {Number} size
             * @returns {String|Array}
             * @protected
             */
            _readFromBuffer: function (size) {
                gpf.ASSERT(0 !== this._buffer.length, "Buffer is not empty");
                if ("string" === this._buffer[0]) {
                    return this._readFromStringBuffer(size);
                } else {
                    return this._readFromByteBuffer(size);
                }
            },

            /**
             * Read string buffer.
             *
             * @param {Number} size
             * @returns {String}
             * @protected
             */
            _readFromStringBuffer: function (size) {
                var
                    result = gpf.stringExtractFromStringArray(this._buffer,
                        size);
                this._bufferLength -= result.length;
                return result;
            },

            /**
             * Read byte buffer.
             *
             * @param {Number} size
             * @returns {Array}
             * @protected
             */
            _readFromByteBuffer: function (size) {
                var
                    buffer = this._buffer,
                    len = buffer.length;
                if (size > len) {
                    size = len;
                }
                this._bufferLength -= size;
                return buffer.splice(0, size);
            }

        },

        //endregion

        //region Implementation

        public: {

            /**
             * @param {gpf.interfaces.IReadableStream} input
             * @constructor
             */
            constructor: function (input) {
                this._iStream = gpfI.query(input, gpfI.IReadableStream, true);
                this._buffer = [];
            },

            //region gpf.interfaces.IReadableStream

            /**
             * @implements gpf.interfaces.IReadableStream:read
             */
            read: function (size, eventsHandler) {
                var
                    iState = this._iState,
                    length = this._bufferLength;
                if (_BUFREADSTREAM_ISTATE_INPROGRESS === iState) {
                    // A read call is already in progress
                    throw gpfI.IReadableStream.EXCEPTION_READ_IN_PROGRESS;

                } else if (size < length
                    || length && _BUFREADSTREAM_ISTATE_EOS === iState) {
                    // Enough chars in the output buffer to do the read
                    // OR there won't be any more chars. Can output something.
                    gpf.events.fire.apply(this, [
                        gpfI.IReadableStream.EVENT_DATA,
                        {
                            buffer: this._readFromBuffer(size)
                        },
                        eventsHandler
                    ]);

                } else if (_BUFREADSTREAM_ISTATE_EOS === iState) {
                    // No more input and output buffer is empty
                    gpf.events.fire.apply(this, [
                        gpfI.IReadableStream.EVENT_END_OF_STREAM,
                        eventsHandler
                    ]);

                } else {
                    // Read input
                    if (_BUFREADSTREAM_ISTATE_INIT === this._iState) {
                        // Very first call, create callback for input reads
                        this._cbRead = new gpf.Callback(this._onRead, this);
                    }
                    this._iState = _BUFREADSTREAM_ISTATE_INPROGRESS;
                    // Backup parameters
                    this._size = size;
                    this._eventsHandler = eventsHandler;
                    this._iStream.read(this._readSize, this._cbRead);
                }
            }

            //endregion
        },

        private: {

            /**
             * Input stream
             * @type {gpf.interfaces.IReadableStream}
             */
            _iStream: null,

            /**
             * Input stream read callback (pointing to this:_onRead)
             * @type {gpf.Callback}
             */
            _cbRead: null,

            /**
             * Input state
             * @type {Number} see _BUFREADSTREAM_ISTATE_xxx
             */
            _iState: _BUFREADSTREAM_ISTATE_INIT,

            /**
             * Pending read call size
             * @type {Number}
             */
            _size: 0,

            /**
             * Pending read call event handlers
             * @type {gpf.events.Handler}
             */
            _eventsHandler: null,

            /**
             * Handles input stream read event
             *
             * @param {gpf.events.Event} event
             * @private
             */
            _onRead: function (event) {
                var
                    type = event.type();
                if (type === gpfI.IReadableStream.EVENT_END_OF_STREAM) {
                    this._iState = _BUFREADSTREAM_ISTATE_EOS;
                    this._endOfInputStream();
                    // Redirect to read with backed parameters
                    return this.read(this._size, this._eventsHandler);

                } else if (type === gpfI.IReadableStream.EVENT_ERROR) {
                    // Forward the event
                    gpf.events.fire.apply(this, [
                        event,
                        this._eventsHandler
                    ]);

                } else {
                    this._iState = _BUFREADSTREAM_ISTATE_WAITING;
                    this._addToBuffer(event.get("buffer"));
                    if (0 < this._bufferLength) {
                        // Redirect to read with backed parameters
                        return this.read(this._size, this._eventsHandler);
                    } else {
                        // Try to read source again
                        this._iStream.read(_BUFREADSTREAM_READ_SIZE,
                            this._cbRead);
                    }
                }
            }

        }

        //endregion
    });

    /**
     *
     * @param {gpf.interfaces.ITextStream} stream
     * @param {gpf.events.Handler} eventsHandler
     * @param {Function} concatMethod
     *
     * @forwardThis
     *
     * @event data finished reading the stream, the buffer is provided
     * @eventParam {Array|String} buffer
     */
    gpf.stream.readAll = function (stream, concatMethod, eventsHandler) {
        stream = gpf.interfaces.query(stream, gpfI.IReadableStream,  true);
        (new StreamReader(this, eventsHandler, concatMethod)).read(stream);
    };

    function StreamReader(scope, eventsHandler, concatMethod) {
        this._scope = gpf.Callback.resolveScope(scope);
        this._eventsHandler = eventsHandler;
        this._concatMethod = concatMethod;
    }
    gpf.extend(StreamReader.prototype, {

        _scope: null,
        _eventsHandler: null,
        _concatMethod: null,
        _buffer: undefined,

        read: function (stream) {
            stream.read(0, gpf.Callback.bind(this, "callback"));
        },

        callback: function (event) {
            var
                type = event.type(),
                stream = event.scope();
            if (type === gpfI.IReadableStream.EVENT_END_OF_STREAM) {
                gpf.events.fire.apply(this._scope, [
                    gpfI.IReadableStream.EVENT_DATA,
                    {
                        buffer: this._concatMethod(this._buffer)
                    },
                    this._eventsHandler
                ]);

            } else if (type === gpfI.IReadableStream.EVENT_ERROR) {
                // Forward the event
                gpf.events.fire.apply(this._scope, [
                    event,
                    this._eventsHandler
                ]);

            } else {
                this._buffer = this._concatMethod(this._buffer,
                    event.get("buffer"));
                this.read(stream);
            }
        }

    });

    //endregion

/*#ifndef(UMD)*/
}()); /* End of privacy scope */
/*#endif*/<|endoftext|>


<commit_before>import {requestType, makeTypeID} from '../requestTypeIdentifier';
import _ from 'lodash';
import {log} from '../utils';

const filePath = __dirname + '/../../doc/work-context.jsonld';
const typeId = makeTypeID(filePath);

/**
 *
 * @param request
 * @returns {*}
 */
function getPids(request) {
  if (!_.has(request, 'pids')) {
    throw new Error('Pid not correctly present in request');
  }
  if (request.pids.length < 1) {
    throw new Error(
      'Illegal number of pids in request - at least one should be present.'
    );
  }
  return request.pids;
}

/**
 *
 * @param context
 * @returns {{agency: *, profile: *}}
 */
function getAndValidateOpensearchContext(context) {
  const searchAgency = context.get('search.agency', true);
  if (!searchAgency) {
    throw new Error('No search agency property present in context');
  }
  const searchProfile = context.get('search.profile', true);
  if (!searchProfile) {
    throw new Error('No search profile property present in context');
  }
  return {agency: searchAgency, profile: searchProfile};
}

const dataObjectRequestTypes = {
  DKABM: 'dkabm',
  BRIEFDISPLAY: 'briefDisplay',
  RELATIONS: 'relations',
  FULLTEXTREVIEWS: 'docbook'
};

/**
 *
 * @param defaultBehaviour
 * @param fields
 * @returns {Array}
 */
function getObjectFormats(defaultBehaviour, fields) {
  const objectFormat = [];

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.BRIEFDISPLAY);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('briefDisplay');
  }

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.DKABM);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('dkabm');
  }

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.DOCBOOK);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('docbook');
  }

  return objectFormat;
}

/**
 *
 * @param defaultBehaviour
 * @param fields
 * @returns {*}
 */
function getRequestRelationData(defaultBehaviour, fields) {
  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.RELATIONS);
    })
  ) {
    // eslint-disable-line brace-style
    return 'uri';
  }

  return null;
}

/**
 *
 * @param request
 * @param context
 * @returns {{action: string, identifier: *, agency: *, profile: *, outputType: string, objectFormat: Array}}
 */
export function requestTransform(request, context) {
  // eslint-disable-line no-unused-vars

  const pids = getPids(request);
  const osContext = getAndValidateOpensearchContext(context);

  // If no fields were given, default behaviour is to get
  // everything from briefDisplay, dkabm and relations.
  const defaultBehaviour = _.has(request, 'fields') ? false : true;
  const fields = request.fields;

  // Create request params.
  // Only add dkabm, briefDisplay and relations if requested.
  const requestParams = {
    action: 'getObject',
    identifier: pids,
    agency: osContext.agency,
    profile: osContext.profile,
    outputType: 'json',
    objectFormat: getObjectFormats(defaultBehaviour, fields)
  };

  const rels = getRequestRelationData(defaultBehaviour, fields);
  if (rels) {
    requestParams.relationData = rels;
  }

  return requestParams;
}

/**
 *
 * @param result
 * @returns {Function}
 */
function retrieveDkabmFields(result) {
  return function(value, key) {
    const a = [];
    _.forEach(value, function(z) {
      // eslint-disable-line
      const x = {key: key};
      if (_.has(z, '$') && _.has(z, '@')) {
        x.ns = z['@'];
        x.value = z.$;
        if (_.has(z, '@type')) {
          x.type = z['@type'].$;
        }
      }
      if (x.value) {
        a.push(x);
      }
    });

    a.map(X => {
      const identifier = X.ns + ':' + key;
      const field = X.type
        ? typeId.getField(identifier, X.type)
        : typeId.getField(identifier);
      if (result[field]) {
        result[field].push(X.value);
      } else {
        // eslint-disable-line brace-style
        result[field] = [X.value];
      }
    });
  };
}

/**
 * If there is no dkabm-record, and empty object will be returned.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetDkabmRecord(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    return {};
  }
  const obj = searchResult.collection.object;
  if (obj.length < 1) {
    return {};
  }
  if (!_.has(obj[0], 'record')) {
    return {};
  }
  const record = obj[0].record; // DKABM-data
  return record;
}

/**
 *
 * @param searchResult
 */
function getDkabmData(searchResult) {
  const record = validateAndGetDkabmRecord(searchResult);

  const result = {};
  _.forOwn(record, retrieveDkabmFields(result));
  return result;
}

/**
 *
 * @param searchResult
 * @returns {{}}
 */
function validateAndGetBriefDisplay(searchResult) {
  if (!_.has(searchResult, 'formattedCollection.briefDisplay.manifestation')) {
    return {};
  }
  const manifestations =
    searchResult.formattedCollection.briefDisplay.manifestation;
  return manifestations.length && manifestations.length > 0
    ? manifestations[0]
    : {};
}

/**
 *
 * @param searchResult
 */
function getBriefDisplayData(searchResult) {
  const briefDisplay = validateAndGetBriefDisplay(searchResult);

  const res = {};
  _.forOwn(briefDisplay, (value, key) => {
    const ns = 'bd';
    const identifier = ns + ':' + key;
    const field = typeId.getField(identifier);
    if (!res[field]) {
      res[field] = [];
    }
    res[field].push(value.$);
  });
  return res;
}

/**
 * If there is no search result or it is empty, an empty array will be returned.
 * @param response
 * @returns {Array}
 */
function validateAndGetSearchResult(response) {
  if (!_.has(response, 'data.searchResponse.result.searchResult')) {
    return [];
  }
  const searchResult = response.data.searchResponse.result.searchResult;
  return searchResult.length && searchResult.length > 0 ? searchResult : [];
}

/**
 * Returns an empty array if no relations or if some property is missing.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetRelations(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    // no object return empty list:
    return [];
  }
  const obj = searchResult.collection.object;
  if (!obj.length || obj.length === 0) {
    return [];
  }
  const obj0 = obj[0];
  if (!_.has(obj0, 'relations.relation')) {
    return [];
  }
  return obj0.relations.relation;
}

/**
 *
 * @param searchResult
 */
function getRelationData(searchResult) {
  const relations = validateAndGetRelations(searchResult);

  const res = {};
  _.forEach(relations, relation => {
    if (!_.has(relation, 'relationType.$')) {
      return;
    }
    const field = typeId.getField(relation.relationType.$);
    if (!res[field]) {
      res[field] = [];
    }
    res[field].push(relation.relationUri.$);
  });
  return res;
}

/**
 * Returns an empty array if no full text reviews or if some property is missing.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetFullTextReviews(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    // no object return empty list:
    return [];
  }
  const obj = searchResult.collection.object;
  if (!obj.length || obj.length === 0) {
    return [];
  }
  return obj;
}

/**
 * Returns a single item of collection.object
 * @param item
 * @returns {Array}
 */
function validateAndGetSingleReview(item) {
  if (
    !_.has(item, 'article.title.$') ||
    item.article.title.$ !== 'Lektørudtalelse'
  ) {
    return [];
  }
  if (!_.has(item, 'article.section') || item.article.section.length === 0) {
    return [];
  }
  return item.article.section;
}

/**
 * Extract data for full text reviews
 * @param searchResult
 * @returns {Array}
 */
function getFullTextReviewsData(searchResult) {
  const object = validateAndGetFullTextReviews(searchResult);
  const reviews = [];
  _.forEach(object, item => {
    // For each lektør
    const singleReview = [];
    const sections = validateAndGetSingleReview(item);
    _.forEach(sections, section => {
      // For each section for one lektør
      let review = {};
      _.forEach(section, (info, key) => {
        // For each title, para etc...
        if (_.has(info, '$')) {
          review[key] = info.$;
        }
      });
      if (review.length !== 0) {
        singleReview.push(review);
      }
    });
    if (singleReview.length !== 0) {
      reviews.push(singleReview);
    }
  });
  return reviews.length === 0 ? {}: {fullTextReviews: reviews};
}

/**
 * Transform the response
 * @param response
 * @param context
 * @param params
 * @returns {*}
 */
export function responseTransform(response, context, params) {
  // eslint-disable-line no-unused-vars
  if (_.has(response, 'data.searchResponse.error.$')) {
    const errMsg = 'Error in opensearchGetObject response.';
    log.error(errMsg);
    return {statusCode: 500, error: errMsg};
  }

  const searchResults = validateAndGetSearchResult(response);
  if (searchResults.length === 0) {
    return {};
  }

  const dataObjectsRequested = params.objectFormat;
  if (params.relationData) {
    dataObjectsRequested.push(dataObjectRequestTypes.RELATIONS);
  }

  const data = searchResults.map(searchResult => {
    const dkabmData = dataObjectsRequested.includes(
      dataObjectRequestTypes.DKABM
    )
      ? getDkabmData(searchResult)
      : {};
    const briefDisplayData = dataObjectsRequested.includes(
      dataObjectRequestTypes.BRIEFDISPLAY
    )
      ? getBriefDisplayData(searchResult)
      : {};
    const relationData = dataObjectsRequested.includes(
      dataObjectRequestTypes.RELATIONS
    )
      ? getRelationData(searchResult)
      : {};
    const fullTextReviewsData = dataObjectsRequested.includes(
      dataObjectRequestTypes.FULLTEXTREVIEWS
    )
      ? getFullTextReviewsData(searchResult)
      : {};
    const result = {};
    _.extend(
      result,
      dkabmData,
      briefDisplayData,
      relationData,
      fullTextReviewsData
    );
    return result;
  });
  return {statusCode: 200, data: data};
}

/**
 *
 * @param request
 * @param context
 * @returns {Request|Promise|*|PromiseLike<({statusCode, error}|{}|{statusCode, data}) | never>|Promise<({statusCode, error}|{}|{statusCode, data}) | never>}
 */
export default (request, context) => {
  const params = requestTransform(request, context);
  return context
    .call('opensearch', params)
    .then(body => responseTransform(body, context, params));
};
<commit_msg>Prettier<commit_after>import {requestType, makeTypeID} from '../requestTypeIdentifier';
import _ from 'lodash';
import {log} from '../utils';

const filePath = __dirname + '/../../doc/work-context.jsonld';
const typeId = makeTypeID(filePath);

/**
 *
 * @param request
 * @returns {*}
 */
function getPids(request) {
  if (!_.has(request, 'pids')) {
    throw new Error('Pid not correctly present in request');
  }
  if (request.pids.length < 1) {
    throw new Error(
      'Illegal number of pids in request - at least one should be present.'
    );
  }
  return request.pids;
}

/**
 *
 * @param context
 * @returns {{agency: *, profile: *}}
 */
function getAndValidateOpensearchContext(context) {
  const searchAgency = context.get('search.agency', true);
  if (!searchAgency) {
    throw new Error('No search agency property present in context');
  }
  const searchProfile = context.get('search.profile', true);
  if (!searchProfile) {
    throw new Error('No search profile property present in context');
  }
  return {agency: searchAgency, profile: searchProfile};
}

const dataObjectRequestTypes = {
  DKABM: 'dkabm',
  BRIEFDISPLAY: 'briefDisplay',
  RELATIONS: 'relations',
  FULLTEXTREVIEWS: 'docbook'
};

/**
 *
 * @param defaultBehaviour
 * @param fields
 * @returns {Array}
 */
function getObjectFormats(defaultBehaviour, fields) {
  const objectFormat = [];

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.BRIEFDISPLAY);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('briefDisplay');
  }

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.DKABM);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('dkabm');
  }

  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.DOCBOOK);
    })
  ) {
    // eslint-disable-line brace-style
    objectFormat.push('docbook');
  }

  return objectFormat;
}

/**
 *
 * @param defaultBehaviour
 * @param fields
 * @returns {*}
 */
function getRequestRelationData(defaultBehaviour, fields) {
  if (
    defaultBehaviour ||
    fields.some(field => {
      return typeId.isType(field, requestType.RELATIONS);
    })
  ) {
    // eslint-disable-line brace-style
    return 'uri';
  }

  return null;
}

/**
 *
 * @param request
 * @param context
 * @returns {{action: string, identifier: *, agency: *, profile: *, outputType: string, objectFormat: Array}}
 */
export function requestTransform(request, context) {
  // eslint-disable-line no-unused-vars

  const pids = getPids(request);
  const osContext = getAndValidateOpensearchContext(context);

  // If no fields were given, default behaviour is to get
  // everything from briefDisplay, dkabm and relations.
  const defaultBehaviour = _.has(request, 'fields') ? false : true;
  const fields = request.fields;

  // Create request params.
  // Only add dkabm, briefDisplay and relations if requested.
  const requestParams = {
    action: 'getObject',
    identifier: pids,
    agency: osContext.agency,
    profile: osContext.profile,
    outputType: 'json',
    objectFormat: getObjectFormats(defaultBehaviour, fields)
  };

  const rels = getRequestRelationData(defaultBehaviour, fields);
  if (rels) {
    requestParams.relationData = rels;
  }

  return requestParams;
}

/**
 *
 * @param result
 * @returns {Function}
 */
function retrieveDkabmFields(result) {
  return function(value, key) {
    const a = [];
    _.forEach(value, function(z) {
      // eslint-disable-line
      const x = {key: key};
      if (_.has(z, '$') && _.has(z, '@')) {
        x.ns = z['@'];
        x.value = z.$;
        if (_.has(z, '@type')) {
          x.type = z['@type'].$;
        }
      }
      if (x.value) {
        a.push(x);
      }
    });

    a.map(X => {
      const identifier = X.ns + ':' + key;
      const field = X.type
        ? typeId.getField(identifier, X.type)
        : typeId.getField(identifier);
      if (result[field]) {
        result[field].push(X.value);
      } else {
        // eslint-disable-line brace-style
        result[field] = [X.value];
      }
    });
  };
}

/**
 * If there is no dkabm-record, and empty object will be returned.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetDkabmRecord(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    return {};
  }
  const obj = searchResult.collection.object;
  if (obj.length < 1) {
    return {};
  }
  if (!_.has(obj[0], 'record')) {
    return {};
  }
  const record = obj[0].record; // DKABM-data
  return record;
}

/**
 *
 * @param searchResult
 */
function getDkabmData(searchResult) {
  const record = validateAndGetDkabmRecord(searchResult);

  const result = {};
  _.forOwn(record, retrieveDkabmFields(result));
  return result;
}

/**
 *
 * @param searchResult
 * @returns {{}}
 */
function validateAndGetBriefDisplay(searchResult) {
  if (!_.has(searchResult, 'formattedCollection.briefDisplay.manifestation')) {
    return {};
  }
  const manifestations =
    searchResult.formattedCollection.briefDisplay.manifestation;
  return manifestations.length && manifestations.length > 0
    ? manifestations[0]
    : {};
}

/**
 *
 * @param searchResult
 */
function getBriefDisplayData(searchResult) {
  const briefDisplay = validateAndGetBriefDisplay(searchResult);

  const res = {};
  _.forOwn(briefDisplay, (value, key) => {
    const ns = 'bd';
    const identifier = ns + ':' + key;
    const field = typeId.getField(identifier);
    if (!res[field]) {
      res[field] = [];
    }
    res[field].push(value.$);
  });
  return res;
}

/**
 * If there is no search result or it is empty, an empty array will be returned.
 * @param response
 * @returns {Array}
 */
function validateAndGetSearchResult(response) {
  if (!_.has(response, 'data.searchResponse.result.searchResult')) {
    return [];
  }
  const searchResult = response.data.searchResponse.result.searchResult;
  return searchResult.length && searchResult.length > 0 ? searchResult : [];
}

/**
 * Returns an empty array if no relations or if some property is missing.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetRelations(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    // no object return empty list:
    return [];
  }
  const obj = searchResult.collection.object;
  if (!obj.length || obj.length === 0) {
    return [];
  }
  const obj0 = obj[0];
  if (!_.has(obj0, 'relations.relation')) {
    return [];
  }
  return obj0.relations.relation;
}

/**
 *
 * @param searchResult
 */
function getRelationData(searchResult) {
  const relations = validateAndGetRelations(searchResult);

  const res = {};
  _.forEach(relations, relation => {
    if (!_.has(relation, 'relationType.$')) {
      return;
    }
    const field = typeId.getField(relation.relationType.$);
    if (!res[field]) {
      res[field] = [];
    }
    res[field].push(relation.relationUri.$);
  });
  return res;
}

/**
 * Returns an empty array if no full text reviews or if some property is missing.
 * @param searchResult
 * @returns {*}
 */
function validateAndGetFullTextReviews(searchResult) {
  if (!_.has(searchResult, 'collection.object')) {
    // no object return empty list:
    return [];
  }
  const obj = searchResult.collection.object;
  if (!obj.length || obj.length === 0) {
    return [];
  }
  return obj;
}

/**
 * Returns a single item of collection.object
 * @param item
 * @returns {Array}
 */
function validateAndGetSingleReview(item) {
  if (
    !_.has(item, 'article.title.$') ||
    item.article.title.$ !== 'Lektørudtalelse'
  ) {
    return [];
  }
  if (!_.has(item, 'article.section') || item.article.section.length === 0) {
    return [];
  }
  return item.article.section;
}

/**
 * Extract data for full text reviews
 * @param searchResult
 * @returns {Array}
 */
function getFullTextReviewsData(searchResult) {
  const object = validateAndGetFullTextReviews(searchResult);
  const reviews = [];
  _.forEach(object, item => {
    // For each lektør
    const singleReview = [];
    const sections = validateAndGetSingleReview(item);
    _.forEach(sections, section => {
      // For each section for one lektør
      let review = {};
      _.forEach(section, (info, key) => {
        // For each title, para etc...
        if (_.has(info, '$')) {
          review[key] = info.$;
        }
      });
      if (review.length !== 0) {
        singleReview.push(review);
      }
    });
    if (singleReview.length !== 0) {
      reviews.push(singleReview);
    }
  });
  return reviews.length === 0 ? {} : {fullTextReviews: reviews};
}

/**
 * Transform the response
 * @param response
 * @param context
 * @param params
 * @returns {*}
 */
export function responseTransform(response, context, params) {
  // eslint-disable-line no-unused-vars
  if (_.has(response, 'data.searchResponse.error.$')) {
    const errMsg = 'Error in opensearchGetObject response.';
    log.error(errMsg);
    return {statusCode: 500, error: errMsg};
  }

  const searchResults = validateAndGetSearchResult(response);
  if (searchResults.length === 0) {
    return {};
  }

  const dataObjectsRequested = params.objectFormat;
  if (params.relationData) {
    dataObjectsRequested.push(dataObjectRequestTypes.RELATIONS);
  }

  const data = searchResults.map(searchResult => {
    const dkabmData = dataObjectsRequested.includes(
      dataObjectRequestTypes.DKABM
    )
      ? getDkabmData(searchResult)
      : {};
    const briefDisplayData = dataObjectsRequested.includes(
      dataObjectRequestTypes.BRIEFDISPLAY
    )
      ? getBriefDisplayData(searchResult)
      : {};
    const relationData = dataObjectsRequested.includes(
      dataObjectRequestTypes.RELATIONS
    )
      ? getRelationData(searchResult)
      : {};
    const fullTextReviewsData = dataObjectsRequested.includes(
      dataObjectRequestTypes.FULLTEXTREVIEWS
    )
      ? getFullTextReviewsData(searchResult)
      : {};
    const result = {};
    _.extend(
      result,
      dkabmData,
      briefDisplayData,
      relationData,
      fullTextReviewsData
    );
    return result;
  });
  return {statusCode: 200, data: data};
}

/**
 *
 * @param request
 * @param context
 * @returns {Request|Promise|*|PromiseLike<({statusCode, error}|{}|{statusCode, data}) | never>|Promise<({statusCode, error}|{}|{statusCode, data}) | never>}
 */
export default (request, context) => {
  const params = requestTransform(request, context);
  return context
    .call('opensearch', params)
    .then(body => responseTransform(body, context, params));
};
<|endoftext|>


<commit_before>import Transport from './Transport';

export default class ForeverFrameTransport extends Transport {
  static supportsKeepAlive = true;

  constructor(client, treaty) {
    super('foreverFrame', client, treaty);
  }
}
<commit_msg>Began implementation of foreverframe. Tests Pass.<commit_after>import Transport from './Transport';

export default class ForeverFrameTransport extends Transport {
  static supportsKeepAlive = true;

  constructor(client, treaty, url) {
    super('foreverFrame', client, treaty);
    this._url = url;
    this._frameCount= 0;
    this._connections = {};
  }
  start(){
    return new Promise((resolve, reject) => {
      if(this._frame) {
        return reject(new Error('An iFrame has already been initialized. Call `stop()` before attempting to `start()` again.'));
      }
      this._frameId = this._frameCount + 1;
      this._frame = this.createFrame();
      this._frame.setAttribute('data-signalr-connection-id', this.id);

      this._url += '&frameId=' + this._frameId;
      document.documentElement.appendChild(this._frame);
      this._logger.info('Binding to iframe\'s event.');

      this._frame.src = this._url;
    });
  }
  stop(){

  }
  _reconnect(){

  }
  createFrame(){
    let frame = document.createElement('iframe');
    frame.setAttribute('style', 'position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;');
    return frame;
  }

}
<|endoftext|>


<commit_before>import {DataTable} from "simple-datatables"
import deepEqual from "fast-deep-equal"

import * as plugins from "../../plugins/books_overview"
import {BookActions} from "./actions"
import {BookAccessRightsDialog} from "./accessrights"
import {ImageDB} from "../images/database"
import {OverviewMenuView, escapeText, findTarget, whenReady, postJson, activateWait, deactivateWait, addAlert, baseBodyTemplate, ensureCSS, setDocTitle, DatatableBulk, localizeDate, shortFileTitle} from "../common"
import {SiteMenu} from "../menu"
import {menuModel, bulkMenuModel} from "./menu"
import {FeedbackTab} from "../feedback"
import {
    docSchema
} from "../schema/document"

export class BookOverview {
    // A class that contains everything that happens on the books page.
    // It is currently not possible to initialize more than one such class,
    // as it contains bindings to menu items, etc. that are uniquely defined.
    constructor({app, user}, path = '/') {
        console.log({path})
        this.app = app
        this.user = user
        this.path = path
        this.schema = docSchema
        this.mod = {}
        this.bookList = []
        this.styles = false
        this.documentList = []
        this.teamMembers = []
        this.accessRights = []
        this.citationStyles = []
        this.subdirs = {}
        this.lastSort = {column: 0, dir: 'asc'}
    }

    init() {
        return whenReady().then(() => {
            this.render()
            const smenu = new SiteMenu(this.app, "books")
            smenu.init()
            new BookActions(this)
            this.menu = new OverviewMenuView(this, menuModel)
            this.menu.init()
            this.dtBulkModel = bulkMenuModel()
            this.activateFidusPlugins()
            this.bind()
            return this.getBookListData().then(
                () => this.app.csl.getStyles().then(
                    styles => {
                        this.citationStyles = styles
                        return deactivateWait()
                        console.log('ALL')
                    }
                )
            )
        })
    }

    showCached() {
        return this.loaddatafromIndexedDB().then(json => {
            console.log({m: 'showCached', json})
            if (!json) {
                activateWait(true)
                return
            }
            return this.initializeView(json)
        })
    }

    activateFidusPlugins() {
        // Add plugins.
        this.plugins = {}

        Object.keys(plugins).forEach(plugin => {
            if (typeof plugins[plugin] === 'function') {
                this.plugins[plugin] = new plugins[plugin](this)
                this.plugins[plugin].init()
            }
        })
    }

    render() {
        ensureCSS([
            'add_remove_dialog.css',
            'access_rights_dialog.css',
            'book.css'
        ])
        this.dom = document.createElement('body')
        this.dom.innerHTML = baseBodyTemplate({
            contents: '',
            user: this.user,
            hasOverview: true
        })
        document.body = this.dom

        setDocTitle(gettext('Book Overview'), this.app)
        const feedbackTab = new FeedbackTab()
        feedbackTab.init()
    }

    getImageDB() {
        if (!this.imageDB) {
            const imageGetter = new ImageDB()
            return new Promise(resolve => {
                imageGetter.getDB().then(
                    () => {
                        this.imageDB = imageGetter
                        resolve()
                    }
                )
            })
        } else {
            return Promise.resolve()
        }
    }

    onResize() {
        if (!this.table) {
            return
        }
        this.initTable()
    }

    /* Initialize the overview table */
    initTable() {
        if (this.table) {
            this.table.destroy()
            this.table = false
        }
        this.subdirs = {}
        const tableEl = document.createElement('table')
        tableEl.classList.add('fw-data-table')
        tableEl.classList.add('fw-large')
        this.dom.querySelector('.fw-contents').innerHTML = ''
        this.dom.querySelector('.fw-contents').appendChild(tableEl)

        this.dtBulk = new DatatableBulk(this, this.dtBulkModel)

        const hiddenCols = [0, 1]

        if (window.innerWidth < 500) {
            hiddenCols.push(2)
            if (window.innerWidth < 400) {
                hiddenCols.push(4)
            }
        }

        const fileList = this.bookList.map(
            book => this.createTableRow(book)
        ).filter(row => !!row)

        if (this.path !== '/') {
            fileList.unshift([
                '-1',
                'top',
                '',
                `

                    ..
                `,
                '',
                '',
                '',
                '',
                ''
            ])
        }

        this.table = new DataTable(tableEl, {
            searchable: true,
            paging: false,
            scrollY: `${Math.max(window.innerHeight - 360, 100)}px`,
            labels: {
                noRows: gettext("No books available") // Message shown when there are no search results
            },
            layout: {
                top: ""
            },
            data: {
                headings: ['', '', this.dtBulk.getHTML(), gettext("Title"), gettext("Created"), gettext("Last changed"), gettext("Owner"), gettext("Rights"), ''],
                data: fileList
            },
            columns: [
                {
                    select: hiddenCols,
                    hidden: true
                },
                {
                    select: [2, 7, 8],
                    sortable: false
                },
                {
                    select: [this.lastSort.column],
                    sort: this.lastSort.dir
                }
            ]
        })

        this.table.on('datatable.sort', (column, dir) => {
            this.lastSort = {column, dir}
        })

        this.dtBulk.init(this.table.table)
    }

    createTableRow(book) {
        let path = book.path
        if (!path.startsWith('/')) {
            path = '/' + path
        }
        if (!path.startsWith(this.path)) {
            return false
        }
        if (path.endsWith('/')) {
            path += book.title
        }

        const currentPath = path.slice(this.path.length)
        if (currentPath.includes('/')) {
            // There is a subdir
            const subdir = currentPath.split('/').shift()
            if (this.subdirs[subdir]) {
                // subdir has been covered already
                // We only update the update/added columns if needed.
                if (book.added < this.subdirs[subdir].added) {
                    this.subdirs[subdir].added = book.added
                    this.subdirs[subdir].row[5] = `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`
                }
                if (doc.updated > this.subdirs[subdir].updated) {
                    this.subdirs[subdir].updated = book.updated
                    this.subdirs[subdir].row[6] = `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`
                }
                return false
            }
            // Display subdir
            const row = [
                '0',
                'folder',
                '',
                `

                    ${escapeText(subdir)}
                `,
                `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`,
                `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`,
                '',
                '',
                ''
            ]
            this.subdirs[subdir] = {row, added: book.added, updated: book.updated}
            return row
        }

        // This is the folder of the file. Return the file.
        return [
            String(book.id),
            'file',
            `<input type="checkbox" class="entry-select fw-check" data-id="${book.id}" id="book-${book.id}"><label for="book-${book.id}"></label>`,
            `

                    ${shortFileTitle(book.title, book.path)}

            `,
            `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`,
            `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`,
            `

            ${escapeText(book.owner_name)}`,
            `

            `,
            `
                ${this.user.id === book.owner ? '' : ''}
           `
        ]
    }

    removeTableRows(ids) {
        const existingRows = this.table.data.map((data, index) => {
            const id = parseInt(data.cells[0].textContent)
            if (ids.includes(id)) {
                return index
            } else {
                return false
            }
        }).filter(rowIndex => rowIndex !== false)

        if (existingRows.length) {
            this.table.rows().remove(existingRows)
        }
    }

    addBookToTable(book) {
        this.table.insert({data: [this.createTableRow(book)]})
        // Redo last sort
        this.table.columns().sort(this.lastSort.column, this.lastSort.dir)
    }

    getBookListData() {
        const cachedPromise = this.showCached()
        if (this.app.isOffline()) {
            return cachedPromise
        }
        return postJson(
            '/api/book/list/'
        ).then(
            ({json}) => {
                console.log({json})
                return cachedPromise.then(
                    () => this.loaddatafromIndexedDB()
                ).then(oldJson => {
                    console.log({oldJson})
                    if (!deepEqual(json, oldJson)) {
                        this.updateIndexedDB(json)
                        this.initializeView(json)
                    }
                })
            }
        ).catch(
            error => {
                if (this.app.isOffline()) {
                    return cachedPromise
                } else {
                    addAlert('error', gettext('Cannot load data of books.'))
                    throw (error)
                }
            }
        ).then(
            () => deactivateWait()
        )
    }

    initializeView(json) {
        this.bookList = this.unpackBooks(json.books)
        this.documentList = json.documents
        this.teamMembers = json.team_members
        this.accessRights = json.access_rights
        this.styles = json.styles

        this.initTable()
    }

    loaddatafromIndexedDB() {
        return this.app.indexedDB.readAllData("books_data").then(
            response => {
                console.log({response})
                if (!response.length) {
                    return false
                }
                const data = response[0]
                delete data.id
                return data
            }
        )
    }

    updateIndexedDB(json) {
        json.id = 1
        // Clear data if any present
        return this.app.indexedDB.clearData("books_data").then(
            () => this.app.indexedDB.insertData("books_data", [json])
        )
    }

    unpackBooks(booksFromServer) {
        // metadata and settings are stored as a json stirng in a text field on
        // the server, so they need to be unpacked before being available.
        return booksFromServer.map(book => {
            const uBook = Object.assign({}, book)
            uBook.metadata = JSON.parse(book.metadata)
            uBook.settings = JSON.parse(book.settings)
            return uBook
        })
    }

    bind() {
        this.dom.addEventListener('click', event => {
            const el = {}
            switch (true) {
            case findTarget(event, '.delete-book', el): {
                const bookId = parseInt(el.target.dataset.id)
                this.mod.actions.deleteBookDialog([bookId])
                break
            }
            case findTarget(event, '.owned-by-user.rights', el): {
                const bookId = parseInt(el.target.dataset.id)
                const accessDialog = new BookAccessRightsDialog(
                    [bookId],
                    this.teamMembers,
                    this.accessRights
                )
                accessDialog.init().then(
                    accessRights => this.accessRights = accessRights
                )
                break
            }
            case findTarget(event, '.book-title', el): {
                const bookId = parseInt(el.target.dataset.id)
                this.getImageDB().then(() => {
                    this.mod.actions.createBookDialog(bookId, this.imageDB)
                })
                break
            }
            case findTarget(event, '.fw-data-table-title .subdir', el):
                this.path += el.target.dataset.subdir + '/'
                window.history.pushState({}, "", '/books' + this.path)
                this.initTable()
                break
            case findTarget(event, '.fw-data-table-title .parentdir', el): {
                const pathParts = this.path.split('/')
                pathParts.pop()
                pathParts.pop()
                this.path = pathParts.join('/') + '/'
                window.history.pushState({}, "", '/books' + this.path)
                this.initTable()
                break
            }
            case findTarget(event, 'a', el):
                if (el.target.hostname === window.location.hostname && el.target.getAttribute('href')[0] === '/') {
                    event.preventDefault()
                    this.app.goTo(el.target.href)
                }
                break
            default:
                break
            }
        })
    }

    getSelected() {
        return Array.from(
            this.dom.querySelectorAll('.entry-select:checked:not(:disabled)')
        ).map(el => parseInt(el.getAttribute('data-id')))
    }
}
<commit_msg>remove console.log<commit_after>import {DataTable} from "simple-datatables"
import deepEqual from "fast-deep-equal"

import * as plugins from "../../plugins/books_overview"
import {BookActions} from "./actions"
import {BookAccessRightsDialog} from "./accessrights"
import {ImageDB} from "../images/database"
import {OverviewMenuView, escapeText, findTarget, whenReady, postJson, activateWait, deactivateWait, addAlert, baseBodyTemplate, ensureCSS, setDocTitle, DatatableBulk, localizeDate, shortFileTitle} from "../common"
import {SiteMenu} from "../menu"
import {menuModel, bulkMenuModel} from "./menu"
import {FeedbackTab} from "../feedback"
import {
    docSchema
} from "../schema/document"

export class BookOverview {
    // A class that contains everything that happens on the books page.
    // It is currently not possible to initialize more than one such class,
    // as it contains bindings to menu items, etc. that are uniquely defined.
    constructor({app, user}, path = '/') {
        this.app = app
        this.user = user
        this.path = path
        this.schema = docSchema
        this.mod = {}
        this.bookList = []
        this.styles = false
        this.documentList = []
        this.teamMembers = []
        this.accessRights = []
        this.citationStyles = []
        this.subdirs = {}
        this.lastSort = {column: 0, dir: 'asc'}
    }

    init() {
        return whenReady().then(() => {
            this.render()
            const smenu = new SiteMenu(this.app, "books")
            smenu.init()
            new BookActions(this)
            this.menu = new OverviewMenuView(this, menuModel)
            this.menu.init()
            this.dtBulkModel = bulkMenuModel()
            this.activateFidusPlugins()
            this.bind()
            return this.getBookListData().then(
                () => this.app.csl.getStyles().then(
                    styles => {
                        this.citationStyles = styles
                        return deactivateWait()
                    }
                )
            )
        })
    }

    showCached() {
        return this.loaddatafromIndexedDB().then(json => {
            if (!json) {
                activateWait(true)
                return
            }
            return this.initializeView(json)
        })
    }

    activateFidusPlugins() {
        // Add plugins.
        this.plugins = {}

        Object.keys(plugins).forEach(plugin => {
            if (typeof plugins[plugin] === 'function') {
                this.plugins[plugin] = new plugins[plugin](this)
                this.plugins[plugin].init()
            }
        })
    }

    render() {
        ensureCSS([
            'add_remove_dialog.css',
            'access_rights_dialog.css',
            'book.css'
        ])
        this.dom = document.createElement('body')
        this.dom.innerHTML = baseBodyTemplate({
            contents: '',
            user: this.user,
            hasOverview: true
        })
        document.body = this.dom

        setDocTitle(gettext('Book Overview'), this.app)
        const feedbackTab = new FeedbackTab()
        feedbackTab.init()
    }

    getImageDB() {
        if (!this.imageDB) {
            const imageGetter = new ImageDB()
            return new Promise(resolve => {
                imageGetter.getDB().then(
                    () => {
                        this.imageDB = imageGetter
                        resolve()
                    }
                )
            })
        } else {
            return Promise.resolve()
        }
    }

    onResize() {
        if (!this.table) {
            return
        }
        this.initTable()
    }

    /* Initialize the overview table */
    initTable() {
        if (this.table) {
            this.table.destroy()
            this.table = false
        }
        this.subdirs = {}
        const tableEl = document.createElement('table')
        tableEl.classList.add('fw-data-table')
        tableEl.classList.add('fw-large')
        this.dom.querySelector('.fw-contents').innerHTML = ''
        this.dom.querySelector('.fw-contents').appendChild(tableEl)

        this.dtBulk = new DatatableBulk(this, this.dtBulkModel)

        const hiddenCols = [0, 1]

        if (window.innerWidth < 500) {
            hiddenCols.push(2)
            if (window.innerWidth < 400) {
                hiddenCols.push(4)
            }
        }

        const fileList = this.bookList.map(
            book => this.createTableRow(book)
        ).filter(row => !!row)

        if (this.path !== '/') {
            fileList.unshift([
                '-1',
                'top',
                '',
                `

                    ..
                `,
                '',
                '',
                '',
                '',
                ''
            ])
        }

        this.table = new DataTable(tableEl, {
            searchable: true,
            paging: false,
            scrollY: `${Math.max(window.innerHeight - 360, 100)}px`,
            labels: {
                noRows: gettext("No books available") // Message shown when there are no search results
            },
            layout: {
                top: ""
            },
            data: {
                headings: ['', '', this.dtBulk.getHTML(), gettext("Title"), gettext("Created"), gettext("Last changed"), gettext("Owner"), gettext("Rights"), ''],
                data: fileList
            },
            columns: [
                {
                    select: hiddenCols,
                    hidden: true
                },
                {
                    select: [2, 7, 8],
                    sortable: false
                },
                {
                    select: [this.lastSort.column],
                    sort: this.lastSort.dir
                }
            ]
        })

        this.table.on('datatable.sort', (column, dir) => {
            this.lastSort = {column, dir}
        })

        this.dtBulk.init(this.table.table)
    }

    createTableRow(book) {
        let path = book.path
        if (!path.startsWith('/')) {
            path = '/' + path
        }
        if (!path.startsWith(this.path)) {
            return false
        }
        if (path.endsWith('/')) {
            path += book.title
        }

        const currentPath = path.slice(this.path.length)
        if (currentPath.includes('/')) {
            // There is a subdir
            const subdir = currentPath.split('/').shift()
            if (this.subdirs[subdir]) {
                // subdir has been covered already
                // We only update the update/added columns if needed.
                if (book.added < this.subdirs[subdir].added) {
                    this.subdirs[subdir].added = book.added
                    this.subdirs[subdir].row[5] = `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`
                }
                if (doc.updated > this.subdirs[subdir].updated) {
                    this.subdirs[subdir].updated = book.updated
                    this.subdirs[subdir].row[6] = `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`
                }
                return false
            }
            // Display subdir
            const row = [
                '0',
                'folder',
                '',
                `

                    ${escapeText(subdir)}
                `,
                `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`,
                `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`,
                '',
                '',
                ''
            ]
            this.subdirs[subdir] = {row, added: book.added, updated: book.updated}
            return row
        }

        // This is the folder of the file. Return the file.
        return [
            String(book.id),
            'file',
            `<input type="checkbox" class="entry-select fw-check" data-id="${book.id}" id="book-${book.id}"><label for="book-${book.id}"></label>`,
            `

                    ${shortFileTitle(book.title, book.path)}

            `,
            `<span class="date">${localizeDate(book.added * 1000, 'sortable-date')}</span>`,
            `<span class="date">${localizeDate(book.updated * 1000, 'sortable-date')}</span>`,
            `

            ${escapeText(book.owner_name)}`,
            `

            `,
            `
                ${this.user.id === book.owner ? '' : ''}
           `
        ]
    }

    removeTableRows(ids) {
        const existingRows = this.table.data.map((data, index) => {
            const id = parseInt(data.cells[0].textContent)
            if (ids.includes(id)) {
                return index
            } else {
                return false
            }
        }).filter(rowIndex => rowIndex !== false)

        if (existingRows.length) {
            this.table.rows().remove(existingRows)
        }
    }

    addBookToTable(book) {
        this.table.insert({data: [this.createTableRow(book)]})
        // Redo last sort
        this.table.columns().sort(this.lastSort.column, this.lastSort.dir)
    }

    getBookListData() {
        const cachedPromise = this.showCached()
        if (this.app.isOffline()) {
            return cachedPromise
        }
        return postJson(
            '/api/book/list/'
        ).then(
            ({json}) => {
                return cachedPromise.then(
                    () => this.loaddatafromIndexedDB()
                ).then(oldJson => {
                    if (!deepEqual(json, oldJson)) {
                        this.updateIndexedDB(json)
                        this.initializeView(json)
                    }
                })
            }
        ).catch(
            error => {
                if (this.app.isOffline()) {
                    return cachedPromise
                } else {
                    addAlert('error', gettext('Cannot load data of books.'))
                    throw (error)
                }
            }
        ).then(
            () => deactivateWait()
        )
    }

    initializeView(json) {
        this.bookList = this.unpackBooks(json.books)
        this.documentList = json.documents
        this.teamMembers = json.team_members
        this.accessRights = json.access_rights
        this.styles = json.styles

        this.initTable()
    }

    loaddatafromIndexedDB() {
        return this.app.indexedDB.readAllData("books_data").then(
            response => {
                if (!response.length) {
                    return false
                }
                const data = response[0]
                delete data.id
                return data
            }
        )
    }

    updateIndexedDB(json) {
        json.id = 1
        // Clear data if any present
        return this.app.indexedDB.clearData("books_data").then(
            () => this.app.indexedDB.insertData("books_data", [json])
        )
    }

    unpackBooks(booksFromServer) {
        // metadata and settings are stored as a json stirng in a text field on
        // the server, so they need to be unpacked before being available.
        return booksFromServer.map(book => {
            const uBook = Object.assign({}, book)
            uBook.metadata = JSON.parse(book.metadata)
            uBook.settings = JSON.parse(book.settings)
            return uBook
        })
    }

    bind() {
        this.dom.addEventListener('click', event => {
            const el = {}
            switch (true) {
            case findTarget(event, '.delete-book', el): {
                const bookId = parseInt(el.target.dataset.id)
                this.mod.actions.deleteBookDialog([bookId])
                break
            }
            case findTarget(event, '.owned-by-user.rights', el): {
                const bookId = parseInt(el.target.dataset.id)
                const accessDialog = new BookAccessRightsDialog(
                    [bookId],
                    this.teamMembers,
                    this.accessRights
                )
                accessDialog.init().then(
                    accessRights => this.accessRights = accessRights
                )
                break
            }
            case findTarget(event, '.book-title', el): {
                const bookId = parseInt(el.target.dataset.id)
                this.getImageDB().then(() => {
                    this.mod.actions.createBookDialog(bookId, this.imageDB)
                })
                break
            }
            case findTarget(event, '.fw-data-table-title .subdir', el):
                this.path += el.target.dataset.subdir + '/'
                window.history.pushState({}, "", '/books' + this.path)
                this.initTable()
                break
            case findTarget(event, '.fw-data-table-title .parentdir', el): {
                const pathParts = this.path.split('/')
                pathParts.pop()
                pathParts.pop()
                this.path = pathParts.join('/') + '/'
                window.history.pushState({}, "", '/books' + this.path)
                this.initTable()
                break
            }
            case findTarget(event, 'a', el):
                if (el.target.hostname === window.location.hostname && el.target.getAttribute('href')[0] === '/') {
                    event.preventDefault()
                    this.app.goTo(el.target.href)
                }
                break
            default:
                break
            }
        })
    }

    getSelected() {
        return Array.from(
            this.dom.querySelectorAll('.entry-select:checked:not(:disabled)')
        ).map(el => parseInt(el.getAttribute('data-id')))
    }
}
<|endoftext|>


<commit_before>/*
 */
function MenuAssistant() {
	/* this is the creator function for your scene assistant object. It will be passed all the
	   additional parameters (after the scene name) that were passed to pushScene. The reference
	   to the scene controller (this.controller) has not be established yet, so any initialization
	   that needs the scene controller should be done in the setup function below. */
}

MenuAssistant.prototype.activate = function(event) {
	/* put in event handlers here that should only be in effect when this scene is active. For
	   example, key handlers that are observing the document */

	// Rerender Menu
	this.load( "thisDay" );

	/* periodically set header as to pick up date changes */
	this.headerReloadTimeout = 1000 * 60 * 10; // every ten minutes ought to be enough
	this.headerReloadTimer = setTimeout( this.reloadHeader.bind(this), this.headerReloadTimeout );
};

MenuAssistant.prototype.reloadHeader = function(event) {
	this.setHeader();
	this.headerReloadTimer = setTimeout( this.reloadHeader.bind(this), this.headerReloadTimeout ); // rinse, repeat
};

MenuAssistant.prototype.deactivate = function(event) {
	/* remove any event handlers you added in activate and do any other cleanup that should happen before
	   this scene is popped or another scene is pushed on top */
	clearTimeout( this.headerReloadTimer );
};

MenuAssistant.prototype.cleanup = function(event) {
	/* this function should do any cleanup needed before the scene is destroyed as
	   a result of being popped off the scene stack */
};

MenuAssistant.prototype.handleFlick = function(event) {
	var minXVel = 200;
	var x = event.velocity.x;
	var y = event.velocity.y;
	if(Math.abs(x) > Math.abs(y)){
		if( x > minXVel && storage.isPrevDayAvailable() ){
			this.load("prevDay");
		} else if ( x < -minXVel && storage.isNextDayAvailable() ) {
			this.load("nextDay");
		}
	}
};

MenuAssistant.prototype.setup = function() {
	// render app menu
	this.controller.setupWidget(Mojo.Menu.appMenu, {omitDefaultItems:true}, {
		items: [
			{ label: "Menu",           command : "menu", disabled: true   },
			{ label: "Konfigurieren",  command : "config" },
			{ label: "Filtern",        command : "filter" },
			{ label: "Über diese App", command : "info"  },
			{ label: "Zurücksetzen",   command : "reset"  }
		]
	});

	if(!conf.isConfigured()){
		this.controller.showAlertDialog({
			onChoose: function(value) {
				if(value === "conf"){
					Mojo.Controller.stageController.pushScene("config");
				}
			},
			title   : info.notConfTitle,
			message : info.notConfText,
			choices : [
				{ label: $L("Jetzt konfigurieren"), value: "conf" }
			]
		});
	}

	// Swipe gestures
	Mojo.Event.listen(this.controller.topContainer(), Mojo.Event.flick, this.handleFlick.bind(this));

	// activity indicator
	this.controller.setupWidget("spinner", {spinnerSize: "large"}, { spinning: false });

	this.controller.setupWidget(Mojo.Menu.viewMenu,
		{spacerHeight: 0, menuClass: "no-fade"},
		this.headerMenu = {
			visible: true,
			items: [
				{}, // <-- helps to center the menu
				{
					items: [
						{ label: $L("Yesterday"), icon  : "back"   , command: "prevDay" }, // left
						{ label: "Heute"        , width : 200      , command: "today"   }, // center
						{ label: $L("Refresh")  , icon  :"forward" , command: "nextDay" }  // right
					]
				},
				{} // <-- helps to center the menu
			]
		}
	);

	// get list data async
	// Data is fetched in activate...
	this.controller.setupWidget("menu",{
		itemTemplate: "menu/static-list-menu-entry",
		emptyTemplate:"menu/emptylist",
		dividerTemplate: "menu/dividerTemplate",
		fixedHeightItems: true,
		hasNoWidgets: true,
		dividerFunction: function(listitem){
			return listitem.mensa;
		}
	},
	this.items = {"items": []} // Modell
	);

	this.menu = this.controller.get("menu");

	this.controller.listen("menu", Mojo.Event.listTap, this.handleTap.bind(this));

	// implement a filterField widget for real time filtering
	this.controller.setupWidget("filterField");
	this.filterField = this.controller.get("filterField");
	Mojo.Event.listen(this.filterField, Mojo.Event.filter, this.handleFilter.bind(this));
	Mojo.Event.listen(this.filterField, Mojo.Event.filterImmediate, this.handleFilterDisplay.bind(this));
};

MenuAssistant.prototype.handleFilter = function(event){
	this.items.items = this.json.filter(function(item){
		return event.filterString === "" || ((item.dish + item.name + item.mensa).toLowerCase().indexOf(event.filterString.toLowerCase()) !== -1);
	});
	this.filterField.mojo.setCount(this.items.items.length);
	this.menu.mojo.setLengthAndInvalidate(this.items.items.length);
};

MenuAssistant.prototype.handleFilterDisplay = function(event){
	var main = document.getElementById("main");
	if(event.filterString){
		main.className = main.className.replace("palm-hasheader", "");
	} else {
		main.className += " palm-hasheader";
	}

	this.headerMenu.visible = event.filterString === "";
	this.controller.modelChanged( this.headerMenu, this);
};

MenuAssistant.prototype.handleTap = function(event){
	//changes the open property in the drawer
	event.item.open = !event.item.open;
	event.item.drawerDisplay = event.item.open ? "block" : "none";
	if( !event.item.propertiesString && !event.item.additivesString){
		event.item.propertiesString = "";
		event.item.additivesString = "";
		var i;
		for(i = 0; i<event.item.properties.length; i++) {
			event.item.propertiesString += "
" + event.item.properties[i] + "
";
		}

		for(i = 0; i<event.item.additives.length; i++) {
			event.item.additivesString += "
" + event.item.additives[i] + "
";
		}

		event.item.propertiesString = event.item.propertiesString ? event.item.propertiesString : "
keine
";
		event.item.additivesString = event.item.additivesString ? event.item.additivesString : "
keine
";
	}
	// invalidate list elements (= rerender list items);
	// to avoid duplicate dividers invalidate surrounding items as well
	this.menu.mojo.invalidateItems(event.index-2, 5);
};

MenuAssistant.prototype.handleCommand = function(event) {
	if( event.command === "prevDay" || event.command === "nextDay" || event.command === "today" ){
		this.load( event.command );
	}
};

MenuAssistant.prototype.load = function(type){
	storage[type](this.fetch.bind(this), false);
	this.timedOut = false;
	this.showLoaderTimer = setTimeout(this.showLoader.bind(this), 100);
};

MenuAssistant.prototype.timedOut = true;
MenuAssistant.prototype.showLoaderTimer = false;

MenuAssistant.prototype.showLoader = function(type){
	this.controller.get("spinner").mojo.start();
	this.menu.style.display = "none";
	this.timedOut = true;
};

MenuAssistant.prototype.fetch = function(json, dateString, date){
	// stop wait indicator
	clearTimeout(this.showLoaderTimer);
	if(this.timedOut){
		this.controller.get("spinner").mojo.stop();
		this.menu.style.display = "block";
	}

	this.json = [];

	// Set price
	// @TODO: move to storage
	var studentPrices = conf.displayStudentPrices();
	for(var i=0; i<json.length; i++){
		json[i].price = studentPrices ? json[i].studPrice : json[i].normalPrice;
		json[i].price = json[i].price ? Mojo.Format.formatCurrency(json[i].price, {fractionDigits: 2, countryCode : "de"}) : "";
		json[i].drawerDisplay = "none";
		this.json[i] = json[i];
	}

	// update menu
	this.items.items = json;
	this.menu.mojo.revealItem(0, false);

	this.menu.mojo.setLengthAndInvalidate( Math.min(6, this.items.items.length) );
	setTimeout(function(){
//		this.controller.modelChanged( this.items, this);
		this.menu.mojo.setLength(this.items.items.length);
	}.bind(this), 1);

	// update header
	this.date = date;
	this.setHeader();
};

MenuAssistant.prototype.setHeader = function(){
	this.headerMenu.items[1].items[0].disabled = !storage.isPrevDayAvailable(); // left
	this.headerMenu.items[1].items[1].label = formatDate(this.date);            // center
	this.headerMenu.items[1].items[2].disabled = !storage.isNextDayAvailable(); // right

	this.controller.modelChanged( this.headerMenu, this);
};
<commit_msg>Mojo: Reduce flashing while searching<commit_after>function MenuAssistant() {
	/* this is the creator function for your scene assistant object. It will be passed all the
	   additional parameters (after the scene name) that were passed to pushScene. The reference
	   to the scene controller (this.controller) has not be established yet, so any initialization
	   that needs the scene controller should be done in the setup function below. */
}

MenuAssistant.prototype.activate = function(event) {
	/* put in event handlers here that should only be in effect when this scene is active. For
	   example, key handlers that are observing the document */

	// Rerender Menu
	this.load( "thisDay" );

	/* periodically set header as to pick up date changes */
	this.headerReloadTimeout = 1000 * 60 * 10; // every ten minutes ought to be enough
	this.headerReloadTimer = setTimeout( this.reloadHeader.bind(this), this.headerReloadTimeout );
};

MenuAssistant.prototype.reloadHeader = function(event) {
	this.setHeader();
	this.headerReloadTimer = setTimeout( this.reloadHeader.bind(this), this.headerReloadTimeout ); // rinse, repeat
};

MenuAssistant.prototype.deactivate = function(event) {
	/* remove any event handlers you added in activate and do any other cleanup that should happen before
	   this scene is popped or another scene is pushed on top */
	clearTimeout( this.headerReloadTimer );
};

MenuAssistant.prototype.cleanup = function(event) {
	/* this function should do any cleanup needed before the scene is destroyed as
	   a result of being popped off the scene stack */
};

MenuAssistant.prototype.handleFlick = function(event) {
	var minXVel = 200;
	var x = event.velocity.x;
	var y = event.velocity.y;
	if(Math.abs(x) > Math.abs(y)){
		if( x > minXVel && storage.isPrevDayAvailable() ){
			this.load("prevDay");
		} else if ( x < -minXVel && storage.isNextDayAvailable() ) {
			this.load("nextDay");
		}
	}
};

MenuAssistant.prototype.setup = function() {
	// render app menu
	this.controller.setupWidget(Mojo.Menu.appMenu, {omitDefaultItems:true}, {
		items: [
			{ label: "Menu",           command : "menu", disabled: true   },
			{ label: "Konfigurieren",  command : "config" },
			{ label: "Filtern",        command : "filter" },
			{ label: "Über diese App", command : "info"  },
			{ label: "Zurücksetzen",   command : "reset"  }
		]
	});

	if(!conf.isConfigured()){
		this.controller.showAlertDialog({
			onChoose: function(value) {
				if(value === "conf"){
					Mojo.Controller.stageController.pushScene("config");
				}
			},
			title   : info.notConfTitle,
			message : info.notConfText,
			choices : [
				{ label: $L("Jetzt konfigurieren"), value: "conf" }
			]
		});
	}

	// Swipe gestures
	Mojo.Event.listen(this.controller.topContainer(), Mojo.Event.flick, this.handleFlick.bind(this));

	// activity indicator
	this.controller.setupWidget("spinner", {spinnerSize: "large"}, { spinning: false });

	this.controller.setupWidget(Mojo.Menu.viewMenu,
		{spacerHeight: 0, menuClass: "no-fade"},
		this.headerMenu = {
			visible: true,
			items: [
				{}, // <-- helps to center the menu
				{
					items: [
						{ label: $L("Yesterday"), icon  : "back"   , command: "prevDay" }, // left
						{ label: "Heute"        , width : 200      , command: "today"   }, // center
						{ label: $L("Refresh")  , icon  :"forward" , command: "nextDay" }  // right
					]
				},
				{} // <-- helps to center the menu
			]
		}
	);

	// get list data async
	// Data is fetched in activate...
	this.controller.setupWidget("menu",{
		itemTemplate: "menu/static-list-menu-entry",
		emptyTemplate:"menu/emptylist",
		dividerTemplate: "menu/dividerTemplate",
		fixedHeightItems: true,
		hasNoWidgets: true,
		dividerFunction: function(listitem){
			return listitem.mensa;
		}
	},
	this.items = {"items": []} // Modell
	);

	this.menu = this.controller.get("menu");

	this.controller.listen("menu", Mojo.Event.listTap, this.handleTap.bind(this));

	// implement a filterField widget for real time filtering
	this.controller.setupWidget("filterField", { delay: 200 });
	this.filterField = this.controller.get("filterField");
	Mojo.Event.listen(this.filterField, Mojo.Event.filter, this.handleFilter.bind(this));
	Mojo.Event.listen(this.filterField, Mojo.Event.filterImmediate, this.handleFilterDisplay.bind(this));
};

MenuAssistant.prototype.handleFilter = function(event){
	this.items.items = this.json.filter(function(item){
		return event.filterString === "" || ((item.dish + item.name + item.mensa).toLowerCase().indexOf(event.filterString.toLowerCase()) !== -1);
	});
	this.filterField.mojo.setCount(this.items.items.length);
	this.menu.mojo.revealItem(0, false);
	setTimeout(function(){
		this.menu.mojo.setLengthAndInvalidate(this.items.items.length);
	}.bind(this), 10);
};

MenuAssistant.prototype.handleFilterDisplay = function(event){
	var main = document.getElementById("main");
	if(event.filterString.length){
		main.className = main.className.replace("palm-hasheader", "");
	} else {
		main.className += " palm-hasheader";
	}

	this.headerMenu.visible = event.filterString === "";
	this.controller.modelChanged( this.headerMenu, this);
};

MenuAssistant.prototype.handleTap = function(event){
	//changes the open property in the drawer
	event.item.open = !event.item.open;
	event.item.drawerDisplay = event.item.open ? "block" : "none";
	if( !event.item.propertiesString && !event.item.additivesString){
		event.item.propertiesString = "";
		event.item.additivesString = "";
		var i;
		for(i = 0; i<event.item.properties.length; i++) {
			event.item.propertiesString += "
" + event.item.properties[i] + "
";
		}

		for(i = 0; i<event.item.additives.length; i++) {
			event.item.additivesString += "
" + event.item.additives[i] + "
";
		}

		event.item.propertiesString = event.item.propertiesString ? event.item.propertiesString : "
keine
";
		event.item.additivesString = event.item.additivesString ? event.item.additivesString : "
keine
";
	}
	// invalidate list elements (= rerender list items);
	// to avoid duplicate dividers invalidate surrounding items as well
	this.menu.mojo.invalidateItems(event.index-2, 5);
};

MenuAssistant.prototype.handleCommand = function(event) {
	if( event.command === "prevDay" || event.command === "nextDay" || event.command === "today" ){
		this.load( event.command );
	}
};

MenuAssistant.prototype.load = function(type){
	storage[type](this.fetch.bind(this), false);
	this.timedOut = false;
	this.showLoaderTimer = setTimeout(this.showLoader.bind(this), 100);
};

MenuAssistant.prototype.timedOut = true;
MenuAssistant.prototype.showLoaderTimer = false;

MenuAssistant.prototype.showLoader = function(type){
	this.controller.get("spinner").mojo.start();
	this.menu.style.display = "none";
	this.timedOut = true;
};

MenuAssistant.prototype.fetch = function(json, dateString, date){
	// stop wait indicator
	clearTimeout(this.showLoaderTimer);
	if(this.timedOut){
		this.controller.get("spinner").mojo.stop();
		this.menu.style.display = "block";
	}

	this.json = [];

	// Set price
	// @TODO: move to storage
	var studentPrices = conf.displayStudentPrices();
	for(var i=0; i<json.length; i++){
		json[i].price = studentPrices ? json[i].studPrice : json[i].normalPrice;
		json[i].price = json[i].price ? Mojo.Format.formatCurrency(json[i].price, {fractionDigits: 2, countryCode : "de"}) : "";
		json[i].drawerDisplay = "none";
		this.json[i] = json[i];
	}

	// update menu
	this.items.items = json;
	this.menu.mojo.revealItem(0, false);

	this.menu.mojo.setLengthAndInvalidate( Math.min(6, this.items.items.length) );
	setTimeout(function(){
//		this.controller.modelChanged( this.items, this);
		this.menu.mojo.setLength(this.items.items.length);
	}.bind(this), 1);

	// update header
	this.date = date;
	this.setHeader();
};

MenuAssistant.prototype.setHeader = function(){
	this.headerMenu.items[1].items[0].disabled = !storage.isPrevDayAvailable(); // left
	this.headerMenu.items[1].items[1].label = formatDate(this.date);            // center
	this.headerMenu.items[1].items[2].disabled = !storage.isNextDayAvailable(); // right

	this.controller.modelChanged( this.headerMenu, this);
};
<|endoftext|>


<commit_before>import React from 'react';
import {NavigationContext} from 'navigation-react';

class Scene extends React.Component {
    constructor(props) {
        super(props);
        this.state = {navigationEvent: null};
    }
    static defaultProps = {
        crumb: 0
    }
    static getDerivedStateFromProps(props) {
        var {crumb, navigationEvent} = props;
        var {state, crumbs} = navigationEvent.stateNavigator.stateContext;
        return (state && crumbs.length === crumb) ? {navigationEvent} : null;
    }
    shouldComponentUpdate() {
        var {crumb, navigationEvent} = props;
        var {state, crumbs} = navigationEvent.stateNavigator.stateContext;
        return state && crumbs.length === crumb;
    }
    render() {
        var {navigationEvent} = this.state;
        if (!navigationEvent) return null;
        var {state, data} = navigationEvent.stateNavigator.stateContext;
        return (
            <NavigationContext.Provider value={navigationEvent}>
                {state.renderScene(data)}
            </NavigationContext.Provider>
        );
    }
}

export default props => (
    <NavigationContext.Consumer>
        {(navigationEvent) => <Scene navigationEvent={navigationEvent} {...props} />}
    </NavigationContext.Consumer>
)
<commit_msg>Added missing props param<commit_after>import React from 'react';
import {NavigationContext} from 'navigation-react';

class Scene extends React.Component {
    constructor(props) {
        super(props);
        this.state = {navigationEvent: null};
    }
    static defaultProps = {
        crumb: 0
    }
    static getDerivedStateFromProps(props) {
        var {crumb, navigationEvent} = props;
        var {state, crumbs} = navigationEvent.stateNavigator.stateContext;
        return (state && crumbs.length === crumb) ? {navigationEvent} : null;
    }
    shouldComponentUpdate(props) {
        var {crumb, navigationEvent} = props;
        var {state, crumbs} = navigationEvent.stateNavigator.stateContext;
        return state && crumbs.length === crumb;
    }
    render() {
        var {navigationEvent} = this.state;
        if (!navigationEvent) return null;
        var {state, data} = navigationEvent.stateNavigator.stateContext;
        return (
            <NavigationContext.Provider value={navigationEvent}>
                {state.renderScene(data)}
            </NavigationContext.Provider>
        );
    }
}

export default props => (
    <NavigationContext.Consumer>
        {(navigationEvent) => <Scene navigationEvent={navigationEvent} {...props} />}
    </NavigationContext.Consumer>
)
<|endoftext|>