<commit_before>/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Promise from 'bluebird';
import utils from '../../utils';

export const VERSION = 6;

/**
 * Implementation of a CryptoStore which is backed by an existing
 * IndexedDB connection. Generally you want IndexedDBCryptoStore
 * which connects to the database and defers to one of these.
 *
 * @implements {module:crypto/store/base~CryptoStore}
 */
export class Backend {
    /**
     * @param {IDBDatabase} db
     */
    constructor(db) {
        this._db = db;

        // make sure we close the db on `onversionchange` - otherwise
        // attempts to delete the database will block (and subsequent
        // attempts to re-create it will also block).
        db.onversionchange = (ev) => {
            console.log(`versionchange for indexeddb ${this._dbName}: closing`);
            db.close();
        };
    }

    /**
     * Look for an existing outgoing room key request, and if none is found,
     * add a new one
     *
     * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
     *
     * @returns {Promise} resolves to
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
     *    same instance as passed in, or the existing one.
     */
    getOrAddOutgoingRoomKeyRequest(request) {
        const requestBody = request.requestBody;

        const deferred = Promise.defer();
        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        txn.onerror = deferred.reject;

        // first see if we already have an entry for this request.
        this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
            if (existing) {
                // this entry matches the request - return it.
                console.log(
                    `already have key request outstanding for ` +
                        `${requestBody.room_id} / ${requestBody.session_id}: ` +
                        `not sending another`,
                );
                deferred.resolve(existing);
                return;
            }

            // we got to the end of the list without finding a match
            // - add the new request.
            console.log(
                `enqueueing key request for ${requestBody.room_id} / ` +
                    requestBody.session_id,
            );
            txn.oncomplete = () => { deferred.resolve(request); };
            const store = txn.objectStore("outgoingRoomKeyRequests");
            store.add(request);
        });

        return deferred.promise;
    }

    /**
     * Look for an existing room key request
     *
     * @param {module:crypto~RoomKeyRequestBody} requestBody
     *    existing request to look for
     *
     * @return {Promise} resolves to the matching
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    not found
     */
    getOutgoingRoomKeyRequest(requestBody) {
        const deferred = Promise.defer();

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
        txn.onerror = deferred.reject;

        this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
            deferred.resolve(existing);
        });
        return deferred.promise;
    }

    /**
     * look for an existing room key request in the db
     *
     * @private
     * @param {IDBTransaction} txn  database transaction
     * @param {module:crypto~RoomKeyRequestBody} requestBody
     *    existing request to look for
     * @param {Function} callback  function to call with the results of the
     *    search. Either passed a matching
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    not found.
     */
    _getOutgoingRoomKeyRequest(txn, requestBody, callback) {
        const store = txn.objectStore("outgoingRoomKeyRequests");

        const idx = store.index("session");
        const cursorReq = idx.openCursor([
            requestBody.room_id,
            requestBody.session_id,
        ]);

        cursorReq.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if(!cursor) {
                // no match found
                callback(null);
                return;
            }

            const existing = cursor.value;

            if (utils.deepCompare(existing.requestBody, requestBody)) {
                // got a match
                callback(existing);
                return;
            }

            // look at the next entry in the index
            cursor.continue();
        };
    }

    /**
     * Look for room key requests by state
     *
     * @param {Array<Number>} wantedStates list of acceptable states
     *
     * @return {Promise} resolves to the a
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    there are no pending requests in those states. If there are multiple
     *    requests in those states, an arbitrary one is chosen.
     */
    getOutgoingRoomKeyRequestByState(wantedStates) {
        if (wantedStates.length === 0) {
            return Promise.resolve(null);
        }

        // this is a bit tortuous because we need to make sure we do the lookup
        // in a single transaction, to avoid having a race with the insertion
        // code.

        // index into the wantedStates array
        let stateIndex = 0;
        let result;

        function onsuccess(ev) {
            const cursor = ev.target.result;
            if (cursor) {
                // got a match
                result = cursor.value;
                return;
            }

            // try the next state in the list
            stateIndex++;
            if (stateIndex >= wantedStates.length) {
                // no matches
                return;
            }

            const wantedState = wantedStates[stateIndex];
            const cursorReq = ev.target.source.openCursor(wantedState);
            cursorReq.onsuccess = onsuccess;
        }

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
        const store = txn.objectStore("outgoingRoomKeyRequests");

        const wantedState = wantedStates[stateIndex];
        const cursorReq = store.index("state").openCursor(wantedState);
        cursorReq.onsuccess = onsuccess;

        return promiseifyTxn(txn).then(() => result);
    }

    /**
     * Look for an existing room key request by id and state, and update it if
     * found
     *
     * @param {string} requestId      ID of request to update
     * @param {number} expectedState  state we expect to find the request in
     * @param {Object} updates        name/value map of updates to apply
     *
     * @returns {Promise} resolves to
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}
     *    updated request, or null if no matching row was found
     */
    updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
        let result = null;

        function onsuccess(ev) {
            const cursor = ev.target.result;
            if (!cursor) {
                return;
            }
            const data = cursor.value;
            if (data.state != expectedState) {
                console.warn(
                    `Cannot update room key request from ${expectedState} ` +
                    `as it was already updated to ${data.state}`,
                );
                return;
            }
            Object.assign(data, updates);
            cursor.update(data);
            result = data;
        }

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
                  .openCursor(requestId);
        cursorReq.onsuccess = onsuccess;
        return promiseifyTxn(txn).then(() => result);
    }

    /**
     * Look for an existing room key request by id and state, and delete it if
     * found
     *
     * @param {string} requestId      ID of request to update
     * @param {number} expectedState  state we expect to find the request in
     *
     * @returns {Promise} resolves once the operation is completed
     */
    deleteOutgoingRoomKeyRequest(requestId, expectedState) {
        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
                  .openCursor(requestId);
        cursorReq.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if (!cursor) {
                return;
            }
            const data = cursor.value;
            if (data.state != expectedState) {
                console.warn(
                    `Cannot delete room key request in state ${data.state} `
                        + `(expected ${expectedState})`,
                );
                return;
            }
            cursor.delete();
        };
        return promiseifyTxn(txn);
    }

    // Olm Account

    getAccount(txn, func) {
        const objectStore = txn.objectStore("account");
        const getReq = objectStore.get("-");
        getReq.onsuccess = function() {
            try {
                func(getReq.result || null);
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeAccount(txn, newData) {
        const objectStore = txn.objectStore("account");
        objectStore.put(newData, "-");
    }

    // Olm Sessions

    countEndToEndSessions(txn, func) {
        const objectStore = txn.objectStore("sessions");
        const countReq = objectStore.count();
        countReq.onsuccess = function() {
            func(countReq.result);
        };
    }

    getEndToEndSessions(deviceKey, txn, func) {
        const objectStore = txn.objectStore("sessions");
        const idx = objectStore.index("deviceKey");
        const getReq = idx.openCursor(deviceKey);
        const results = {};
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                results[cursor.value.sessionId] = cursor.value.session;
                cursor.continue();
            } else {
                try {
                    func(results);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    getEndToEndSession(deviceKey, sessionId, txn, func) {
        const objectStore = txn.objectStore("sessions");
        const getReq = objectStore.get([deviceKey, sessionId]);
        getReq.onsuccess = function() {
            try {
                if (getReq.result) {
                    func(getReq.result.session);
                } else {
                    func(null);
                }
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeEndToEndSession(deviceKey, sessionId, session, txn) {
        const objectStore = txn.objectStore("sessions");
        objectStore.put({deviceKey, sessionId, session});
    }

    // Inbound group sessions

    getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const getReq = objectStore.get([senderCurve25519Key, sessionId]);
        getReq.onsuccess = function() {
            try {
                if (getReq.result) {
                    func(getReq.result.session);
                } else {
                    func(null);
                }
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    getAllEndToEndInboundGroupSessions(txn, func) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const getReq = objectStore.openCursor();
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                try {
                    func({
                        senderKey: cursor.value.senderCurve25519Key,
                        sessionId: cursor.value.sessionId,
                        sessionData: cursor.value.session,
                    });
                } catch (e) {
                    abortWithException(txn, e);
                }
                cursor.continue();
            } else {
                try {
                    func(null);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const addReq = objectStore.add({
            senderCurve25519Key, sessionId, session: sessionData,
        });
        addReq.onerror = () => {
            if (addReq.name === 'ConstraintError') {
                console.log(
                    "Ignoring duplicate inbound group session: " +
                    senderCurve25519Key + " / " + sessionId,
                );
            } else {
                abortWithException(txn, new Error(
                    "Failed to add inbound group session: " + addReq.error,
                ));
            }
        };
    }

    storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        objectStore.put({
            senderCurve25519Key, sessionId, session: sessionData,
        });
    }

    getEndToEndDeviceData(txn, func) {
        const objectStore = txn.objectStore("device_data");
        const getReq = objectStore.get("-");
        getReq.onsuccess = function() {
            try {
                func(getReq.result || null);
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeEndToEndDeviceData(deviceData, txn) {
        const objectStore = txn.objectStore("device_data");
        objectStore.put(deviceData, "-");
    }

    storeEndToEndRoom(roomId, roomInfo, txn) {
        const objectStore = txn.objectStore("rooms");
        objectStore.put(roomInfo, roomId);
    }

    getEndToEndRooms(txn, func) {
        const rooms = {};
        const objectStore = txn.objectStore("rooms");
        const getReq = objectStore.openCursor();
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                rooms[cursor.key] = cursor.value;
                cursor.continue();
            } else {
                try {
                    func(rooms);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    doTxn(mode, stores, func) {
        const txn = this._db.transaction(stores, mode);
        const promise = promiseifyTxn(txn);
        const result = func(txn);
        return promise.then(() => {
            return result;
        });
    }
}

export function upgradeDatabase(db, oldVersion) {
    console.log(
        `Upgrading IndexedDBCryptoStore from version ${oldVersion}`
            + ` to ${VERSION}`,
    );
    if (oldVersion < 1) { // The database did not previously exist.
        createDatabase(db);
    }
    if (oldVersion < 2) {
        db.createObjectStore("account");
    }
    if (oldVersion < 3) {
        const sessionsStore = db.createObjectStore("sessions", {
            keyPath: ["deviceKey", "sessionId"],
        });
        sessionsStore.createIndex("deviceKey", "deviceKey");
    }
    if (oldVersion < 4) {
        db.createObjectStore("inbound_group_sessions", {
            keyPath: ["senderCurve25519Key", "sessionId"],
        });
    }
    if (oldVersion < 5) {
        db.createObjectStore("device_data");
    }
    if (oldVersion < 6) {
        db.createObjectStore("rooms");
    }
    // Expand as needed.
}

function createDatabase(db) {
    const outgoingRoomKeyRequestsStore =
        db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });

    // we assume that the RoomKeyRequestBody will have room_id and session_id
    // properties, to make the index efficient.
    outgoingRoomKeyRequestsStore.createIndex("session",
        ["requestBody.room_id", "requestBody.session_id"],
    );

    outgoingRoomKeyRequestsStore.createIndex("state", "state");
}

/*
 * Aborts a transaction with a given exception
 * The transaction promise will be rejected with this exception.
 */
function abortWithException(txn, e) {
    // We cheekily stick our exception onto the transaction object here
    // We could alternatively make the thing we pass back to the app
    // an object containing the transaction and exception.
    txn._mx_abortexception = e;
    try {
        txn.abort();
    } catch (e) {
        // sometimes we won't be able to abort the transaction
        // (ie. if it's aborted or completed)
    }
}

function promiseifyTxn(txn) {
    return new Promise((resolve, reject) => {
        txn.oncomplete = () => {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            resolve();
        };
        txn.onerror = () => {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            reject();
        };
        txn.onabort = () => reject(txn._mx_abortexception);
    });
}
<commit_msg>Use the name of the error, not the request<commit_after>/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Promise from 'bluebird';
import utils from '../../utils';

export const VERSION = 6;

/**
 * Implementation of a CryptoStore which is backed by an existing
 * IndexedDB connection. Generally you want IndexedDBCryptoStore
 * which connects to the database and defers to one of these.
 *
 * @implements {module:crypto/store/base~CryptoStore}
 */
export class Backend {
    /**
     * @param {IDBDatabase} db
     */
    constructor(db) {
        this._db = db;

        // make sure we close the db on `onversionchange` - otherwise
        // attempts to delete the database will block (and subsequent
        // attempts to re-create it will also block).
        db.onversionchange = (ev) => {
            console.log(`versionchange for indexeddb ${this._dbName}: closing`);
            db.close();
        };
    }

    /**
     * Look for an existing outgoing room key request, and if none is found,
     * add a new one
     *
     * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
     *
     * @returns {Promise} resolves to
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
     *    same instance as passed in, or the existing one.
     */
    getOrAddOutgoingRoomKeyRequest(request) {
        const requestBody = request.requestBody;

        const deferred = Promise.defer();
        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        txn.onerror = deferred.reject;

        // first see if we already have an entry for this request.
        this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
            if (existing) {
                // this entry matches the request - return it.
                console.log(
                    `already have key request outstanding for ` +
                        `${requestBody.room_id} / ${requestBody.session_id}: ` +
                        `not sending another`,
                );
                deferred.resolve(existing);
                return;
            }

            // we got to the end of the list without finding a match
            // - add the new request.
            console.log(
                `enqueueing key request for ${requestBody.room_id} / ` +
                    requestBody.session_id,
            );
            txn.oncomplete = () => { deferred.resolve(request); };
            const store = txn.objectStore("outgoingRoomKeyRequests");
            store.add(request);
        });

        return deferred.promise;
    }

    /**
     * Look for an existing room key request
     *
     * @param {module:crypto~RoomKeyRequestBody} requestBody
     *    existing request to look for
     *
     * @return {Promise} resolves to the matching
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    not found
     */
    getOutgoingRoomKeyRequest(requestBody) {
        const deferred = Promise.defer();

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
        txn.onerror = deferred.reject;

        this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
            deferred.resolve(existing);
        });
        return deferred.promise;
    }

    /**
     * look for an existing room key request in the db
     *
     * @private
     * @param {IDBTransaction} txn  database transaction
     * @param {module:crypto~RoomKeyRequestBody} requestBody
     *    existing request to look for
     * @param {Function} callback  function to call with the results of the
     *    search. Either passed a matching
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    not found.
     */
    _getOutgoingRoomKeyRequest(txn, requestBody, callback) {
        const store = txn.objectStore("outgoingRoomKeyRequests");

        const idx = store.index("session");
        const cursorReq = idx.openCursor([
            requestBody.room_id,
            requestBody.session_id,
        ]);

        cursorReq.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if(!cursor) {
                // no match found
                callback(null);
                return;
            }

            const existing = cursor.value;

            if (utils.deepCompare(existing.requestBody, requestBody)) {
                // got a match
                callback(existing);
                return;
            }

            // look at the next entry in the index
            cursor.continue();
        };
    }

    /**
     * Look for room key requests by state
     *
     * @param {Array<Number>} wantedStates list of acceptable states
     *
     * @return {Promise} resolves to the a
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
     *    there are no pending requests in those states. If there are multiple
     *    requests in those states, an arbitrary one is chosen.
     */
    getOutgoingRoomKeyRequestByState(wantedStates) {
        if (wantedStates.length === 0) {
            return Promise.resolve(null);
        }

        // this is a bit tortuous because we need to make sure we do the lookup
        // in a single transaction, to avoid having a race with the insertion
        // code.

        // index into the wantedStates array
        let stateIndex = 0;
        let result;

        function onsuccess(ev) {
            const cursor = ev.target.result;
            if (cursor) {
                // got a match
                result = cursor.value;
                return;
            }

            // try the next state in the list
            stateIndex++;
            if (stateIndex >= wantedStates.length) {
                // no matches
                return;
            }

            const wantedState = wantedStates[stateIndex];
            const cursorReq = ev.target.source.openCursor(wantedState);
            cursorReq.onsuccess = onsuccess;
        }

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
        const store = txn.objectStore("outgoingRoomKeyRequests");

        const wantedState = wantedStates[stateIndex];
        const cursorReq = store.index("state").openCursor(wantedState);
        cursorReq.onsuccess = onsuccess;

        return promiseifyTxn(txn).then(() => result);
    }

    /**
     * Look for an existing room key request by id and state, and update it if
     * found
     *
     * @param {string} requestId      ID of request to update
     * @param {number} expectedState  state we expect to find the request in
     * @param {Object} updates        name/value map of updates to apply
     *
     * @returns {Promise} resolves to
     *    {@link module:crypto/store/base~OutgoingRoomKeyRequest}
     *    updated request, or null if no matching row was found
     */
    updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
        let result = null;

        function onsuccess(ev) {
            const cursor = ev.target.result;
            if (!cursor) {
                return;
            }
            const data = cursor.value;
            if (data.state != expectedState) {
                console.warn(
                    `Cannot update room key request from ${expectedState} ` +
                    `as it was already updated to ${data.state}`,
                );
                return;
            }
            Object.assign(data, updates);
            cursor.update(data);
            result = data;
        }

        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
                  .openCursor(requestId);
        cursorReq.onsuccess = onsuccess;
        return promiseifyTxn(txn).then(() => result);
    }

    /**
     * Look for an existing room key request by id and state, and delete it if
     * found
     *
     * @param {string} requestId      ID of request to update
     * @param {number} expectedState  state we expect to find the request in
     *
     * @returns {Promise} resolves once the operation is completed
     */
    deleteOutgoingRoomKeyRequest(requestId, expectedState) {
        const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
        const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
                  .openCursor(requestId);
        cursorReq.onsuccess = (ev) => {
            const cursor = ev.target.result;
            if (!cursor) {
                return;
            }
            const data = cursor.value;
            if (data.state != expectedState) {
                console.warn(
                    `Cannot delete room key request in state ${data.state} `
                        + `(expected ${expectedState})`,
                );
                return;
            }
            cursor.delete();
        };
        return promiseifyTxn(txn);
    }

    // Olm Account

    getAccount(txn, func) {
        const objectStore = txn.objectStore("account");
        const getReq = objectStore.get("-");
        getReq.onsuccess = function() {
            try {
                func(getReq.result || null);
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeAccount(txn, newData) {
        const objectStore = txn.objectStore("account");
        objectStore.put(newData, "-");
    }

    // Olm Sessions

    countEndToEndSessions(txn, func) {
        const objectStore = txn.objectStore("sessions");
        const countReq = objectStore.count();
        countReq.onsuccess = function() {
            func(countReq.result);
        };
    }

    getEndToEndSessions(deviceKey, txn, func) {
        const objectStore = txn.objectStore("sessions");
        const idx = objectStore.index("deviceKey");
        const getReq = idx.openCursor(deviceKey);
        const results = {};
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                results[cursor.value.sessionId] = cursor.value.session;
                cursor.continue();
            } else {
                try {
                    func(results);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    getEndToEndSession(deviceKey, sessionId, txn, func) {
        const objectStore = txn.objectStore("sessions");
        const getReq = objectStore.get([deviceKey, sessionId]);
        getReq.onsuccess = function() {
            try {
                if (getReq.result) {
                    func(getReq.result.session);
                } else {
                    func(null);
                }
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeEndToEndSession(deviceKey, sessionId, session, txn) {
        const objectStore = txn.objectStore("sessions");
        objectStore.put({deviceKey, sessionId, session});
    }

    // Inbound group sessions

    getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const getReq = objectStore.get([senderCurve25519Key, sessionId]);
        getReq.onsuccess = function() {
            try {
                if (getReq.result) {
                    func(getReq.result.session);
                } else {
                    func(null);
                }
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    getAllEndToEndInboundGroupSessions(txn, func) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const getReq = objectStore.openCursor();
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                try {
                    func({
                        senderKey: cursor.value.senderCurve25519Key,
                        sessionId: cursor.value.sessionId,
                        sessionData: cursor.value.session,
                    });
                } catch (e) {
                    abortWithException(txn, e);
                }
                cursor.continue();
            } else {
                try {
                    func(null);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        const addReq = objectStore.add({
            senderCurve25519Key, sessionId, session: sessionData,
        });
        addReq.onerror = () => {
            if (addReq.error.name === 'ConstraintError') {
                console.log(
                    "Ignoring duplicate inbound group session: " +
                    senderCurve25519Key + " / " + sessionId,
                );
            } else {
                abortWithException(txn, new Error(
                    "Failed to add inbound group session: " + addReq.error,
                ));
            }
        };
    }

    storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        const objectStore = txn.objectStore("inbound_group_sessions");
        objectStore.put({
            senderCurve25519Key, sessionId, session: sessionData,
        });
    }

    getEndToEndDeviceData(txn, func) {
        const objectStore = txn.objectStore("device_data");
        const getReq = objectStore.get("-");
        getReq.onsuccess = function() {
            try {
                func(getReq.result || null);
            } catch (e) {
                abortWithException(txn, e);
            }
        };
    }

    storeEndToEndDeviceData(deviceData, txn) {
        const objectStore = txn.objectStore("device_data");
        objectStore.put(deviceData, "-");
    }

    storeEndToEndRoom(roomId, roomInfo, txn) {
        const objectStore = txn.objectStore("rooms");
        objectStore.put(roomInfo, roomId);
    }

    getEndToEndRooms(txn, func) {
        const rooms = {};
        const objectStore = txn.objectStore("rooms");
        const getReq = objectStore.openCursor();
        getReq.onsuccess = function() {
            const cursor = getReq.result;
            if (cursor) {
                rooms[cursor.key] = cursor.value;
                cursor.continue();
            } else {
                try {
                    func(rooms);
                } catch (e) {
                    abortWithException(txn, e);
                }
            }
        };
    }

    doTxn(mode, stores, func) {
        const txn = this._db.transaction(stores, mode);
        const promise = promiseifyTxn(txn);
        const result = func(txn);
        return promise.then(() => {
            return result;
        });
    }
}

export function upgradeDatabase(db, oldVersion) {
    console.log(
        `Upgrading IndexedDBCryptoStore from version ${oldVersion}`
            + ` to ${VERSION}`,
    );
    if (oldVersion < 1) { // The database did not previously exist.
        createDatabase(db);
    }
    if (oldVersion < 2) {
        db.createObjectStore("account");
    }
    if (oldVersion < 3) {
        const sessionsStore = db.createObjectStore("sessions", {
            keyPath: ["deviceKey", "sessionId"],
        });
        sessionsStore.createIndex("deviceKey", "deviceKey");
    }
    if (oldVersion < 4) {
        db.createObjectStore("inbound_group_sessions", {
            keyPath: ["senderCurve25519Key", "sessionId"],
        });
    }
    if (oldVersion < 5) {
        db.createObjectStore("device_data");
    }
    if (oldVersion < 6) {
        db.createObjectStore("rooms");
    }
    // Expand as needed.
}

function createDatabase(db) {
    const outgoingRoomKeyRequestsStore =
        db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });

    // we assume that the RoomKeyRequestBody will have room_id and session_id
    // properties, to make the index efficient.
    outgoingRoomKeyRequestsStore.createIndex("session",
        ["requestBody.room_id", "requestBody.session_id"],
    );

    outgoingRoomKeyRequestsStore.createIndex("state", "state");
}

/*
 * Aborts a transaction with a given exception
 * The transaction promise will be rejected with this exception.
 */
function abortWithException(txn, e) {
    // We cheekily stick our exception onto the transaction object here
    // We could alternatively make the thing we pass back to the app
    // an object containing the transaction and exception.
    txn._mx_abortexception = e;
    try {
        txn.abort();
    } catch (e) {
        // sometimes we won't be able to abort the transaction
        // (ie. if it's aborted or completed)
    }
}

function promiseifyTxn(txn) {
    return new Promise((resolve, reject) => {
        txn.oncomplete = () => {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            resolve();
        };
        txn.onerror = () => {
            if (txn._mx_abortexception !== undefined) {
                reject(txn._mx_abortexception);
            }
            reject();
        };
        txn.onabort = () => reject(txn._mx_abortexception);
    });
}
<|endoftext|>


<commit_before>import { attempt, isError } from 'lodash';
import TestRepoBackend from "./test-repo/implementation";
import GitHubBackend from "./github/implementation";
import NetlifyAuthBackend from "./netlify-auth/implementation";
import { resolveFormat } from "../formats/formats";
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries, selectFolderEntryExtension } from "../reducers/collections";
import { createEntry } from "../valueObjects/Entry";
import slug from 'slug';

class LocalStorageAuthStore {
  storageKey = "netlify-cms-user";

  retrieve() {
    const data = window.localStorage.getItem(this.storageKey);
    return data && JSON.parse(data);
  }

  store(userData) {
    window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
  }

  logout() {
    window.localStorage.removeItem(this.storageKey);
  }
}

const slugFormatter = (template = "{{slug}}", entryData) => {
  const date = new Date();
  const identifier = entryData.get("title", entryData.get("path"));
  return template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => {
    switch (field) {
      case "year":
        return date.getFullYear();
      case "month":
        return (`0${ date.getMonth() + 1 }`).slice(-2);
      case "day":
        return (`0${ date.getDate() }`).slice(-2);
      case "slug":
        return slug(identifier.trim(), {lower: true});
      default:
        return slug(entryData.get(field, "").trim(), {lower: true});
    }
  });
};

class Backend {
  constructor(implementation, authStore = null) {
    this.implementation = implementation;
    this.authStore = authStore;
    if (this.implementation === null) {
      throw new Error("Cannot instantiate a Backend with no implementation");
    }
  }

  currentUser() {
    if (this.user) { return this.user; }
    const stored = this.authStore && this.authStore.retrieve();
    if (stored) {
      return Promise.resolve(this.implementation.setUser(stored)).then(() => stored);
    }
    return Promise.resolve(null);
  }

  authComponent() {
    return this.implementation.authComponent();
  }

  authenticate(credentials) {
    return this.implementation.authenticate(credentials).then((user) => {
      if (this.authStore) { this.authStore.store(user); }
      return user;
    });
  }

  logout() {
    if (this.authStore) {
      this.authStore.logout();
    } else {
      throw new Error("User isn't authenticated.");
    }
  }

  getToken = () => this.implementation.getToken();

  listEntries(collection) {
    const listMethod = this.implementation[selectListMethod(collection)];
    const extension = selectFolderEntryExtension(collection);
    const collectionFilter = collection.get('filter');
    return listMethod.call(this.implementation, collection, extension)
      .then(loadedEntries => (
        loadedEntries.map(loadedEntry => createEntry(
          collection.get("name"),
          selectEntrySlug(collection, loadedEntry.file.path),
          loadedEntry.file.path,
          { raw: loadedEntry.data || '', label: loadedEntry.file.label }
        ))
      ))
      .then(entries => (
        {
          entries: entries.map(this.entryWithFormat(collection)),
        }
      ))
      // If this collection has a "filter" property, filter entries accordingly
      .then(loadedCollection => (
        {
          entries: collectionFilter ? this.filterEntries(loadedCollection, collectionFilter) : loadedCollection.entries
        }
      ));
  }

  getEntry(collection, slug) {
    return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug))
      .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
        collection.get("name"),
        slug,
        loadedEntry.file.path,
        { raw: loadedEntry.data, label: loadedEntry.file.label }
      ))
    );
  }

  entryWithFormat(collectionOrEntity) {
    return (entry) => {
      const format = resolveFormat(collectionOrEntity, entry);
      if (entry && entry.raw !== undefined) {
        const data = (format && attempt(format.fromFile.bind(null, entry.raw))) || {};
        if (isError(data)) console.error(data);
        return Object.assign(entry, { data: isError(data) ? {} : data });
      }
      return format.fromFile(entry);
    };
  }

  unpublishedEntries(page, perPage) {
    return this.implementation.unpublishedEntries(page, perPage)
    .then(loadedEntries => loadedEntries.filter(entry => entry !== null))
    .then(entries => (
      entries.map((loadedEntry) => {
        const entry = createEntry(
          loadedEntry.metaData.collection,
          loadedEntry.slug,
          loadedEntry.file.path,
          {
            raw: loadedEntry.data,
            isModification: loadedEntry.isModification,
          }
        );
        entry.metaData = loadedEntry.metaData;
        return entry;
      })
    ))
    .then(entries => ({
      pagination: 0,
      entries: entries.map(this.entryWithFormat("editorialWorkflow")),
    }));
  }

  unpublishedEntry(collection, slug) {
    return this.implementation.unpublishedEntry(collection, slug)
    .then((loadedEntry) => {
      const entry = createEntry(
        "draft",
        loadedEntry.slug,
        loadedEntry.file.path,
        {
          raw: loadedEntry.data,
          isModification: loadedEntry.isModification,
        });
      entry.metaData = loadedEntry.metaData;
      return entry;
    })
    .then(this.entryWithFormat(collection, slug));
  }

  persistEntry(config, collection, entryDraft, MediaFiles, options) {
    const newEntry = entryDraft.getIn(["entry", "newRecord"]) || false;

    const parsedData = {
      title: entryDraft.getIn(["entry", "data", "title"], "No Title"),
      description: entryDraft.getIn(["entry", "data", "description"], "No Description!"),
    };

    const entryData = entryDraft.getIn(["entry", "data"]).toJS();
    let entryObj;
    if (newEntry) {
      if (!selectAllowNewEntries(collection)) {
        throw (new Error("Not allowed to create new entries in this collection"));
      }
      const slug = slugFormatter(collection.get("slug"), entryDraft.getIn(["entry", "data"]));
      const path = selectEntryPath(collection, slug);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get("entry")),
      };
    } else {
      const path = entryDraft.getIn(["entry", "path"]);
      const slug = entryDraft.getIn(["entry", "slug"]);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get("entry")),
      };
    }

    const commitMessage = `${ (newEntry ? "Create " : "Update ") +
          collection.get("label") } “${ entryObj.slug }”`;

    const mode = config.get("publish_mode");

    const collectionName = collection.get("name");

    return this.implementation.persistEntry(entryObj, MediaFiles, {
      newEntry, parsedData, commitMessage, collectionName, mode, ...options,
    });
  }

  persistUnpublishedEntry(config, collection, entryDraft, MediaFiles) {
    return this.persistEntry(config, collection, entryDraft, MediaFiles, { unpublished: true });
  }

  updateUnpublishedEntryStatus(collection, slug, newStatus) {
    return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
  }

  publishUnpublishedEntry(collection, slug) {
    return this.implementation.publishUnpublishedEntry(collection, slug);
  }

  deleteUnpublishedEntry(collection, slug) {
    return this.implementation.deleteUnpublishedEntry(collection, slug);
  }

  entryToRaw(collection, entry) {
    const format = resolveFormat(collection, entry.toJS());
    const fieldsOrder = this.fieldsOrder(collection, entry);
    return format && format.toFile(entry.get("data").toJS(), fieldsOrder);
  }

  fieldsOrder(collection, entry) {
    const fields = collection.get('fields');
    if (fields) {
      return collection.get('fields').map(f => f.get('name')).toArray();
    }

    const files = collection.get('files');
    const file = (files || []).filter(f => f.get("name") === entry.get("slug")).get(0);
    if (file == null) {
      throw new Error(`No file found for ${ entry.get("slug") } in ${ collection.get('name') }`);
    }
    return file.get('fields').map(f => f.get('name')).toArray();
  }

  filterEntries(collection, filterRule) {
    return collection.entries.filter(entry => (
      entry.data[filterRule.get('field')] === filterRule.get('value')
    ));
  }
}

export function resolveBackend(config) {
  const name = config.getIn(["backend", "name"]);
  if (name == null) {
    throw new Error("No backend defined in configuration");
  }

  const authStore = new LocalStorageAuthStore();

  switch (name) {
    case "test-repo":
      return new Backend(new TestRepoBackend(config), authStore);
    case "github":
      return new Backend(new GitHubBackend(config), authStore);
    case "netlify-auth":
      return new Backend(new NetlifyAuthBackend(config), authStore);
    default:
      throw new Error(`Backend not found: ${ name }`);
  }
}

export const currentBackend = (function () {
  let backend = null;

  return (config) => {
    if (backend) { return backend; }
    if (config.get("backend")) {
      return backend = resolveBackend(config);
    }
  };
}());
<commit_msg>Remove case sensitivity of names of fields used in slug construction<commit_after>import { attempt, isError } from 'lodash';
import TestRepoBackend from "./test-repo/implementation";
import GitHubBackend from "./github/implementation";
import NetlifyAuthBackend from "./netlify-auth/implementation";
import { resolveFormat } from "../formats/formats";
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries, selectFolderEntryExtension } from "../reducers/collections";
import { createEntry } from "../valueObjects/Entry";
import slug from 'slug';

class LocalStorageAuthStore {
  storageKey = "netlify-cms-user";

  retrieve() {
    const data = window.localStorage.getItem(this.storageKey);
    return data && JSON.parse(data);
  }

  store(userData) {
    window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
  }

  logout() {
    window.localStorage.removeItem(this.storageKey);
  }
}

const slugFormatter = (template = "{{slug}}", entryData) => {
  const date = new Date();

  const getIdentifier = (entryData) => {
	  const validIdentifierFields = ["title", "path"];
	  const identifiers = validIdentifierFields.map((field) => {
	  	return entryData.find((_, key) => {
	  		return key.toLowerCase() === field;
	  	});
	  });

	  const identifier = identifiers.find(i => typeof i !== 'undefined');

	  if (typeof identifier === 'undefined') {
	      throw new Error("Collection must have a field name that is a valid entry identifier");
	  } else {
	  	return identifier;
	  }
  };

  return template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => {
    switch (field) {
      case "year":
        return date.getFullYear();
      case "month":
        return (`0${ date.getMonth() + 1 }`).slice(-2);
      case "day":
        return (`0${ date.getDate() }`).slice(-2);
      case "slug":
        return slug(getIdentifier(entryData).trim(), {lower: true});
      default:
        return slug(entryData.get(field, "").trim(), {lower: true});
    }
  });
};

class Backend {
  constructor(implementation, authStore = null) {
    this.implementation = implementation;
    this.authStore = authStore;
    if (this.implementation === null) {
      throw new Error("Cannot instantiate a Backend with no implementation");
    }
  }

  currentUser() {
    if (this.user) { return this.user; }
    const stored = this.authStore && this.authStore.retrieve();
    if (stored) {
      return Promise.resolve(this.implementation.setUser(stored)).then(() => stored);
    }
    return Promise.resolve(null);
  }

  authComponent() {
    return this.implementation.authComponent();
  }

  authenticate(credentials) {
    return this.implementation.authenticate(credentials).then((user) => {
      if (this.authStore) { this.authStore.store(user); }
      return user;
    });
  }

  logout() {
    if (this.authStore) {
      this.authStore.logout();
    } else {
      throw new Error("User isn't authenticated.");
    }
  }

  getToken = () => this.implementation.getToken();

  listEntries(collection) {
    const listMethod = this.implementation[selectListMethod(collection)];
    const extension = selectFolderEntryExtension(collection);
    const collectionFilter = collection.get('filter');
    return listMethod.call(this.implementation, collection, extension)
      .then(loadedEntries => (
        loadedEntries.map(loadedEntry => createEntry(
          collection.get("name"),
          selectEntrySlug(collection, loadedEntry.file.path),
          loadedEntry.file.path,
          { raw: loadedEntry.data || '', label: loadedEntry.file.label }
        ))
      ))
      .then(entries => (
        {
          entries: entries.map(this.entryWithFormat(collection)),
        }
      ))
      // If this collection has a "filter" property, filter entries accordingly
      .then(loadedCollection => (
        {
          entries: collectionFilter ? this.filterEntries(loadedCollection, collectionFilter) : loadedCollection.entries
        }
      ));
  }

  getEntry(collection, slug) {
    return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug))
      .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
        collection.get("name"),
        slug,
        loadedEntry.file.path,
        { raw: loadedEntry.data, label: loadedEntry.file.label }
      ))
    );
  }

  entryWithFormat(collectionOrEntity) {
    return (entry) => {
      const format = resolveFormat(collectionOrEntity, entry);
      if (entry && entry.raw !== undefined) {
        const data = (format && attempt(format.fromFile.bind(null, entry.raw))) || {};
        if (isError(data)) console.error(data);
        return Object.assign(entry, { data: isError(data) ? {} : data });
      }
      return format.fromFile(entry);
    };
  }

  unpublishedEntries(page, perPage) {
    return this.implementation.unpublishedEntries(page, perPage)
    .then(loadedEntries => loadedEntries.filter(entry => entry !== null))
    .then(entries => (
      entries.map((loadedEntry) => {
        const entry = createEntry(
          loadedEntry.metaData.collection,
          loadedEntry.slug,
          loadedEntry.file.path,
          {
            raw: loadedEntry.data,
            isModification: loadedEntry.isModification,
          }
        );
        entry.metaData = loadedEntry.metaData;
        return entry;
      })
    ))
    .then(entries => ({
      pagination: 0,
      entries: entries.map(this.entryWithFormat("editorialWorkflow")),
    }));
  }

  unpublishedEntry(collection, slug) {
    return this.implementation.unpublishedEntry(collection, slug)
    .then((loadedEntry) => {
      const entry = createEntry(
        "draft",
        loadedEntry.slug,
        loadedEntry.file.path,
        {
          raw: loadedEntry.data,
          isModification: loadedEntry.isModification,
        });
      entry.metaData = loadedEntry.metaData;
      return entry;
    })
    .then(this.entryWithFormat(collection, slug));
  }

  persistEntry(config, collection, entryDraft, MediaFiles, options) {
    const newEntry = entryDraft.getIn(["entry", "newRecord"]) || false;

    const parsedData = {
      title: entryDraft.getIn(["entry", "data", "title"], "No Title"),
      description: entryDraft.getIn(["entry", "data", "description"], "No Description!"),
    };

    const entryData = entryDraft.getIn(["entry", "data"]).toJS();
    let entryObj;
    if (newEntry) {
      if (!selectAllowNewEntries(collection)) {
        throw (new Error("Not allowed to create new entries in this collection"));
      }
      const slug = slugFormatter(collection.get("slug"), entryDraft.getIn(["entry", "data"]));
      const path = selectEntryPath(collection, slug);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get("entry")),
      };
    } else {
      const path = entryDraft.getIn(["entry", "path"]);
      const slug = entryDraft.getIn(["entry", "slug"]);
      entryObj = {
        path,
        slug,
        raw: this.entryToRaw(collection, entryDraft.get("entry")),
      };
    }

    const commitMessage = `${ (newEntry ? "Create " : "Update ") +
          collection.get("label") } “${ entryObj.slug }”`;

    const mode = config.get("publish_mode");

    const collectionName = collection.get("name");

    return this.implementation.persistEntry(entryObj, MediaFiles, {
      newEntry, parsedData, commitMessage, collectionName, mode, ...options,
    });
  }

  persistUnpublishedEntry(config, collection, entryDraft, MediaFiles) {
    return this.persistEntry(config, collection, entryDraft, MediaFiles, { unpublished: true });
  }

  updateUnpublishedEntryStatus(collection, slug, newStatus) {
    return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
  }

  publishUnpublishedEntry(collection, slug) {
    return this.implementation.publishUnpublishedEntry(collection, slug);
  }

  deleteUnpublishedEntry(collection, slug) {
    return this.implementation.deleteUnpublishedEntry(collection, slug);
  }

  entryToRaw(collection, entry) {
    const format = resolveFormat(collection, entry.toJS());
    const fieldsOrder = this.fieldsOrder(collection, entry);
    return format && format.toFile(entry.get("data").toJS(), fieldsOrder);
  }

  fieldsOrder(collection, entry) {
    const fields = collection.get('fields');
    if (fields) {
      return collection.get('fields').map(f => f.get('name')).toArray();
    }

    const files = collection.get('files');
    const file = (files || []).filter(f => f.get("name") === entry.get("slug")).get(0);
    if (file == null) {
      throw new Error(`No file found for ${ entry.get("slug") } in ${ collection.get('name') }`);
    }
    return file.get('fields').map(f => f.get('name')).toArray();
  }

  filterEntries(collection, filterRule) {
    return collection.entries.filter(entry => (
      entry.data[filterRule.get('field')] === filterRule.get('value')
    ));
  }
}

export function resolveBackend(config) {
  const name = config.getIn(["backend", "name"]);
  if (name == null) {
    throw new Error("No backend defined in configuration");
  }

  const authStore = new LocalStorageAuthStore();

  switch (name) {
    case "test-repo":
      return new Backend(new TestRepoBackend(config), authStore);
    case "github":
      return new Backend(new GitHubBackend(config), authStore);
    case "netlify-auth":
      return new Backend(new NetlifyAuthBackend(config), authStore);
    default:
      throw new Error(`Backend not found: ${ name }`);
  }
}

export const currentBackend = (function () {
  let backend = null;

  return (config) => {
    if (backend) { return backend; }
    if (config.get("backend")) {
      return backend = resolveBackend(config);
    }
  };
}());
<|endoftext|>


<commit_before>/**
 * Tangram
 * Copyright 2009 Baidu Inc. All rights reserved.
 */

///import baidu.ui.createUI;
///import baidu.ui.behavior.posable.setPositionByElement;
///import baidu.ui.behavior.posable.setPositionByMouse;

///import baidu.object.extend;
///import baidu.dom.g;
///import baidu.dom.setStyles;
///import baidu.dom.remove;
///import baidu.string.format;
///import baidu.dom.insertHTML;
///import baidu.lang.toArray;
///import baidu.dom.children;
///import baidu.object.each;
///import baidu.array.each;
///import baidu.dom.getAttr;
///import baidu.dom.setAttr;

 /**
 * 弹出tip层,类似鼠标划过含title属性元素的效果
 * @class
 * @param       {Object}          options         选项.
 * @config      {String|Array}    target          目标元素或元素id。可直接设置多个目标元素
 * @config      {String}          type            （可选）触发展开的类型，可以为:hover和click。默认为click
 * @config      {Element}         contentElement  （可选）Tooltip元素的内部HTMLElement。
 * @config      {String}          content         （可选）Tooltip元素的内部HTML String。若target存在title，则以title为准
 * @config      {String}          width           （可选）宽度
 * @config      {String}          height          （可选）高度
 * @config      {Array|Object}    offset          （可选）偏移量。若为数组，索引0为x方向，索引1为y方向；若为Object，键x为x方向，键y为y方向。单位：px，默认值：[0,0]。
 * @config      {boolean}         single          （可选）是否全局单例。若该值为true，则全局共用唯一的浮起tooltip元素，默认为true。
 * @config      {Number}          zIndex          （可选）浮起tooltip层的z-index值，默认为3000。
 * @config      {String}          positionBy      （可选）浮起tooltip层的位置参考，取值['mouse','element']，分别对应针对鼠标位置或者element元素计算偏移，默认mouse
 * @config      {Element}         positionElement （可选）定位元素，设置此元素且positionBy为element时，根据改元素计算位置
 * @config      {Boolean}         autoRender       是否自动渲染。
 * @config      {Function}        onopen          （可选）打开tooltip时触发。
 * @config      {Function}        onclose         （可选）关闭tooltip时触发。
 * @config      {Function}        onbeforeopen    （可选）打开tooltip前触发。
 * @config      {Function}        onbeforeclose   （可选）关闭tooltip前触发。
 * @plugin      fx                Tooltip的展现和消失效果支持。
 * @return     {baidu.ui.Tooltip}        Tooltip实例
 */

baidu.ui.Tooltip = baidu.ui.createUI(function(options) {

    var me = this;
    me.target = me.getTarget();
    me.offset = options.offset || [0, 0];
    me.positionElement = null;

    baidu.ui.Tooltip.showing[me.guid] = me;

}).extend(
    /**
     *  @lends baidu.ui.Tooltip.prototype
     */
{
    uiType: 'tooltip',

    width: '',
    height: '',
    zIndex: 3000,
    currentTarget: null,

    type: 'click',

    posable: true,
    positionBy: 'element',
	offsetPosition: 'bottomright',

    isShowing: false,

    tplBody: '

',

    /**
     * 获取Tooltip的HTML字符串
     * @private
     * @return {String} TooltipHtml
     */
    getString: function() {
		var me = this;
		return baidu.format(me.tplBody, {
			id: me.getId(),
			'class' : me.getClass()
		});
	},

    /**
	 * 开关函数,返回false时不显示
     * @private
     */
	toggle: function() {return true},

    /**
     * 渲染Tooltip到HTML
     * @public
     */
    render: function() {
        var me = this,
            main,title;

        main = me.renderMain();

        baidu.each(me.target, function(t,index){
            if((title = baidu.getAttr(t, 'title')) && title != ''){
                baidu.setAttr(t, 'tangram-tooltip-title', title);
                baidu.setAttr(t, 'title', '');
            }
        });
        baidu.dom.insertHTML(main,"beforeend",me.getString());
        me._update();
        me._close();

        me.dispatchEvent('onload');
    },

	/**
	 * 打开tooltip
	 * @public
     * @param {HTMLElement} [target] 显示tooltip所参照的html元素
	 */
	open: function(target) {
		var me = this,
            showTooltip = baidu.ui.Tooltip.showing,
            isSingleton = baidu.ui.Tooltip.isSingleton,
            target = target || me.target[0],
            currentTarget = me.currentTarget,
            body = me.getBody();

         //判断是否为当前打开tooltip的target
         //若是，则直接返回
        if(currentTarget === target) return;

        //若target为本组中之一，则关闭当前current
        me.isShowing && me.close(currentTarget);

        //查看当前tooltip全局设置,若为单例，关闭当前打开的tooltip
        if(isSingleton){
            baidu.object.each(showTooltip,function(tooltip,key){
                if(key != me.guid && tooltip.isShowing){
                    tooltip.close();
                }
            });
        }

        //若toggle函数返回false，则直接返回
        if (typeof me.toggle == 'function' && !me.toggle()) return;

        me.currentTarget = target;

        me._updateBodyByTitle();
        me._setPosition();
        me.isShowing = true;

        //若onbeforeopen事件返回值为false，则直接返回
        if (me.dispatchEvent('onbeforeopen')){
            me.dispatchEvent('open');
            return;
        }
	},

    _updateBody: function(options){
        var me = this,
            options = options || {},
            body = me.getBody(),
            title;

        if(me.contentElement && me.contentElement !== body.firstChild){

            //若存在me.content 并且该content和content里面的firstChild不一样
            body.innerHTML = '';
            body.appendChild(me.contentElement);
            me.contentElement = body.firstChild;

        }else if(typeof options.contentElement != 'undefined'){

            //若options.content存在，则认为用户向对content进行更新
            body.innerHTML = '';
            options.contentElement != null && body.appendChild(options.contentElement);

        }

        if(!options.contentElement){
            if(typeof options.content == 'string'){

                //若存在options.contentText，则认为用户相对contentText进行更新
                body.innerHTML = '';
                body.innerHTML = options.content;

            }else if(typeof me.content == 'string' && baidu.dom.children(body).length == 0 ) {
                //第一次new Tooltip时传入contentText，进行渲染
                body.innerHTML = me.content;
            }
        }
    },

    _updateBodyByTitle:function(){
        var me = this,
            body = me.getBody();

        if(!me.contentElement && !me.content && me.currentTarget){
            if((title = baidu.getAttr(me.currentTarget, 'tangram-tooltip-title')) && title != ''){
                body.innerHTML = title;
            }else{
                body.innerHTML = '';
            }
        }

    },

    /**
     * 更新tooltip属性值
     * @private
     * @param {Object} options 属性集合
     */
    _update: function(options){
        var me = this,
            options = options || {},
            main = me.getMain(),
            body = me.getBody();

        me._updateBody(options);
        baidu.object.extend(me, options);
        me.contentElement = baidu.dom.children(body).length > 0 ? body.firstChild : null;
        me._updateBodyByTitle();

        //更新寛高数据
        baidu.dom.setStyles(main, {
            zIndex: me.zIndex,
            width: me.width,
            height: me.height,
            // 防止插件更改display属性,比如fx.
            display: ''
        });
    },

    /**
     * 更新options
     * @public
     * @param       {Object}          options         选项.
     * @config      {String|Array}    target          目标元素或元素id。可直接设置多个目标元素
     * @config      {String}          type            （可选）触发展开的类型，可以为:hover和click。默认为click
     * @config      {Element}         contentElement  （可选）Tooltip元素的内部HTMLElement。
     * @config      {String}          content         （可选）Tooltip元素的内部HTML String。若target存在title，则以title为准
     * @config      {String}          width           （可选）宽度
     * @config      {String}          height          （可选）高度
     * @config      {Array|Object}    offset          （可选）偏移量。若为数组，索引0为x方向，索引1为y方向；若为Object，键x为x方向，键y为y方向。单位：px，默认值：[0,0]。
     * @config      {boolean}         single          （可选）是否全局单例。若该值为true，则全局共用唯一的浮起tooltip元素，默认为true。
     * @config      {Number}          zIndex          （可选）浮起tooltip层的z-index值，默认为3000。
     * @config      {String}          positionBy      （可选）浮起tooltip层的位置参考，取值['mouse','element']，分别对应针对鼠标位置或者element元素计算偏移，默认mouse。
     * @config      {Element}         positionElement （可选）定位元素，设置此元素且positionBy为element时，根据改元素计算位置
     * @config      {Boolean}         autoRender       是否自动渲染。
     * @config      {Function}        onopen          （可选）打开tooltip时触发。
     * @config      {Function}        onclose         （可选）关闭tooltip时触发。
     * @config      {Function}        onbeforeopen    （可选）打开tooltip前触发。
     * @config      {Function}        onbeforeclose   （可选）关闭tooltip前触发。
     */
    update: function(options){
        var me = this;
        me._update(options);
        me._setPosition();
        me.dispatchEvent('onupdate');
    },

    /**
     * 设置position
     * @private
     */
	_setPosition: function() {
		var me = this,
            insideScreen = typeof me.insideScreen == 'string' ? me.insideScreen : 'surround',
			positionOptions = {
				once: true,
				offset: me.offset,
				position: me.offsetPosition,
				insideScreen: insideScreen
			};
		switch (me.positionBy) {
			case 'element':
				me.setPositionByElement(me.positionElement || me.currentTarget, me.getMain(), positionOptions);
				break;
			case 'mouse':
				me.setPositionByMouse(me.getMain(), positionOptions);
				break;
			default :
				break;
		}
	},

	/**
	 * 关闭tooltip
	 * @public
	 */
	close: function() {
		var me = this;

        if(!me.isShowing) return;

        me.isShowing = false;
        if(me.dispatchEvent('onbeforeclose')){
            me._close();
            me.dispatchEvent('onclose');
        }
        me.currentTarget = null;
    },

    _close: function() {
        var me = this;

        me.getMain().style.left = '-100000px';
    },
	/**
	 * 销毁控件
	 * @public
	 */
	dispose: function() {
		var me = this;
		me.dispatchEvent('ondispose');
		if (me.getBody()) {
			baidu.dom.remove(me.getBody());
		}
        delete(baidu.ui.Tooltip.showing[me.guid]);
		baidu.lang.Class.prototype.dispose.call(me);
	},
    /**
     * 获取target元素
	 * @private
	 */
    getTarget: function() {
        var me = this,
            target = [];

        baidu.each(baidu.lang.toArray(me.target),function(item){
            target.push(baidu.G(item));
        });

        return target;
    }
});

baidu.ui.Tooltip.isSingleton = false;
baidu.ui.Tooltip.showing = {};
<commit_msg>删除main元素<commit_after>/**
 * Tangram
 * Copyright 2009 Baidu Inc. All rights reserved.
 */

///import baidu.ui.createUI;
///import baidu.ui.behavior.posable.setPositionByElement;
///import baidu.ui.behavior.posable.setPositionByMouse;

///import baidu.object.extend;
///import baidu.dom.g;
///import baidu.dom.setStyles;
///import baidu.dom.remove;
///import baidu.string.format;
///import baidu.dom.insertHTML;
///import baidu.lang.toArray;
///import baidu.dom.children;
///import baidu.object.each;
///import baidu.array.each;
///import baidu.dom.getAttr;
///import baidu.dom.setAttr;

 /**
 * 弹出tip层,类似鼠标划过含title属性元素的效果
 * @class
 * @param       {Object}          options         选项.
 * @config      {String|Array}    target          目标元素或元素id。可直接设置多个目标元素
 * @config      {String}          type            （可选）触发展开的类型，可以为:hover和click。默认为click
 * @config      {Element}         contentElement  （可选）Tooltip元素的内部HTMLElement。
 * @config      {String}          content         （可选）Tooltip元素的内部HTML String。若target存在title，则以title为准
 * @config      {String}          width           （可选）宽度
 * @config      {String}          height          （可选）高度
 * @config      {Array|Object}    offset          （可选）偏移量。若为数组，索引0为x方向，索引1为y方向；若为Object，键x为x方向，键y为y方向。单位：px，默认值：[0,0]。
 * @config      {boolean}         single          （可选）是否全局单例。若该值为true，则全局共用唯一的浮起tooltip元素，默认为true。
 * @config      {Number}          zIndex          （可选）浮起tooltip层的z-index值，默认为3000。
 * @config      {String}          positionBy      （可选）浮起tooltip层的位置参考，取值['mouse','element']，分别对应针对鼠标位置或者element元素计算偏移，默认mouse
 * @config      {Element}         positionElement （可选）定位元素，设置此元素且positionBy为element时，根据改元素计算位置
 * @config      {Boolean}         autoRender       是否自动渲染。
 * @config      {Function}        onopen          （可选）打开tooltip时触发。
 * @config      {Function}        onclose         （可选）关闭tooltip时触发。
 * @config      {Function}        onbeforeopen    （可选）打开tooltip前触发。
 * @config      {Function}        onbeforeclose   （可选）关闭tooltip前触发。
 * @plugin      fx                Tooltip的展现和消失效果支持。
 * @return     {baidu.ui.Tooltip}        Tooltip实例
 */

baidu.ui.Tooltip = baidu.ui.createUI(function(options) {

    var me = this;
    me.target = me.getTarget();
    me.offset = options.offset || [0, 0];
    me.positionElement = null;

    baidu.ui.Tooltip.showing[me.guid] = me;

}).extend(
    /**
     *  @lends baidu.ui.Tooltip.prototype
     */
{
    uiType: 'tooltip',

    width: '',
    height: '',
    zIndex: 3000,
    currentTarget: null,

    type: 'click',

    posable: true,
    positionBy: 'element',
	offsetPosition: 'bottomright',

    isShowing: false,

    tplBody: '

',

    /**
     * 获取Tooltip的HTML字符串
     * @private
     * @return {String} TooltipHtml
     */
    getString: function() {
		var me = this;
		return baidu.format(me.tplBody, {
			id: me.getId(),
			'class' : me.getClass()
		});
	},

    /**
	 * 开关函数,返回false时不显示
     * @private
     */
	toggle: function() {return true},

    /**
     * 渲染Tooltip到HTML
     * @public
     */
    render: function() {
        var me = this,
            main,title;

        main = me.renderMain();

        baidu.each(me.target, function(t,index){
            if((title = baidu.getAttr(t, 'title')) && title != ''){
                baidu.setAttr(t, 'tangram-tooltip-title', title);
                baidu.setAttr(t, 'title', '');
            }
        });
        baidu.dom.insertHTML(main,"beforeend",me.getString());
        me._update();
        me._close();

        me.dispatchEvent('onload');
    },

	/**
	 * 打开tooltip
	 * @public
     * @param {HTMLElement} [target] 显示tooltip所参照的html元素
	 */
	open: function(target) {
		var me = this,
            showTooltip = baidu.ui.Tooltip.showing,
            isSingleton = baidu.ui.Tooltip.isSingleton,
            target = target || me.target[0],
            currentTarget = me.currentTarget,
            body = me.getBody();

         //判断是否为当前打开tooltip的target
         //若是，则直接返回
        if(currentTarget === target) return;

        //若target为本组中之一，则关闭当前current
        me.isShowing && me.close(currentTarget);

        //查看当前tooltip全局设置,若为单例，关闭当前打开的tooltip
        if(isSingleton){
            baidu.object.each(showTooltip,function(tooltip,key){
                if(key != me.guid && tooltip.isShowing){
                    tooltip.close();
                }
            });
        }

        //若toggle函数返回false，则直接返回
        if (typeof me.toggle == 'function' && !me.toggle()) return;

        me.currentTarget = target;

        me._updateBodyByTitle();
        me._setPosition();
        me.isShowing = true;

        //若onbeforeopen事件返回值为false，则直接返回
        if (me.dispatchEvent('onbeforeopen')){
            me.dispatchEvent('open');
            return;
        }
	},

    _updateBody: function(options){
        var me = this,
            options = options || {},
            body = me.getBody(),
            title;

        if(me.contentElement && me.contentElement !== body.firstChild){

            //若存在me.content 并且该content和content里面的firstChild不一样
            body.innerHTML = '';
            body.appendChild(me.contentElement);
            me.contentElement = body.firstChild;

        }else if(typeof options.contentElement != 'undefined'){

            //若options.content存在，则认为用户向对content进行更新
            body.innerHTML = '';
            options.contentElement != null && body.appendChild(options.contentElement);

        }

        if(!options.contentElement){
            if(typeof options.content == 'string'){

                //若存在options.contentText，则认为用户相对contentText进行更新
                body.innerHTML = '';
                body.innerHTML = options.content;

            }else if(typeof me.content == 'string' && baidu.dom.children(body).length == 0 ) {
                //第一次new Tooltip时传入contentText，进行渲染
                body.innerHTML = me.content;
            }
        }
    },

    _updateBodyByTitle:function(){
        var me = this,
            body = me.getBody();

        if(!me.contentElement && !me.content && me.currentTarget){
            if((title = baidu.getAttr(me.currentTarget, 'tangram-tooltip-title')) && title != ''){
                body.innerHTML = title;
            }else{
                body.innerHTML = '';
            }
        }

    },

    /**
     * 更新tooltip属性值
     * @private
     * @param {Object} options 属性集合
     */
    _update: function(options){
        var me = this,
            options = options || {},
            main = me.getMain(),
            body = me.getBody();

        me._updateBody(options);
        baidu.object.extend(me, options);
        me.contentElement = baidu.dom.children(body).length > 0 ? body.firstChild : null;
        me._updateBodyByTitle();

        //更新寛高数据
        baidu.dom.setStyles(main, {
            zIndex: me.zIndex,
            width: me.width,
            height: me.height,
            // 防止插件更改display属性,比如fx.
            display: ''
        });
    },

    /**
     * 更新options
     * @public
     * @param       {Object}          options         选项.
     * @config      {String|Array}    target          目标元素或元素id。可直接设置多个目标元素
     * @config      {String}          type            （可选）触发展开的类型，可以为:hover和click。默认为click
     * @config      {Element}         contentElement  （可选）Tooltip元素的内部HTMLElement。
     * @config      {String}          content         （可选）Tooltip元素的内部HTML String。若target存在title，则以title为准
     * @config      {String}          width           （可选）宽度
     * @config      {String}          height          （可选）高度
     * @config      {Array|Object}    offset          （可选）偏移量。若为数组，索引0为x方向，索引1为y方向；若为Object，键x为x方向，键y为y方向。单位：px，默认值：[0,0]。
     * @config      {boolean}         single          （可选）是否全局单例。若该值为true，则全局共用唯一的浮起tooltip元素，默认为true。
     * @config      {Number}          zIndex          （可选）浮起tooltip层的z-index值，默认为3000。
     * @config      {String}          positionBy      （可选）浮起tooltip层的位置参考，取值['mouse','element']，分别对应针对鼠标位置或者element元素计算偏移，默认mouse。
     * @config      {Element}         positionElement （可选）定位元素，设置此元素且positionBy为element时，根据改元素计算位置
     * @config      {Boolean}         autoRender       是否自动渲染。
     * @config      {Function}        onopen          （可选）打开tooltip时触发。
     * @config      {Function}        onclose         （可选）关闭tooltip时触发。
     * @config      {Function}        onbeforeopen    （可选）打开tooltip前触发。
     * @config      {Function}        onbeforeclose   （可选）关闭tooltip前触发。
     */
    update: function(options){
        var me = this;
        me._update(options);
        me._setPosition();
        me.dispatchEvent('onupdate');
    },

    /**
     * 设置position
     * @private
     */
	_setPosition: function() {
		var me = this,
            insideScreen = typeof me.insideScreen == 'string' ? me.insideScreen : 'surround',
			positionOptions = {
				once: true,
				offset: me.offset,
				position: me.offsetPosition,
				insideScreen: insideScreen
			};
		switch (me.positionBy) {
			case 'element':
				me.setPositionByElement(me.positionElement || me.currentTarget, me.getMain(), positionOptions);
				break;
			case 'mouse':
				me.setPositionByMouse(me.getMain(), positionOptions);
				break;
			default :
				break;
		}
	},

	/**
	 * 关闭tooltip
	 * @public
	 */
	close: function() {
		var me = this;

        if(!me.isShowing) return;

        me.isShowing = false;
        if(me.dispatchEvent('onbeforeclose')){
            me._close();
            me.dispatchEvent('onclose');
        }
        me.currentTarget = null;
    },

    _close: function() {
        var me = this;

        me.getMain().style.left = '-100000px';
    },
	/**
	 * 销毁控件
	 * @public
	 */
	dispose: function() {
		var me = this;
		me.dispatchEvent('ondispose');
		if (me.getMain()) {
			baidu.dom.remove(me.getMain());
		}
        delete(baidu.ui.Tooltip.showing[me.guid]);
		baidu.lang.Class.prototype.dispose.call(me);
	},
    /**
     * 获取target元素
	 * @private
	 */
    getTarget: function() {
        var me = this,
            target = [];

        baidu.each(baidu.lang.toArray(me.target),function(item){
            target.push(baidu.G(item));
        });

        return target;
    }
});

baidu.ui.Tooltip.isSingleton = false;
baidu.ui.Tooltip.showing = {};
<|endoftext|>


<commit_before>Com['Gallery'] = function(o){
    var that = this,
        config = cm.merge({
            'container' : cm.Node('div'),
            'node' : cm.Node('div'),
            'data' : [],
            'duration' : 500,
            'showCaption' : true,
            'showArrowTitles' : false,
            'nodes' : {},
            'events' : {},
            'langs' : {
                'next' : 'Next',
                'prev' : 'Previous'
            }
        }, o),
        API = {
            'onSet' : [],
            'onChange' : [],
            'onItemLoad' : [],
            'onItemSet' : []
        },
        nodes = {
            'items' : []
        },
        items = [],
        anim = {},
        active;

    var init = function(){
        convertEvents(config['events']);
        getConfig(config['node']);
        getNodes(config['node']);
        render();
        // Collect items
        cm.forEach(nodes['items'], collectItem);
        // Process config items
        cm.forEach(config['items'], processItem);
    };

    var render = function(){
        // Structure
        nodes['container'] = cm.Node('div', {'class' : 'com-gallery'},
            nodes['holder'] = cm.Node('div', {'class' : 'holder'}),
            nodes['bar'] = cm.Node('div', {'class' : 'bar'},
                nodes['next'] = cm.Node('div', {'class' : 'bar-arrow next'},
                    cm.Node('div', {'class' : 'icon'})
                ),
                nodes['prev'] = cm.Node('div', {'class' : 'bar-arrow prev'},
                    cm.Node('div', {'class' : 'icon'})
                )
            ),
            nodes['loader'] = cm.Node('div', {'class' : 'loader'},
                cm.Node('div', {'class' : 'bg'}),
                cm.Node('div', {'class' : 'icon small loader centered'})
            )
        );
        // Arrow titles
        if(config['showArrowTitles']){
            nodes['next'].setAttribute('title', lang('next'));
            nodes['prev'].setAttribute('title', lang('prev'));
        }
        // Set events
        cm.addEvent(nodes['next'], 'click', next);
        cm.addEvent(nodes['prev'], 'click', prev);
        // Init animation
        anim['loader'] = new cm.Animation(nodes['loader']);
        // Embed
        config['container'].appendChild(nodes['container']);
    };

    var collectItem = function(item, i){
        if(!item['link']){
            item['link'] = cm.Node('a')
        }
        item = cm.merge({
            'src' : item['link'].getAttribute('href') || '',
            'title' : item['link'].getAttribute('title') || ''
        }, item);
        processItem(item, i);
    };

    var processItem = function(item, i){
        item = cm.merge({
            'index' : i,
            'isLoad' : false,
            'type' : 'image',        // image | video
            'nodes' : {}
        }, item);
        /// Check type
        item['type'] = /(\.jpg|\.png|\.gif|\.jpeg|\.bmp|\.tga)$/gi.test(item['src']) ? 'image' : 'video';
        // Structure
        item['nodes']['container'] = cm.Node('div', {'class' : 'cm-imagebox is-no-hover is-centered'},
            item['nodes']['inner'] = cm.Node('div', {'class' : 'inner'})
        );
        // Render by type
        switch(item['type']){
            case 'image':
                item['nodes']['inner'].appendChild(
                    item['nodes']['content'] = cm.Node('img', {'class' : 'descr', 'alt' : item['title']})
                );
                // Caption
                if(config['showCaption'] && !cm.isEmpty(item['title'])){
                    item['nodes']['inner'].appendChild(
                        cm.Node('div', {'class' : 'title'},
                            cm.Node('div', {'class' : 'inner'}, item['title'])
                        )
                    );
                }
                break;

            default:
                item['nodes']['inner'].appendChild(
                    item['nodes']['content'] = cm.Node('iframe', {'class' : 'descr'})
                );
                break;
        }
        // Init animation
        item['anim'] = new cm.Animation(item['nodes']['container']);
        // Set image on thumb click
        cm.addEvent(item['link'], 'click', function(e){
            e = cm.getEvent(e);
            cm.preventDefault(e);
            set(item['index']);
        }, true, true);
        // Push item to array
        items.push(item);
    };

    var set = function(i){
        var item = items[i],
            itemOld = items[active];
        // API onSet
        executeEvent('onSet', {
            'current' : item,
            'previous' : itemOld
        });
        // If current active item not equal new item - process with new item, else redraw window alignment and dimensions
        if(i != active){
            // API onSet
            executeEvent('onChange', {
                'current' : item,
                'previous' : itemOld
            });
            // Check type
            if(item['type'] == 'image'){
                cm.removeClass(nodes['container'], 'has-iframe');
            }else{
                cm.addClass(nodes['container'], 'has-iframe');
                nodes['holder'].appendChild(item['nodes']['container']);
            }
            // Check is content load
            if(!item['isLoad'] || item['type'] != 'image'){
                // Show loader
                nodes['loader'].style.display = 'block';
                anim['loader'].go({'style' : {'opacity' : 1}, 'anim' : 'smooth', 'duration' : config['duration']});
                // Add image load event and src
                cm.addEvent(item['nodes']['content'], 'load', function(){
                    // Hide loader
                    anim['loader'].go({'style' : {'opacity' : 0}, 'anim' : 'smooth', 'duration' : config['duration'], 'onStop' : function(){
                        nodes['loader'].style.display = 'none';
                    }});
                    // Set and show image
                    setImage(i, item, itemOld);
                });
                item['nodes']['content'].src = item['src'];
            }else{
                // Set and show image
                setImage(i, item, itemOld);
            }
        }
    };

    var setImage = function(i, item, itemOld){
        // Set new active
        active = i;
        item['isLoad'] = true;
        // API onImageSetStart
        executeEvent('onItemLoad', item);
        // Embed item content
        if(item['type'] == 'image'){
            nodes['holder'].appendChild(item['nodes']['container']);
        }
        // Animate Slide
        item['anim'].go({'style' : {'opacity' : 1}, 'anim' : 'smooth', 'duration' : config['duration'], 'onStop' : function(){
            // Remove old item
            if(itemOld){
                cm.setOpacity(itemOld['nodes']['container'], 0);
                cm.remove(itemOld['nodes']['container']);
            }
            // API onImageSet event
            executeEvent('onItemSet', item);
        }});
    };

    var next = function(){
        set((active == items.length - 1)? 0 : active + 1);
    };

    var prev = function(){
        set((active == 0)? items.length - 1 : active - 1);
    };

    /* *** MISC FUNCTIONS *** */

    var convertEvents = function(o){
        cm.forEach(o, function(item, key){
            if(API[key] && typeof item == 'function'){
                API[key].push(item);
            }
        });
    };

    var lang = function(str){
        if(!config['langs'][str]){
            config['langs'][str] = str;
        }
        return config['langs'][str];
    };

    var getNodes = function(container, marker){
        if(container){
            var sourceNodes = {};
            if(marker){
                sourceNodes = cm.getNodes(container)[marker] || {};
            }else{
                sourceNodes = cm.getNodes(container);
            }
            nodes = cm.merge(nodes, sourceNodes);
        }
        nodes = cm.merge(nodes, config['nodes']);
    };

    var getConfig = function(container, marker){
        if(container){
            marker = marker || 'data-config';
            var sourceConfig = container.getAttribute(marker);
            if(sourceConfig){
                config = cm.merge(config, JSON.parse(sourceConfig));
            }
        }
    };

    var executeEvent = function(event, params){
        API[event].forEach(function(item){
            item(that, params || {});
        });
    };

    /* ******* MAIN ******* */

    that.set = function(i){
        if(!isNaN(i) && items[i]){
            set(i);
        }
        return that;
    };

    that.next = function(){
        next();
        return that;
    };

    that.prev = function(){
        prev();
        return that;
    };

    that.addEvent = function(event, handler){
        if(API[event] && typeof handler == 'function'){
            API[event].push(handler);
        }
        return that;
    };

    that.removeEvent = function(event, handler){
        if(API[event] && typeof handler == 'function'){
            API[event] = API[event].filter(function(item){
                return item != handler;
            });
        }
        return that;
    };

    init();
};<commit_msg>* JS. Small changes in Com.Gallery.<commit_after>Com['Gallery'] = function(o){
    var that = this,
        config = cm.merge({
            'container' : cm.Node('div'),
            'node' : cm.Node('div'),
            'data' : [],
            'duration' : 500,
            'showCaption' : true,
            'showArrowTitles' : false,
            'nodes' : {},
            'events' : {},
            'langs' : {
                'next' : 'Next',
                'prev' : 'Previous'
            }
        }, o),
        API = {
            'onSet' : [],
            'onChange' : [],
            'onItemLoad' : [],
            'onItemSet' : []
        },
        nodes = {
            'items' : []
        },
        items = [],
        anim = {},
        active;

    var init = function(){
        convertEvents(config['events']);
        getConfig(config['node']);
        getNodes(config['node']);
        render();
        // Collect items
        cm.forEach(nodes['items'], collectItem);
        // Process config items
        cm.forEach(config['items'], processItem);
    };

    var render = function(){
        // Structure
        nodes['container'] = cm.Node('div', {'class' : 'com-gallery'},
            nodes['holder'] = cm.Node('div', {'class' : 'holder'}),
            nodes['bar'] = cm.Node('div', {'class' : 'bar'},
                nodes['next'] = cm.Node('div', {'class' : 'bar-arrow next'},
                    cm.Node('div', {'class' : 'icon'})
                ),
                nodes['prev'] = cm.Node('div', {'class' : 'bar-arrow prev'},
                    cm.Node('div', {'class' : 'icon'})
                )
            ),
            nodes['loader'] = cm.Node('div', {'class' : 'loader'},
                cm.Node('div', {'class' : 'bg'}),
                cm.Node('div', {'class' : 'icon small loader centered'})
            )
        );
        // Arrow titles
        if(config['showArrowTitles']){
            nodes['next'].setAttribute('title', lang('next'));
            nodes['prev'].setAttribute('title', lang('prev'));
        }
        // Set events
        cm.addEvent(nodes['next'], 'click', next);
        cm.addEvent(nodes['prev'], 'click', prev);
        // Init animation
        anim['loader'] = new cm.Animation(nodes['loader']);
        // Embed
        config['container'].appendChild(nodes['container']);
    };

    var collectItem = function(item, i){
        if(!item['link']){
            item['link'] = cm.Node('a')
        }
        item = cm.merge({
            'src' : item['link'].getAttribute('href') || '',
            'title' : item['link'].getAttribute('title') || ''
        }, item);
        processItem(item, i);
    };

    var processItem = function(item, i){
        item = cm.merge({
            'index' : i,
            'isLoad' : false,
            'type' : 'image',        // image | video
            'nodes' : {}
        }, item);
        /// Check type
        item['type'] = /(\.jpg|\.png|\.gif|\.jpeg|\.bmp|\.tga)$/gi.test(item['src']) ? 'image' : 'video';
        // Structure
        item['nodes']['container'] = cm.Node('div', {'class' : 'cm-imagebox is-no-hover is-centered'},
            item['nodes']['inner'] = cm.Node('div', {'class' : 'inner'})
        );
        // Render by type
        switch(item['type']){
            case 'image':
                item['nodes']['inner'].appendChild(
                    item['nodes']['content'] = cm.Node('img', {'class' : 'descr', 'alt' : item['title']})
                );
                // Caption
                if(config['showCaption'] && !cm.isEmpty(item['title'])){
                    item['nodes']['inner'].appendChild(
                        cm.Node('div', {'class' : 'title'},
                            cm.Node('div', {'class' : 'inner'}, item['title'])
                        )
                    );
                }
                break;

            default:
                item['nodes']['inner'].appendChild(
                    item['nodes']['content'] = cm.Node('iframe', {'class' : 'descr'})
                );
                break;
        }
        // Init animation
        item['anim'] = new cm.Animation(item['nodes']['container']);
        // Set image on thumb click
        cm.addEvent(item['link'], 'click', function(e){
            e = cm.getEvent(e);
            cm.preventDefault(e);
            set(item['index']);
        }, true, true);
        // Push item to array
        items.push(item);
    };

    var set = function(i){
        var item = items[i],
            itemOld = items[active];
        // API onSet
        executeEvent('onSet', {
            'current' : item,
            'previous' : itemOld
        });
        // If current active item not equal new item - process with new item, else redraw window alignment and dimensions
        if(i != active){
            // API onSet
            executeEvent('onChange', {
                'current' : item,
                'previous' : itemOld
            });
            // Check type
            if(item['type'] == 'image'){
                cm.removeClass(nodes['container'], 'has-iframe');
            }else{
                cm.addClass(nodes['container'], 'has-iframe');
                nodes['holder'].appendChild(item['nodes']['container']);
            }
            // Check is content load
            if(!item['isLoad'] || item['type'] != 'image'){
                // Show loader
                nodes['loader'].style.display = 'block';
                anim['loader'].go({'style' : {'opacity' : 1}, 'anim' : 'smooth', 'duration' : config['duration']});
                // Add image load event and src
                cm.addEvent(item['nodes']['content'], 'load', function(){
                    // Hide loader
                    anim['loader'].go({'style' : {'opacity' : 0}, 'anim' : 'smooth', 'duration' : config['duration'], 'onStop' : function(){
                        nodes['loader'].style.display = 'none';
                    }});
                    // Set and show image
                    setImage(i, item, itemOld);
                });
                item['nodes']['content'].src = item['src'];
            }else{
                // Set and show image
                setImage(i, item, itemOld);
            }
        }
    };

    var setImage = function(i, item, itemOld){
        // Set new active
        active = i;
        item['isLoad'] = true;
        // API onImageSetStart
        executeEvent('onItemLoad', item);
        // Embed item content
        if(itemOld){
            itemOld['nodes']['container'].style.zIndex = 1;
            item['nodes']['container'].style.zIndex = 2;
        }
        if(item['type'] == 'image'){
            nodes['holder'].appendChild(item['nodes']['container']);
        }
        // Animate Slide
        item['anim'].go({'style' : {'opacity' : 1}, 'anim' : 'smooth', 'duration' : config['duration'], 'onStop' : function(){
            // Remove old item
            if(itemOld){
                cm.setOpacity(itemOld['nodes']['container'], 0);
                cm.remove(itemOld['nodes']['container']);
            }
            // API onImageSet event
            executeEvent('onItemSet', item);
        }});
    };

    var next = function(){
        set((active == items.length - 1)? 0 : active + 1);
    };

    var prev = function(){
        set((active == 0)? items.length - 1 : active - 1);
    };

    /* *** MISC FUNCTIONS *** */

    var convertEvents = function(o){
        cm.forEach(o, function(item, key){
            if(API[key] && typeof item == 'function'){
                API[key].push(item);
            }
        });
    };

    var lang = function(str){
        if(!config['langs'][str]){
            config['langs'][str] = str;
        }
        return config['langs'][str];
    };

    var getNodes = function(container, marker){
        if(container){
            var sourceNodes = {};
            if(marker){
                sourceNodes = cm.getNodes(container)[marker] || {};
            }else{
                sourceNodes = cm.getNodes(container);
            }
            nodes = cm.merge(nodes, sourceNodes);
        }
        nodes = cm.merge(nodes, config['nodes']);
    };

    var getConfig = function(container, marker){
        if(container){
            marker = marker || 'data-config';
            var sourceConfig = container.getAttribute(marker);
            if(sourceConfig){
                config = cm.merge(config, JSON.parse(sourceConfig));
            }
        }
    };

    var executeEvent = function(event, params){
        API[event].forEach(function(item){
            item(that, params || {});
        });
    };

    /* ******* MAIN ******* */

    that.set = function(i){
        if(!isNaN(i) && items[i]){
            set(i);
        }
        return that;
    };

    that.next = function(){
        next();
        return that;
    };

    that.prev = function(){
        prev();
        return that;
    };

    that.addEvent = function(event, handler){
        if(API[event] && typeof handler == 'function'){
            API[event].push(handler);
        }
        return that;
    };

    that.removeEvent = function(event, handler){
        if(API[event] && typeof handler == 'function'){
            API[event] = API[event].filter(function(item){
                return item != handler;
            });
        }
        return that;
    };

    init();
};<|endoftext|>