<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 LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';

/**
 * Internal module. indexeddb storage for e2e.
 *
 * @module
 */

/**
 * An implementation of CryptoStore, which is normally backed by an indexeddb,
 * but with fallback to MemoryCryptoStore.
 *
 * @implements {module:crypto/store/base~CryptoStore}
 */
export default class IndexedDBCryptoStore {
    /**
     * Create a new IndexedDBCryptoStore
     *
     * @param {IDBFactory} indexedDB  global indexedDB instance
     * @param {string} dbName   name of db to connect to
     */
    constructor(indexedDB, dbName) {
        this._indexedDB = indexedDB;
        this._dbName = dbName;
        this._backendPromise = null;
    }

    /**
     * Ensure the database exists and is up-to-date, or fall back to
     * a local storage or in-memory store.
     *
     * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
     * or a MemoryCryptoStore
     */
    _connect() {
        if (this._backendPromise) {
            return this._backendPromise;
        }

        this._backendPromise = new Promise((resolve, reject) => {
            if (!this._indexedDB) {
                reject(new Error('no indexeddb support available'));
                return;
            }

            console.log(`connecting to indexeddb ${this._dbName}`);

            const req = this._indexedDB.open(
                this._dbName, IndexedDBCryptoStoreBackend.VERSION,
            );

            req.onupgradeneeded = (ev) => {
                const db = ev.target.result;
                const oldVersion = ev.oldVersion;
                IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
            };

            req.onblocked = () => {
                console.log(
                    `can't yet open IndexedDBCryptoStore because it is open elsewhere`,
                );
            };

            req.onerror = (ev) => {
                reject(ev.target.error);
            };

            req.onsuccess = (r) => {
                const db = r.target.result;

                console.log(`connected to indexeddb ${this._dbName}`);
                resolve(new IndexedDBCryptoStoreBackend.Backend(db));
            };
        }).catch((e) => {
            console.warn(
                `unable to connect to indexeddb ${this._dbName}` +
                    `: falling back to localStorage store: ${e}`,
            );
            return new LocalStorageCryptoStore(global.localStorage);
        }).catch((e) => {
            console.warn(
                `unable to open localStorage: falling back to in-memory store: ${e}`,
            );
            return new MemoryCryptoStore();
        });

        return this._backendPromise;
    }

    /**
     * Delete all data from this store.
     *
     * @returns {Promise} resolves when the store has been cleared.
     */
    deleteAllData() {
        return new Promise((resolve, reject) => {
            if (!this._indexedDB) {
                reject(new Error('no indexeddb support available'));
                return;
            }

            console.log(`Removing indexeddb instance: ${this._dbName}`);
            const req = this._indexedDB.deleteDatabase(this._dbName);

            req.onblocked = () => {
                console.log(
                    `can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
                );
            };

            req.onerror = (ev) => {
                reject(ev.target.error);
            };

            req.onsuccess = () => {
                console.log(`Removed indexeddb instance: ${this._dbName}`);
                resolve();
            };
        }).catch((e) => {
            // in firefox, with indexedDB disabled, this fails with a
            // DOMError. We treat this as non-fatal, so that people can
            // still use the app.
            console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOrAddOutgoingRoomKeyRequest(request);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOutgoingRoomKeyRequest(requestBody);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOutgoingRoomKeyRequestByState(wantedStates);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.updateOutgoingRoomKeyRequest(
                requestId, expectedState, updates,
            );
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
        });
    }

    // Olm Account

    /*
     * Get the account pickle from the store.
     * This requires an active transaction. See doTxn().
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(string)} func Called with the account pickle
     */
    getAccount(txn, func) {
        this._backendPromise.value().getAccount(txn, func);
    }

    /*
     * Write the account pickle to the store.
     * This requires an active transaction. See doTxn().
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {string} newData The new account pickle to store.
     */
    storeAccount(txn, newData) {
        this._backendPromise.value().storeAccount(txn, newData);
    }

    // Olm sessions

    /**
     * Returns the number of end-to-end sessions in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(int)} func Called with the count of sessions
     */
    countEndToEndSessions(txn, func) {
        this._backendPromise.value().countEndToEndSessions(txn, func);
    }

    /**
     * Retrieve a specific end-to-end session between the logged-in user
     * and another device.
     * @param {string} deviceKey The public key of the other device.
     * @param {string} sessionId The ID of the session to retrieve
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndSession(deviceKey, sessionId, txn, func) {
        this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
    }

    /**
     * Retrieve the end-to-end sessions between the logged-in user and another
     * device.
     * @param {string} deviceKey The public key of the other device.
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndSessions(deviceKey, txn, func) {
        this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
    }

    /**
     * Store a session between the logged-in user and another device
     * @param {string} deviceKey The public key of the other device.
     * @param {string} sessionId The ID for this end-to-end session.
     * @param {string} session Base64 encoded end-to-end session.
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndSession(deviceKey, sessionId, session, txn) {
        this._backendPromise.value().storeEndToEndSession(
            deviceKey, sessionId, session, txn,
        );
    }

    // Inbound group saessions

    /**
     * Retrieve the end-to-end inbound group session for a given
     * server key and session ID
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
        this._backendPromise.value().getEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, txn, func,
        );
    }

    /**
     * Fetches all inbound group sessions in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called once for each group session
     *     in the store with an object having keys {senderKey, sessionId,
     *     sessionData}, then once with null to indicate the end of the list.
     */
    getAllEndToEndInboundGroupSessions(txn, func) {
        this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
    }

    /**
     * Adds an end-to-end inbound group session to the store.
     * If there already exists an inbound group session with the same
     * senderCurve25519Key and sessionID, the session will not be added.
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {object} sessionData The session data structure
     * @param {*} txn An active transaction. See doTxn().
     */
    addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        this._backendPromise.value().addEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, sessionData, txn,
        );
    }

    /**
     * Writes an end-to-end inbound group session to the store.
     * If there already exists an inbound group session with the same
     * senderCurve25519Key and sessionID, it will be overwritten.
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {object} sessionData The session data structure
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        this._backendPromise.value().storeEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, sessionData, txn,
        );
    }

    // End-to-end device tracking

    /**
     * Store the state of all tracked devices
     * This contains devices for each user, a tracking state for each user
     * and a sync token matching the point in time the snapshot represents.
     * These all need to be written out in full each time such that the snapshot
     * is always consistent, so they are stored in one object.
     *
     * @param {Object} deviceData
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndDeviceData(deviceData, txn) {
        this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
    }

    /**
     * Get the state of all tracked devices
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(Object)} func Function called with the
     *     device data
     */
    getEndToEndDeviceData(txn, func) {
        this._backendPromise.value().getEndToEndDeviceData(txn, func);
    }

    // End to End Rooms

    /**
     * Store the end-to-end state for a room.
     * @param {string} roomId The room's ID.
     * @param {object} roomInfo The end-to-end info for the room.
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndRoom(roomId, roomInfo, txn) {
        this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
    }

    /**
     * Get an object of roomId->roomInfo for all e2e rooms in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(Object)} func Function called with the end to end encrypted rooms
     */
    getEndToEndRooms(txn, func) {
        this._backendPromise.value().getEndToEndRooms(txn, func);
    }

    /**
     * Perform a transaction on the crypto store. Any store methods
     * that require a transaction (txn) object to be passed in may
     * only be called within a callback of either this function or
     * one of the store functions operating on the same transaction.
     *
     * @param {string} mode 'readwrite' if you need to call setter
     *     functions with this transaction. Otherwise, 'readonly'.
     * @param {string[]} stores List IndexedDBCryptoStore.STORE_*
     *     options representing all types of object that will be
     *     accessed or written to with this transaction.
     * @param {function(*)} func Function called with the
     *     transaction object: an opaque object that should be passed
     *     to store functions.
     * @return {Promise} Promise that resolves with the result of the `func`
     *     when the transaction is complete. If the backend is
     *     async (ie. the indexeddb backend) any of the callback
     *     functions throwing an exception will cause this promise to
     *     reject with that exception. On synchronous backends, the
     *     exception will propagate to the caller of the getFoo method.
     */
    doTxn(mode, stores, func) {
        return this._connect().then((backend) => {
            return backend.doTxn(mode, stores, func);
        });
    }
}

IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
<commit_msg>Make e2e work on Edge<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 LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';

/**
 * Internal module. indexeddb storage for e2e.
 *
 * @module
 */

/**
 * An implementation of CryptoStore, which is normally backed by an indexeddb,
 * but with fallback to MemoryCryptoStore.
 *
 * @implements {module:crypto/store/base~CryptoStore}
 */
export default class IndexedDBCryptoStore {
    /**
     * Create a new IndexedDBCryptoStore
     *
     * @param {IDBFactory} indexedDB  global indexedDB instance
     * @param {string} dbName   name of db to connect to
     */
    constructor(indexedDB, dbName) {
        this._indexedDB = indexedDB;
        this._dbName = dbName;
        this._backendPromise = null;
    }

    /**
     * Ensure the database exists and is up-to-date, or fall back to
     * a local storage or in-memory store.
     *
     * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
     * or a MemoryCryptoStore
     */
    _connect() {
        if (this._backendPromise) {
            return this._backendPromise;
        }

        this._backendPromise = new Promise((resolve, reject) => {
            if (!this._indexedDB) {
                reject(new Error('no indexeddb support available'));
                return;
            }

            console.log(`connecting to indexeddb ${this._dbName}`);

            const req = this._indexedDB.open(
                this._dbName, IndexedDBCryptoStoreBackend.VERSION,
            );

            req.onupgradeneeded = (ev) => {
                const db = ev.target.result;
                const oldVersion = ev.oldVersion;
                IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
            };

            req.onblocked = () => {
                console.log(
                    `can't yet open IndexedDBCryptoStore because it is open elsewhere`,
                );
            };

            req.onerror = (ev) => {
                reject(ev.target.error);
            };

            req.onsuccess = (r) => {
                const db = r.target.result;

                console.log(`connected to indexeddb ${this._dbName}`);
                resolve(new IndexedDBCryptoStoreBackend.Backend(db));
            };
        }).then((backend) => {
            // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
            // Try a dummy query which will fail if the browser doesn't support compund keys, so
            // we can fall back to a different backend.
            return backend.doTxn('readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
                backend.getEndToEndInboundGroupSession('', '', txn, () => {});
            }).then(() => {
                return backend;
            });
        }).catch((e) => {
            console.warn(
                `unable to connect to indexeddb ${this._dbName}` +
                    `: falling back to localStorage store: ${e}`,
            );
            return new LocalStorageCryptoStore(global.localStorage);
        }).catch((e) => {
            console.warn(
                `unable to open localStorage: falling back to in-memory store: ${e}`,
            );
            return new MemoryCryptoStore();
        });

        return this._backendPromise;
    }

    /**
     * Delete all data from this store.
     *
     * @returns {Promise} resolves when the store has been cleared.
     */
    deleteAllData() {
        return new Promise((resolve, reject) => {
            if (!this._indexedDB) {
                reject(new Error('no indexeddb support available'));
                return;
            }

            console.log(`Removing indexeddb instance: ${this._dbName}`);
            const req = this._indexedDB.deleteDatabase(this._dbName);

            req.onblocked = () => {
                console.log(
                    `can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
                );
            };

            req.onerror = (ev) => {
                reject(ev.target.error);
            };

            req.onsuccess = () => {
                console.log(`Removed indexeddb instance: ${this._dbName}`);
                resolve();
            };
        }).catch((e) => {
            // in firefox, with indexedDB disabled, this fails with a
            // DOMError. We treat this as non-fatal, so that people can
            // still use the app.
            console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOrAddOutgoingRoomKeyRequest(request);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOutgoingRoomKeyRequest(requestBody);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.getOutgoingRoomKeyRequestByState(wantedStates);
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.updateOutgoingRoomKeyRequest(
                requestId, expectedState, updates,
            );
        });
    }

    /**
     * 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) {
        return this._connect().then((backend) => {
            return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
        });
    }

    // Olm Account

    /*
     * Get the account pickle from the store.
     * This requires an active transaction. See doTxn().
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(string)} func Called with the account pickle
     */
    getAccount(txn, func) {
        this._backendPromise.value().getAccount(txn, func);
    }

    /*
     * Write the account pickle to the store.
     * This requires an active transaction. See doTxn().
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {string} newData The new account pickle to store.
     */
    storeAccount(txn, newData) {
        this._backendPromise.value().storeAccount(txn, newData);
    }

    // Olm sessions

    /**
     * Returns the number of end-to-end sessions in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(int)} func Called with the count of sessions
     */
    countEndToEndSessions(txn, func) {
        this._backendPromise.value().countEndToEndSessions(txn, func);
    }

    /**
     * Retrieve a specific end-to-end session between the logged-in user
     * and another device.
     * @param {string} deviceKey The public key of the other device.
     * @param {string} sessionId The ID of the session to retrieve
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndSession(deviceKey, sessionId, txn, func) {
        this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
    }

    /**
     * Retrieve the end-to-end sessions between the logged-in user and another
     * device.
     * @param {string} deviceKey The public key of the other device.
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndSessions(deviceKey, txn, func) {
        this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
    }

    /**
     * Store a session between the logged-in user and another device
     * @param {string} deviceKey The public key of the other device.
     * @param {string} sessionId The ID for this end-to-end session.
     * @param {string} session Base64 encoded end-to-end session.
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndSession(deviceKey, sessionId, session, txn) {
        this._backendPromise.value().storeEndToEndSession(
            deviceKey, sessionId, session, txn,
        );
    }

    // Inbound group saessions

    /**
     * Retrieve the end-to-end inbound group session for a given
     * server key and session ID
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called with A map from sessionId
     *     to Base64 end-to-end session.
     */
    getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
        this._backendPromise.value().getEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, txn, func,
        );
    }

    /**
     * Fetches all inbound group sessions in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(object)} func Called once for each group session
     *     in the store with an object having keys {senderKey, sessionId,
     *     sessionData}, then once with null to indicate the end of the list.
     */
    getAllEndToEndInboundGroupSessions(txn, func) {
        this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
    }

    /**
     * Adds an end-to-end inbound group session to the store.
     * If there already exists an inbound group session with the same
     * senderCurve25519Key and sessionID, the session will not be added.
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {object} sessionData The session data structure
     * @param {*} txn An active transaction. See doTxn().
     */
    addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        this._backendPromise.value().addEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, sessionData, txn,
        );
    }

    /**
     * Writes an end-to-end inbound group session to the store.
     * If there already exists an inbound group session with the same
     * senderCurve25519Key and sessionID, it will be overwritten.
     * @param {string} senderCurve25519Key The sender's curve 25519 key
     * @param {string} sessionId The ID of the session
     * @param {object} sessionData The session data structure
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
        this._backendPromise.value().storeEndToEndInboundGroupSession(
            senderCurve25519Key, sessionId, sessionData, txn,
        );
    }

    // End-to-end device tracking

    /**
     * Store the state of all tracked devices
     * This contains devices for each user, a tracking state for each user
     * and a sync token matching the point in time the snapshot represents.
     * These all need to be written out in full each time such that the snapshot
     * is always consistent, so they are stored in one object.
     *
     * @param {Object} deviceData
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndDeviceData(deviceData, txn) {
        this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
    }

    /**
     * Get the state of all tracked devices
     *
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(Object)} func Function called with the
     *     device data
     */
    getEndToEndDeviceData(txn, func) {
        this._backendPromise.value().getEndToEndDeviceData(txn, func);
    }

    // End to End Rooms

    /**
     * Store the end-to-end state for a room.
     * @param {string} roomId The room's ID.
     * @param {object} roomInfo The end-to-end info for the room.
     * @param {*} txn An active transaction. See doTxn().
     */
    storeEndToEndRoom(roomId, roomInfo, txn) {
        this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
    }

    /**
     * Get an object of roomId->roomInfo for all e2e rooms in the store
     * @param {*} txn An active transaction. See doTxn().
     * @param {function(Object)} func Function called with the end to end encrypted rooms
     */
    getEndToEndRooms(txn, func) {
        this._backendPromise.value().getEndToEndRooms(txn, func);
    }

    /**
     * Perform a transaction on the crypto store. Any store methods
     * that require a transaction (txn) object to be passed in may
     * only be called within a callback of either this function or
     * one of the store functions operating on the same transaction.
     *
     * @param {string} mode 'readwrite' if you need to call setter
     *     functions with this transaction. Otherwise, 'readonly'.
     * @param {string[]} stores List IndexedDBCryptoStore.STORE_*
     *     options representing all types of object that will be
     *     accessed or written to with this transaction.
     * @param {function(*)} func Function called with the
     *     transaction object: an opaque object that should be passed
     *     to store functions.
     * @return {Promise} Promise that resolves with the result of the `func`
     *     when the transaction is complete. If the backend is
     *     async (ie. the indexeddb backend) any of the callback
     *     functions throwing an exception will cause this promise to
     *     reject with that exception. On synchronous backends, the
     *     exception will propagate to the caller of the getFoo method.
     */
    doTxn(mode, stores, func) {
        return this._connect().then((backend) => {
            return backend.doTxn(mode, stores, func);
        });
    }
}

IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
<|endoftext|>


<commit_before>;(function(){
  var pageAnalyse = function(current, max, navSize) {
    var prev,
      next,
      prevMore,
      nextMore,
      from,
      to
    prev = current > 1 ? current - 1 : null
    next = max > current ? current + 1 : null

    if (max <= navSize) {
      from = 1
      to = max
      prevMore = false
      nextMore = false
    } else {
      if (current <= Math.ceil(navSize / 2)) {
        from = 1
        to = navSize - 1
        prevMore = false
        nextMore = true
      } else if (max - current < Math.ceil(navSize / 2)) {
        to = max
        from = max - Math.ceil(navSize / 2) - 1
        prevMore = true
        nextMore = false
      } else {
        from = current - Math.ceil(navSize / 2) + 2
        to = current + Math.ceil(navSize / 2) - 2
        prevMore = true
        nextMore = true
      }
    }
    return {
      prev: prev,
      next: next,
      prevMore: prevMore,
      nextMore: nextMore,
      from: from,
      to: to
    }
  }

  var numberHelper = function(page, prop) {
    /*
    * prop.current bool 当前页
    * prop.first bool 第一页，等同于 page === 1
    * prop.last bool 最后一页
    * */
    if (prop.current) {
      return '' + page + ''
    } else {
      return '' + page + ''
    }
  }
  var nextHelper = function(page, prop) {
    /*
    * prop.disabled 下一页是否可用
    * */
    if (prop.disabled) {
      return '' + this.nextText + ''
    } else {
      return '' + this.nextText + ''
    }
  }
  var prevHelper = function(page, prop) {
    /*
     * prop.disabled 上一页是否可用
     * */
    if (prop.disabled) {
      return '' + this.prevText + ''
    } else {
      return '' + this.prevText + ''
    }
  }
  var moreHelper = function() {
    return '' + this.moreText + ''
  }

  var linkHelper = function() {
    /*
     * args[0] 链接所指页数
     * */
    return 'javascript:void(0);'
  }

  var entitize = function(str) {
    return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
  }

  function PageNavigator(setting) {
    setting = setting || {}
    this._numberHelper = setting.numberHelper || numberHelper
    this._nextHelper = setting.nextHelper || nextHelper
    this._prevHelper = setting.prevHelper || prevHelper
    this._prevMoreHelper = setting.prevMoreHelper || moreHelper
    this._nextMoreHelper = setting.nextMoreHelper || moreHelper
    this.prevText = entitize(setting.prevText || '上一页')
    this.nextText = entitize(setting.nextText || '下一页')
    this.moreText = entitize(setting.moreText || '...')
    this._navSize = setting.size || 7
    this.link = setting.linkHelper || linkHelper
    if(typeof this.link === 'string'){
      var _link = this.link
      this.link = function(page) {
        return _link.replace(/\{\{page\}\}/g, page)
      }
    }
  }

  PageNavigator.prototype.create = function(current, max) {
    var analyseRst = pageAnalyse(current, max, this._navSize)

    var str = ''

    str = str + this._prevHelper(analyseRst.prev, {
      disabled: !analyseRst.prev
    })

    if (analyseRst.prevMore) {
      str = str + this._numberHelper(1, {
        current: false,
        first: true
      }) + this._prevMoreHelper()
    }

    for (var i = analyseRst.from; i <= analyseRst.to; i++) {
      str = str + this._numberHelper(i, {
        current: i === current,
        first: i === 1,
        last: i === max
      })
    }

    if (analyseRst.nextMore) {
      str = str + this._nextMoreHelper() + this._numberHelper(max, {
        current: false,
        last: true
      })
    }

    str = str + this._nextHelper(analyseRst.next, {
      disabled: !analyseRst.next
    })

    return str;
  }
  window.PageNavigator = PageNavigator
})()
<commit_msg>字符串拼接模板<commit_after>;(function(){
  var pageAnalyse = function(current, max, navSize) {
    var prev,
      next,
      prevMore,
      nextMore,
      from,
      to
    prev = current > 1 ? current - 1 : null
    next = max > current ? current + 1 : null

    if (max <= navSize) {
      from = 1
      to = max
      prevMore = false
      nextMore = false
    } else {
      if (current <= Math.ceil(navSize / 2)) {
        from = 1
        to = navSize - 1
        prevMore = false
        nextMore = true
      } else if (max - current < Math.ceil(navSize / 2)) {
        to = max
        from = max - Math.ceil(navSize / 2) - 1
        prevMore = true
        nextMore = false
      } else {
        from = current - Math.ceil(navSize / 2) + 2
        to = current + Math.ceil(navSize / 2) - 2
        prevMore = true
        nextMore = true
      }
    }
    return {
      prev: prev,
      next: next,
      prevMore: prevMore,
      nextMore: nextMore,
      from: from,
      to: to
    }
  }

  var stringReplace = function(str, dict) {
    // 未对正则特殊字符进行处理，注意
    for(var key in dict){
      str = str.replace(new RegExp('{{'+key+'}}', 'g'), dict[key])
    }
    return str
  }

  var numberHelper = function(page, prop) {
    /*
    * prop.current bool 当前页
    * prop.first bool 第一页，等同于 page === 1
    * prop.last bool 最后一页
    * */
    if (prop.current) {
      return stringReplace('{{page}}', {
        page: page
      })
    } else {
      return stringReplace('{{page}}', {
        page: page,
        link: this.link(page)
      })
    }
  }
  var nextHelper = function(page, prop) {
    /*
    * prop.disabled 下一页是否可用
    * */
    if (prop.disabled) {
      return stringReplace('{{nextText}}', {
        page: page,
        nextText: this.nextText
      })
    } else {
      return stringReplace('{{nextText}}', {
        link: this.link(page),
        page: page,
        nextText: this.nextText
      })
    }
  }
  var prevHelper = function(page, prop) {
    /*
     * prop.disabled 上一页是否可用
     * */
    if (prop.disabled) {
      return stringReplace('{{prevText}}', {
        page: page,
        prevText: this.prevText
      })
    } else {
      return stringReplace('{{prevText}}', {
        link: this.link(page),
        page: page,
        prevText: this.prevText
      })
    }
  }
  var moreHelper = function() {
    return stringReplace('{{moreText}}', {
      moreText: this.moreText
    })
  }

  var linkHelper = function() {
    /*
     * args[0] 链接所指页数
     * */
    return 'javascript:void(0);'
  }

  var entitize = function(str) {
    return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
  }

  function PageNavigator(setting) {
    setting = setting || {}
    this._numberHelper = setting.numberHelper || numberHelper
    this._nextHelper = setting.nextHelper || nextHelper
    this._prevHelper = setting.prevHelper || prevHelper
    this._prevMoreHelper = setting.prevMoreHelper || moreHelper
    this._nextMoreHelper = setting.nextMoreHelper || moreHelper
    this.prevText = entitize(setting.prevText || '上一页')
    this.nextText = entitize(setting.nextText || '下一页')
    this.moreText = entitize(setting.moreText || '...')
    this._navSize = setting.size || 7
    this.link = setting.linkHelper || linkHelper
    if(typeof this.link === 'string'){
      var _link = this.link
      this.link = function(page) {
        return stringReplace(_link, {page: page})
      }
    }
  }

  PageNavigator.prototype.create = function(current, max) {
    var analyseRst = pageAnalyse(current, max, this._navSize)

    var str = ''

    str = str + this._prevHelper(analyseRst.prev, {
      disabled: !analyseRst.prev
    })

    if (analyseRst.prevMore) {
      str = str + this._numberHelper(1, {
        current: false,
        first: true
      }) + this._prevMoreHelper()
    }

    for (var i = analyseRst.from; i <= analyseRst.to; i++) {
      str = str + this._numberHelper(i, {
        current: i === current,
        first: i === 1,
        last: i === max
      })
    }

    if (analyseRst.nextMore) {
      str = str + this._nextMoreHelper() + this._numberHelper(max, {
        current: false,
        last: true
      })
    }

    str = str + this._nextHelper(analyseRst.next, {
      disabled: !analyseRst.next
    })

    return str;
  }
  window.PageNavigator = PageNavigator
})()
<|endoftext|>


<commit_before><commit_msg>fixed theme issues.<commit_after><|endoftext|>


<commit_before><commit_msg>Fixing a bug in hiding/showing child sample values on the Collection page.<commit_after><|endoftext|>


<commit_before>/* eslint max-len: 0 */
import React from 'react';
import { Link } from 'routes';

// Redux
import withRedux from 'next-redux-wrapper';
import { initStore } from 'store';
import { getInsights } from 'redactions/insights';

// Layout
import Page from 'components/app/layout/Page';
import Layout from 'components/app/layout/Layout';

// Components
import Banner from 'components/app/common/Banner';
import CardStatic from 'components/app/common/CardStatic';
import Rating from 'components/app/common/Rating';

const exploreCards = [
  {
    tag: 'Explore Data',
    title: 'Dive into the data',
    intro: 'Create and download custom visualisations using our collection of datasets related to natural resources.',
    buttons: [
      {
        text: 'Explore data',
        path: 'explore',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/explore_data_1.png)'
  },
  {
    tag: 'Dashboards',
    title: 'Review the topic or country you care about most',
    intro: 'Find facts and figures about a country or topic, or build your own dashboard to monitor the data you care about.',
    buttons: [
      {
        text: 'View dashboards',
        path: 'dashboards',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/explore_data_2.png)'
  },
  {
    tag: 'Planet Pulse',
    title: 'Take the pulse of our planet',
    intro: 'A global snapshot of key impacts on livelihoods from the latest data.',
    buttons: [
      {
        text: 'Launch Planet Pulse',
        path: 'pulse',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/planetpulse.jpg) 67% center'
  }
];

class Home extends Page {
  static insightsCardsStatic(insightsData) {
    return insightsData.map(c =>
      (<CardStatic
        key={`insight-card-${c.tag}`}
        className="-alt"
        background={c.background}
        clickable
        route={`/blog/${c.slug}`}
      >

{c.tag}

            <Link route={`/blog/${c.slug}`}>
              {c.title}
            </Link>

              by {c.source.name}

          {c.ranking && <Rating rating={c.ranking} />}

      </CardStatic>)
    );
  }

  static exploreCardsStatic() {
    return exploreCards.map(c =>
      (<div key={`explore-card-${c.title}`} className="column small-12 medium-4">
        <CardStatic
          className="-alt"
          background={c.background}
          clickable
          route={c.buttons[0].path}
        >

{c.tag}

{c.title}

{c.intro}

            {c.buttons.map(b => (
              <Link route={b.path} key={b.path}><a className={`c-btn -alt ${b.className}`}>{b.text}</Link>
            ))}

        </CardStatic>

)
    );
  }

  componentDidMount() {
    super.componentDidMount();
    this.props.getInsights();
  }

  render() {
    const { insights } = this.props;
    const insightsCardsStatic = Home.insightsCardsStatic(insights);
    const exploreCardsStatic = Home.exploreCardsStatic();

    return (
      <Layout
        title="Resource Watch"
        description="Resource Watch description"
        url={this.props.url}
        user={this.props.user}
        className="page-home"
      >

            <iframe
              id="video-intro"
              title="Video Intro"
              frameBorder="0"
              allowFullScreen
              // Loop parameter has limited support in the AS3 player and in IFrame embeds, which could load either the AS3 or HTML5 player.
              // Currently, the loop parameter only works in the AS3 player when used in conjunction with the playlist parameter.
              // To loop a single video, set the loop parameter value to 1 and set the playlist parameter value to the same video ID already specified in the Player API URL
              src="https://youtube.com/embed/XryMlA-8IwE?controls=0&showinfo=0&rel=0&autoplay=1&loop=1&playlist=XryMlA-8IwE"
            />

Explore a world
of natural resource data

Discover the latest data, make connections, and help create a more sustainable planet.

Discover Signals

Read the stories behind the data on our Signals blog.

                  {insightsCardsStatic[0]}

                    {insightsCardsStatic[1]}
                    {insightsCardsStatic[2]}

                  <Link route="insights">
                    More Signals
                  </Link>

Explore the data

Discover data, create visualizations, and subscribe to updates on key datasets.

                {exploreCardsStatic}

        <Banner className="get-involved" bgImage={'/static/images/backgrounds/mod_getInvolved.jpg'}>

Get involved

                We{'’'}ve brought together the best datasets related to natural resources,
                so you can find new insights, influence decisions and change the world.
                There{'’'}s a world of opportunity to take this futher. Here are
                some ideas to get you started.

                <Link route="get_involved_detail" params={{ id: 'contribute-data', source: 'home' }}>Contribute data</Link>

                <Link route="get_involved_detail" params={{ id: 'join-community', source: 'home' }}>Join the community</Link>

                <Link route="get_involved_detail" params={{ id: 'submit-an-insight', source: 'home' }}>Submit a story</Link>

                <Link route="get_involved_detail" params={{ id: 'develop-app', source: 'home' }}>Develop your app</Link>

        </Banner>

      </Layout>
    );
  }
}

const mapStateToProps = state => ({ insights: state.insights.list });

const mapDispatchToProps = dispatch => ({
  getInsights: () => dispatch(getInsights())
});

export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Home);
<commit_msg>Home banner fix<commit_after>/* eslint max-len: 0 */
import React from 'react';
import { Link } from 'routes';

// Redux
import withRedux from 'next-redux-wrapper';
import { initStore } from 'store';
import { getInsights } from 'redactions/insights';

// Layout
import Page from 'components/app/layout/Page';
import Layout from 'components/app/layout/Layout';

// Components
import Banner from 'components/app/common/Banner';
import CardStatic from 'components/app/common/CardStatic';
import Rating from 'components/app/common/Rating';

const exploreCards = [
  {
    tag: 'Explore Data',
    title: 'Dive into the data',
    intro: 'Create and download custom visualisations using our collection of datasets related to natural resources.',
    buttons: [
      {
        text: 'Explore data',
        path: 'explore',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/explore_data_1.png)'
  },
  {
    tag: 'Dashboards',
    title: 'Review the topic or country you care about most',
    intro: 'Find facts and figures about a country or topic, or build your own dashboard to monitor the data you care about.',
    buttons: [
      {
        text: 'View dashboards',
        path: 'dashboards',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/explore_data_2.png)'
  },
  {
    tag: 'Planet Pulse',
    title: 'Take the pulse of our planet',
    intro: 'A global snapshot of key impacts on livelihoods from the latest data.',
    buttons: [
      {
        text: 'Launch Planet Pulse',
        path: 'pulse',
        className: '-primary'
      }
    ],
    background: 'url(/static/tempImages/backgrounds/planetpulse.jpg) 67% center'
  }
];

class Home extends Page {
  static insightsCardsStatic(insightsData) {
    return insightsData.map(c =>
      (<CardStatic
        key={`insight-card-${c.tag}`}
        className="-alt"
        background={c.background}
        clickable
        route={`/blog/${c.slug}`}
      >

{c.tag}

            <Link route={`/blog/${c.slug}`}>
              {c.title}
            </Link>

              by {c.source.name}

          {c.ranking && <Rating rating={c.ranking} />}

      </CardStatic>)
    );
  }

  static exploreCardsStatic() {
    return exploreCards.map(c =>
      (<div key={`explore-card-${c.title}`} className="column small-12 medium-4">
        <CardStatic
          className="-alt"
          background={c.background}
          clickable
          route={c.buttons[0].path}
        >

{c.tag}

{c.title}

{c.intro}

            {c.buttons.map(b => (
              <Link route={b.path} key={b.path}><a className={`c-btn -alt ${b.className}`}>{b.text}</Link>
            ))}

        </CardStatic>

)
    );
  }

  componentDidMount() {
    super.componentDidMount();
    this.props.getInsights();
  }

  render() {
    const { insights } = this.props;
    const insightsCardsStatic = Home.insightsCardsStatic(insights);
    const exploreCardsStatic = Home.exploreCardsStatic();

    return (
      <Layout
        title="Resource Watch"
        description="Resource Watch description"
        url={this.props.url}
        user={this.props.user}
        className="page-home"
      >

            <iframe
              id="video-intro"
              title="Video Intro"
              frameBorder="0"
              allowFullScreen
              // Loop parameter has limited support in the AS3 player and in IFrame embeds, which could load either the AS3 or HTML5 player.
              // Currently, the loop parameter only works in the AS3 player when used in conjunction with the playlist parameter.
              // To loop a single video, set the loop parameter value to 1 and set the playlist parameter value to the same video ID already specified in the Player API URL
              src="https://youtube.com/embed/XryMlA-8IwE?controls=0&showinfo=0&rel=0&autoplay=1&loop=1&playlist=XryMlA-8IwE"
            />

Explore a world
of natural resource data

Discover the latest data, make connections, and help create a more sustainable planet.

Discover Signals

Read the stories behind the data on our Signals blog.

                  {insightsCardsStatic[0]}

                    {insightsCardsStatic[1]}
                    {insightsCardsStatic[2]}

                  <Link route="insights">
                    More Signals
                  </Link>

Explore the data

Discover data, create visualizations, and subscribe to updates on key datasets.

                {exploreCardsStatic}

        <Banner className="get-involved" bgImage={'/static/images/backgrounds/mod_getInvolved.jpg'}>

Get involved

                  We{'’'}ve brought together the best datasets related to natural resources,
                  so you can find new insights, influence decisions and change the world.
                  There{'’'}s a world of opportunity to take this futher. Here are
                  some ideas to get you started.

                  <Link route="get_involved_detail" params={{ id: 'contribute-data', source: 'home' }}>Contribute data</Link>

                  <Link route="get_involved_detail" params={{ id: 'join-community', source: 'home' }}>Join the community</Link>

                  <Link route="get_involved_detail" params={{ id: 'submit-an-insight', source: 'home' }}>Submit a story</Link>

                  <Link route="get_involved_detail" params={{ id: 'develop-app', source: 'home' }}>Develop your app</Link>

        </Banner>

      </Layout>
    );
  }
}

const mapStateToProps = state => ({ insights: state.insights.list });

const mapDispatchToProps = dispatch => ({
  getInsights: () => dispatch(getInsights())
});

export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Home);
<|endoftext|>


<commit_before>/* @flow */
import React from 'react';
import ReactDOM from 'react-dom';

type Props = {
  data: string,
}

function createElement(html: string): HTMLElement {
  // Create a range to ensure that scripts are invoked from within the HTML
  if (document.createRange && Range && Range.prototype.createContextualFragment) {
    const range = document.createRange();
    const fragment = range.createContextualFragment(html);
    return fragment;
  }
  d = document.createElement('div');
  d.innerHTML = html;
  return d;
}

export default class HTMLDisplay extends React.Component {
  props: Props;
  el: HTMLElement;

  componentDidMount(): void {
    this.el.appendChild(createElement(this.props.data));
  }

  shouldComponentUpdate(old): boolean {
    return true;
  }
  componentDidUpdate(prevProps: Props): void {
    // clear out all DOM element children
    while (this.el.firstChild) {
      this.el.removeChild(this.el.firstChild);
    }
    this.el.appendChild(createElement(this.props.data));
  }

  render(): ?React.Element<any> {
    return (

 { this.el = el; }} />
    );
  }
}
<commit_msg>refactor(display): only update HTML when data changes<commit_after>/* @flow */
import React from 'react';
import ReactDOM from 'react-dom';

type Props = {
  data: string,
}

function createElement(html: string): HTMLElement {
  // Create a range to ensure that scripts are invoked from within the HTML
  if (document.createRange && Range && Range.prototype.createContextualFragment) {
    const range = document.createRange();
    const fragment = range.createContextualFragment(html);
    return fragment;
  }
  d = document.createElement('div');
  d.innerHTML = html;
  return d;
}

export default class HTMLDisplay extends React.Component {
  props: Props;
  el: HTMLElement;

  componentDidMount(): void {
    this.el.appendChild(createElement(this.props.data));
  }

  shouldComponentUpdate(prevProps: Props): boolean {
    return prevProps.data !== this.props.data;
  }
  componentDidUpdate(prevProps: Props): void {
    // clear out all DOM element children
    while (this.el.firstChild) {
      this.el.removeChild(this.el.firstChild);
    }
    this.el.appendChild(createElement(this.props.data));
  }

  render(): ?React.Element<any> {
    return (

 { this.el = el; }} />
    );
  }
}
<|endoftext|>


<commit_before>/*!
 * ${copyright}
 */

// Provides the concept of a sorter for list bindings
sap.ui.define(['sap/ui/base/Object'],
	function(BaseObject) {
	"use strict";

	/**
	 *
	 * Constructor for Sorter
	 *
	 * @class
	 * Sorter for the list binding
	 * This object defines the sort order for the list binding.
	 *
	 *
	 * @param {String} sPath the binding path used for sorting
	 * @param {boolean} [bDescending=false] whether the sort order should be descending
	 * @param {boolean|function} [vGroup] configure grouping of the content, can either be true to enable grouping
	 *        based on the raw model property value, or a function which calculates the group value out of the
	 *        context (e.g. oContext.getProperty("date").getYear() for year grouping). The control needs to
	 *        implement the grouping behaviour for the aggregation which you want to group. In case a function
	 *        is provided it must either return a primitive type value as the group key or an object containing
	 *        a "key" property an may contain additional properties needed for group visualization.
	 * @param {function} [fnComparator] a custom comparator function, which is used for clientside sorting instead
	 *        of the default comparator method.
	 * @public
	 * @alias sap.ui.model.Sorter
	 * @extends sap.ui.base.Object
	 */
	var Sorter = BaseObject.extend("sap.ui.model.Sorter", /** @lends sap.ui.model.Sorter.prototype */ {

		constructor : function(sPath, bDescending, vGroup, fnComparator){
			if (typeof sPath === "object") {
				var oSorterData = sPath;
				sPath = oSorterData.path;
				bDescending = oSorterData.descending;
				vGroup = oSorterData.group;
				fnComparator = oSorterData.comparator;
			}
			this.sPath = sPath;

			// if a model separator is found in the path, extract model name
			var iSeparatorPos = this.sPath.indexOf(">");
			if (iSeparatorPos > 0) {
				// Model names are ignored, this must be kept for compatibility reasons. But using model names in the
				// sorter path make no technical sense as the binding cannot access any other models.
				jQuery.sap.log.error("Model names are not allowed in sorter-paths: \"" + this.sPath + "\"");
				this.sPath = this.sPath.substr(iSeparatorPos + 1);
			}

			this.bDescending = bDescending;
			this.vGroup = vGroup;
			if (typeof vGroup == "boolean" && vGroup) {
				this.fnGroup = function(oContext) {
					return oContext.getProperty(this.sPath);
				};
			}
			if (typeof vGroup == "function") {
				this.fnGroup = vGroup;
			}
			this.fnCompare = fnComparator;
		},

		/**
		 * Returns a group object, at least containing a key property for group detection.
		 * May contain additional properties as provided by a custom group function.
		 *
		 * @param {sap.ui.model.Context} oContext the binding context
		 * @return {object} An object containing a key property and optional custom properties
		 * @public
		 */
		getGroup : function(oContext) {
			var oGroup = this.fnGroup(oContext);
			if (typeof oGroup === "string" || typeof oGroup === "number" || typeof oGroup === "boolean" || oGroup == null) {
				oGroup = {
					key: oGroup
				};
			}
			return oGroup;
		}

	});

	/**
	 * Compares two values
	 *
	 * This is the default comparator function used for clientside sorting, if no custom comparator is given in the
	 * constructor. It does compare just by using equal/less than/greater than with automatic type casting, except
	 * for null values, which are always last, and string values where localeCompare is used.
	 *
	 * The comparator method returns -1, 0 or 1, depending on the order of the two items and is
	 * suitable to be used as a comparator method for Array.sort.
	 *
	 * @param {any} a the first value to compare
	 * @param {any} b the second value to compare
	 * @returns {int} -1, 0 or 1 depending on the compare result
	 * @public
	 */
	Sorter.defaultComparator = function(a, b) {
		if (a == b) {
			return 0;
		}
		if (b == null) {
			return -1;
		}
		if (a == null) {
			return 1;
		}
		if (typeof a == "string" && typeof b == "string") {
			return a.localeCompare(b);
		}
		if (a < b) {
			return -1;
		}
		if (a > b) {
			return 1;
		}
		return 0;
	};

	return Sorter;

});
<commit_msg>[FEATURE] Sorter: Provide a getter for the group function<commit_after>/*!
 * ${copyright}
 */

// Provides the concept of a sorter for list bindings
sap.ui.define(['sap/ui/base/Object'],
	function(BaseObject) {
	"use strict";

	/**
	 *
	 * Constructor for Sorter
	 *
	 * @class
	 * Sorter for the list binding
	 * This object defines the sort order for the list binding.
	 *
	 *
	 * @param {String} sPath the binding path used for sorting
	 * @param {boolean} [bDescending=false] whether the sort order should be descending
	 * @param {boolean|function} [vGroup] configure grouping of the content, can either be true to enable grouping
	 *        based on the raw model property value, or a function which calculates the group value out of the
	 *        context (e.g. oContext.getProperty("date").getYear() for year grouping). The control needs to
	 *        implement the grouping behaviour for the aggregation which you want to group. In case a function
	 *        is provided it must either return a primitive type value as the group key or an object containing
	 *        a "key" property an may contain additional properties needed for group visualization.
	 * @param {function} [fnComparator] a custom comparator function, which is used for clientside sorting instead
	 *        of the default comparator method.
	 * @public
	 * @alias sap.ui.model.Sorter
	 * @extends sap.ui.base.Object
	 */
	var Sorter = BaseObject.extend("sap.ui.model.Sorter", /** @lends sap.ui.model.Sorter.prototype */ {

		constructor : function(sPath, bDescending, vGroup, fnComparator){
			if (typeof sPath === "object") {
				var oSorterData = sPath;
				sPath = oSorterData.path;
				bDescending = oSorterData.descending;
				vGroup = oSorterData.group;
				fnComparator = oSorterData.comparator;
			}
			this.sPath = sPath;

			// if a model separator is found in the path, extract model name
			var iSeparatorPos = this.sPath.indexOf(">");
			if (iSeparatorPos > 0) {
				// Model names are ignored, this must be kept for compatibility reasons. But using model names in the
				// sorter path make no technical sense as the binding cannot access any other models.
				jQuery.sap.log.error("Model names are not allowed in sorter-paths: \"" + this.sPath + "\"");
				this.sPath = this.sPath.substr(iSeparatorPos + 1);
			}

			this.bDescending = bDescending;
			this.vGroup = vGroup;
			if (typeof vGroup == "boolean" && vGroup) {
				this.fnGroup = function(oContext) {
					return oContext.getProperty(this.sPath);
				};
			}
			if (typeof vGroup == "function") {
				this.fnGroup = vGroup;
			}
			this.fnCompare = fnComparator;
		},

		/**
		 * Returns a group object, at least containing a key property for group detection.
		 * May contain additional properties as provided by a custom group function.
		 *
		 * @param {sap.ui.model.Context} oContext the binding context
		 * @return {object} An object containing a key property and optional custom properties
		 * @public
		 */
		getGroup : function(oContext) {
			var oGroup = this.fnGroup(oContext);
			if (typeof oGroup === "string" || typeof oGroup === "number" || typeof oGroup === "boolean" || oGroup == null) {
				oGroup = {
					key: oGroup
				};
			}
			return oGroup;
		},

		/**
		 * Returns the group function of this Sorter. If grouping is not enabled on this Sorter, it will return
		 * undefined, if no explicit group function has been defined the default group function is returned.
		 * The returned function is bound to its Sorter, so it will group according to its own property path,
		 * even if it is used in the context of another Sorter.
		 *
		 * @return {function} The group function
		 * @public
		 */
		getGroupFunction : function() {
			return this.fnGroup.bind(this);
		}

	});

	/**
	 * Compares two values
	 *
	 * This is the default comparator function used for clientside sorting, if no custom comparator is given in the
	 * constructor. It does compare just by using equal/less than/greater than with automatic type casting, except
	 * for null values, which are always last, and string values where localeCompare is used.
	 *
	 * The comparator method returns -1, 0 or 1, depending on the order of the two items and is
	 * suitable to be used as a comparator method for Array.sort.
	 *
	 * @param {any} a the first value to compare
	 * @param {any} b the second value to compare
	 * @returns {int} -1, 0 or 1 depending on the compare result
	 * @public
	 */
	Sorter.defaultComparator = function(a, b) {
		if (a == b) {
			return 0;
		}
		if (b == null) {
			return -1;
		}
		if (a == null) {
			return 1;
		}
		if (typeof a == "string" && typeof b == "string") {
			return a.localeCompare(b);
		}
		if (a < b) {
			return -1;
		}
		if (a > b) {
			return 1;
		}
		return 0;
	};

	return Sorter;

});
<|endoftext|>


<commit_before>window.multigraph.util.namespace("window.multigraph.utilityFunctions", function (ns) {
    "use strict";

    ns.parseBoolean = function (param) {
        switch (param.toLowerCase()) {
            case "true":
            case "yes":
                return true;
            case "false":
            case "no":
                return false;
            default:
                return param;
        }
    };

});
<commit_msg>adds utility_functions which abstract parsing<commit_after>window.multigraph.util.namespace("window.multigraph.utilityFunctions", function (ns) {
    "use strict";

    ns.parseAttribute = function (value, attribute, preprocessor) {
        if (value !== undefined) {
            attribute(preprocessor(value));
            return true;
        }
        return false;
    };

    ns.parseString = function (value) {
        return value;
    };

    ns.parseInteger = function (value) {
        return parseInt(value, 10);
    };

    ns.parseDataValue = function (type) {
        return function (value) {
            return window.multigraph.core.DataValue.parse(type, value);
        };
    };

    ns.parseDataMeasure = function (type) {
        return function (value) {
            return window.multigraph.core.DataMeasure.parse(type, value);
        };
    };

    ns.parseBoolean = function (param) {
        switch (param.toLowerCase()) {
            case "true":
            case "yes":
                return true;
            case "false":
            case "no":
                return false;
            default:
                return param;
        }
    };

});
<|endoftext|>


<commit_before>// Copyright (c) 2011 gocept gmbh & co. kg
// See also LICENSE.txt

zeit.cms.ObjectSequenceWidget = gocept.Class.extend({

    construct: function(widget_id, accept) {
        var self = this;
        self.widget_id = widget_id;
        self.element = $(widget_id);
        self.ul_element = MochiKit.DOM.getFirstElementByTagAndClassName(
            'ul', null, self.element);

        self.initialize_autocomplete();
        self.initialize();
        // XXX Need to unregister those events
        MochiKit.Signal.connect(
            self.element, 'onclick', self, self.handleClick);
        new MochiKit.DragAndDrop.Droppable(self.element, {
            accept: accept,
            activeclass: 'droppable-active',
            hoverclass: 'hover-content',
            ondrop: function(element, last_active_element, event) {
                self.handleDrop(element);
            }
        });
    },

    initialize_autocomplete: function() {
        var self = this;
        self.autocomplete = MochiKit.DOM.getFirstElementByTagAndClassName(
            'input', 'autocomplete', self.element);
        if (isNull(self.autocomplete)) {
            return;
        }
        jQuery(self.autocomplete).autocomplete({
            source: self.autocomplete.getAttribute('cms:autocomplete-source'),
            select: function(event, ui) {
                self.add(ui.item.value);
            }
        });
    },

    initialize: function() {
        var self = this;
        MochiKit.Sortable.destroy(self.ul_element);
        self.ul_element.innerHTML = '';
        var i = 0;
        forEach(
            MochiKit.DOM.getElementsByTagAndClassName(
                'input', 'uniqueId', self.element),
            function(input) {
                MochiKit.DOM.appendChildNodes(
                    self.ul_element,
                    self.renderElement(i, input.value));
                i += 1;
        });
        MochiKit.Sortable.create(
            self.ul_element, {
            onUpdate: function() { self.update_order_from_ul(); }
            });
        if (i == 0 && isNull(self.autocomplete)) {
            self.ul_element.appendChild(LI(
                {'class': 'new'},
                'Ziehen Sie Inhalte hierher um sie zu verknüpfen.'));
            MochiKit.DOM.addElementClass(self.element, 'landing-zone');
            MochiKit.DOM.addElementClass(self.element, 'visible');
        } else {
            MochiKit.DOM.removeElementClass(self.element, 'landing-zone');
            MochiKit.DOM.removeElementClass(self.element, 'visible');
        }
    },

    renderElement: function(index, uniqueId) {
        var self = this;
        var li_id = self.widget_id + '_sort_li_' + index;
        var li = LI({'class': 'element busy', 'index': index, 'id': li_id});
        var d = zeit.cms.load_object_details(uniqueId);
        d.addCallbacks(
            function(result) {
                li.innerHTML = result;
                li.insertBefore(
                    A({href: index, rel: "remove", title: "Entfernen"}),
                    li.firstChild);
                return result;
            },
            function(error) {
                zeit.cms.log_error(error);
                li.innerHTML = "Fehler beim Laden";
                return error;
            });
        d.addBoth(function(result) {
            MochiKit.DOM.removeElementClass(li, 'busy');
            return result;
        });
        return li;
    },

    increaseCount: function() {
        var count_field = this.getCountField();
        var count = Number(count_field.value);
        count_field.value = count + 1;
        return count_field.value;
    },

    decreaseCount: function() {
        var count_field = this.getCountField();
        var count = Number(count_field.value);
        count_field.value = count - 1;
        return count_field.value;
    },

    getCountField: function() {
        return MochiKit.DOM.getElement(this.widget_id + '.count');
    },

    getValueField: function(index) {
        return MochiKit.DOM.getElement(this.getValueFieldName(index));
    },

    getValueFieldName: function(index) {
        var self = this;
        return self.widget_id + '.' + index;
    },

    add: function(value) {
        var self = this;
        var next_id = self.increaseCount() - 1;

        var id_field_name = self.getValueFieldName(next_id);

        MochiKit.DOM.appendChildNodes(
            self.element,
            INPUT({'type': 'hidden',
                   'class': 'uniqueId',
                   'name': id_field_name,
                   'id': id_field_name,
                   'value': value}));
        this.initialize();
        self.changed();
    },

    remove: function(index) {
        var self = this;

        MochiKit.DOM.removeElement(self.getValueField(index));

        var new_index = 0;
        self.iterFields(function(value_field) {
            var value_field_name = self.getValueFieldName(new_index);
            value_field.setAttribute('name', value_field_name);
            value_field.setAttribute('id', value_field_name);
            new_index += 1;
        });
        self.decreaseCount();
        self.initialize();
        self.changed();
    },

    iterFields: function(callable) {
        var othis = this;
        var count_field = this.getCountField();
        var amount = Number(count_field.value);
        forEach(MochiKit.Iter.range(amount), function(iteration_index) {
            var value_field = othis.getValueField(iteration_index);
            if (value_field === null) {
                return;
            }
            callable(value_field);
        });
    },

    handleClick: function(event) {
        var self = this;
        var target = event.target();
        var action;
        var argument;
        if (target.nodeName == 'A' &&
            target.rel) {
            action = target.rel;
            argument = target.getAttribute('href');
        }
        if (action && argument) {
            self[action](argument);
            event.stop();
        }
    },

    handleDrop: function(element) {
        var self = this;
        if (MochiKit.DOM.isChildNode(element, self.ul_element)) {
            return;
        }
        var unique_id = element.uniqueId;
        if (unique_id) {
            self.add.call(self, unique_id);
        }
    },

    changed: function() {
        var self = this;
        MochiKit.Signal.signal(
            self.getCountField(), 'onchange', {target: self.getCountField()});
    },

    update_order_from_ul: function() {
        var self = this;
        var ordered_ids = [];
        forEach(
            MochiKit.DOM.getElementsByTagAndClassName(
                'li', null, self.ul_element),
            function(li) {
                var index = li.getAttribute('index');
                if (!isUndefinedOrNull(index)) {
                    ordered_ids.push(self.getValueField(index).value);
                }
        });
        var i;
        for (i = 0; i < ordered_ids.length; i++) {
            self.getValueField(i).value = ordered_ids[i];
        }
        self.changed();
    },

    show_add_view: function(add_view) {
        var self = this;
        var url = window.application_url + '/@@' + add_view;
        self.lightbox = new zeit.cms.ObjectAddForm(url, $('body'));
        self.lightbox.events.push(MochiKit.Signal.connect(
            self.lightbox, 'zeit.cms.ObjectReferenceWidget.selected',
            function(uniqueId) {
                self.add(uniqueId);
                self.lightbox.close();
            }));
    }

});

zeit.cms.DropObjectWidget = gocept.Class.extend({

    construct: function(element, accept) {
        var self = this;
        self.element = element;
        self.input = MochiKit.DOM.getFirstElementByTagAndClassName(
            'input', null, self.element);
        self.details = MochiKit.DOM.getFirstElementByTagAndClassName(
            'div', 'object-reference', self.element);
        new MochiKit.DragAndDrop.Droppable(self.element, {
            accept: accept,
            activeclass: 'droppable-active',
            hoverclass: 'hover-content',
            ondrop: function(element, last_active_element, event) {
                    self.handleDrop(element);
                    }});
        MochiKit.Signal.connect(
            self.element, 'onclick', self, self.handleClick);
        self.update_details();
    },

    handleDrop: function(element) {
        var self = this;
        self.input.value = element.uniqueId;
        self.changed();
        self.update_details();
    },

    remove: function() {
        var self = this;
        self.input.value = '';
        self.changed();
        self.update_details();
    },

    changed: function() {
        var self = this;
        MochiKit.Signal.signal(
            self.input, 'onchange', {target: self.input});
    },

    update_details: function() {
        var self = this;
        if (self.input.value) {
            MochiKit.DOM.addElementClass(self.element, 'busy');
            var d = zeit.cms.load_object_details(self.input.value);
            d.addCallbacks(
                function(result) {
                    self.details.innerHTML = result;
                    self.details.insertBefore(
                        A({href: '#', rel: "remove", title: "Entfernen"}),
                        self.details.firstChild);
                    return result;
                },
                function(error) {
                    self.etails.innerHTML = 'Fehler';
                    return error;
                });
            d.addBoth(function(result) {
                MochiKit.DOM.removeElementClass(self.element, 'busy');
                MochiKit.DOM.removeElementClass(self.element, 'landing-zone');
                MochiKit.DOM.removeElementClass(self.element, 'visible');
                });
        } else {
            self.details.innerHTML =
                'Ziehen Sie Inhalte hierher um sie zu verknüpfen.';
            MochiKit.DOM.addElementClass(self.element, 'landing-zone');
            MochiKit.DOM.addElementClass(self.element, 'visible');
        }
    },

    handleClick: function(event) {
        var self = this;
        var target = event.target();
        var action;
        var argument;
        if (target.nodeName == 'A' &&
            target.rel) {
            action = target.rel;
            argument = target.getAttribute('href');
        }
        if (action) {
            self[action](argument);
            event.stop();
        }
    }

});

zeit.cms._load_object_details_cache = {};

zeit.cms.load_object_details = function(uniqueId) {
    var content = zeit.cms._load_object_details_cache[uniqueId];
    var d = new MochiKit.Async.Deferred();
    if (isUndefinedOrNull(content)) {
        d = MochiKit.Async.doSimpleXMLHttpRequest(
            application_url + '/@@redirect_to', {
            unique_id: uniqueId,
            view: '@@object-details'});
        d.addCallback(function(result) {
            // Store the result for later use. This is just magnitudes
            // faster than an HTTP request.
            zeit.cms._load_object_details_cache[uniqueId] =
                result.responseText;
            return result.responseText;
        });
    } else {
        d = new MochiKit.Async.Deferred();
        d.callback(content);
    }
    return d;
};
<commit_msg>added XXX comment on i18n<commit_after>// Copyright (c) 2011 gocept gmbh & co. kg
// See also LICENSE.txt

zeit.cms.ObjectSequenceWidget = gocept.Class.extend({

    construct: function(widget_id, accept) {
        var self = this;
        self.widget_id = widget_id;
        self.element = $(widget_id);
        self.ul_element = MochiKit.DOM.getFirstElementByTagAndClassName(
            'ul', null, self.element);

        self.initialize_autocomplete();
        self.initialize();
        // XXX Need to unregister those events
        MochiKit.Signal.connect(
            self.element, 'onclick', self, self.handleClick);
        new MochiKit.DragAndDrop.Droppable(self.element, {
            accept: accept,
            activeclass: 'droppable-active',
            hoverclass: 'hover-content',
            ondrop: function(element, last_active_element, event) {
                self.handleDrop(element);
            }
        });
    },

    initialize_autocomplete: function() {
        var self = this;
        self.autocomplete = MochiKit.DOM.getFirstElementByTagAndClassName(
            'input', 'autocomplete', self.element);
        if (isNull(self.autocomplete)) {
            return;
        }
        jQuery(self.autocomplete).autocomplete({
            source: self.autocomplete.getAttribute('cms:autocomplete-source'),
            select: function(event, ui) {
                self.add(ui.item.value);
            }
        });
    },

    initialize: function() {
        var self = this;
        MochiKit.Sortable.destroy(self.ul_element);
        self.ul_element.innerHTML = '';
        var i = 0;
        forEach(
            MochiKit.DOM.getElementsByTagAndClassName(
                'input', 'uniqueId', self.element),
            function(input) {
                MochiKit.DOM.appendChildNodes(
                    self.ul_element,
                    self.renderElement(i, input.value));
                i += 1;
        });
        MochiKit.Sortable.create(
            self.ul_element, {
            onUpdate: function() { self.update_order_from_ul(); }
            });
        if (i == 0 && isNull(self.autocomplete)) {
            self.ul_element.appendChild(LI(
                {'class': 'new'},
                'Ziehen Sie Inhalte hierher um sie zu verknüpfen.'));
            MochiKit.DOM.addElementClass(self.element, 'landing-zone');
            MochiKit.DOM.addElementClass(self.element, 'visible');
        } else {
            MochiKit.DOM.removeElementClass(self.element, 'landing-zone');
            MochiKit.DOM.removeElementClass(self.element, 'visible');
        }
    },

    renderElement: function(index, uniqueId) {
        var self = this;
        var li_id = self.widget_id + '_sort_li_' + index;
        var li = LI({'class': 'element busy', 'index': index, 'id': li_id});
        var d = zeit.cms.load_object_details(uniqueId);
        d.addCallbacks(
            function(result) {
                li.innerHTML = result;
                li.insertBefore(
                    // XXX proper i18n
                    A({href: index, rel: "remove", title: "Entfernen"}),
                    li.firstChild);
                return result;
            },
            function(error) {
                zeit.cms.log_error(error);
                li.innerHTML = "Fehler beim Laden";
                return error;
            });
        d.addBoth(function(result) {
            MochiKit.DOM.removeElementClass(li, 'busy');
            return result;
        });
        return li;
    },

    increaseCount: function() {
        var count_field = this.getCountField();
        var count = Number(count_field.value);
        count_field.value = count + 1;
        return count_field.value;
    },

    decreaseCount: function() {
        var count_field = this.getCountField();
        var count = Number(count_field.value);
        count_field.value = count - 1;
        return count_field.value;
    },

    getCountField: function() {
        return MochiKit.DOM.getElement(this.widget_id + '.count');
    },

    getValueField: function(index) {
        return MochiKit.DOM.getElement(this.getValueFieldName(index));
    },

    getValueFieldName: function(index) {
        var self = this;
        return self.widget_id + '.' + index;
    },

    add: function(value) {
        var self = this;
        var next_id = self.increaseCount() - 1;

        var id_field_name = self.getValueFieldName(next_id);

        MochiKit.DOM.appendChildNodes(
            self.element,
            INPUT({'type': 'hidden',
                   'class': 'uniqueId',
                   'name': id_field_name,
                   'id': id_field_name,
                   'value': value}));
        this.initialize();
        self.changed();
    },

    remove: function(index) {
        var self = this;

        MochiKit.DOM.removeElement(self.getValueField(index));

        var new_index = 0;
        self.iterFields(function(value_field) {
            var value_field_name = self.getValueFieldName(new_index);
            value_field.setAttribute('name', value_field_name);
            value_field.setAttribute('id', value_field_name);
            new_index += 1;
        });
        self.decreaseCount();
        self.initialize();
        self.changed();
    },

    iterFields: function(callable) {
        var othis = this;
        var count_field = this.getCountField();
        var amount = Number(count_field.value);
        forEach(MochiKit.Iter.range(amount), function(iteration_index) {
            var value_field = othis.getValueField(iteration_index);
            if (value_field === null) {
                return;
            }
            callable(value_field);
        });
    },

    handleClick: function(event) {
        var self = this;
        var target = event.target();
        var action;
        var argument;
        if (target.nodeName == 'A' &&
            target.rel) {
            action = target.rel;
            argument = target.getAttribute('href');
        }
        if (action && argument) {
            self[action](argument);
            event.stop();
        }
    },

    handleDrop: function(element) {
        var self = this;
        if (MochiKit.DOM.isChildNode(element, self.ul_element)) {
            return;
        }
        var unique_id = element.uniqueId;
        if (unique_id) {
            self.add.call(self, unique_id);
        }
    },

    changed: function() {
        var self = this;
        MochiKit.Signal.signal(
            self.getCountField(), 'onchange', {target: self.getCountField()});
    },

    update_order_from_ul: function() {
        var self = this;
        var ordered_ids = [];
        forEach(
            MochiKit.DOM.getElementsByTagAndClassName(
                'li', null, self.ul_element),
            function(li) {
                var index = li.getAttribute('index');
                if (!isUndefinedOrNull(index)) {
                    ordered_ids.push(self.getValueField(index).value);
                }
        });
        var i;
        for (i = 0; i < ordered_ids.length; i++) {
            self.getValueField(i).value = ordered_ids[i];
        }
        self.changed();
    },

    show_add_view: function(add_view) {
        var self = this;
        var url = window.application_url + '/@@' + add_view;
        self.lightbox = new zeit.cms.ObjectAddForm(url, $('body'));
        self.lightbox.events.push(MochiKit.Signal.connect(
            self.lightbox, 'zeit.cms.ObjectReferenceWidget.selected',
            function(uniqueId) {
                self.add(uniqueId);
                self.lightbox.close();
            }));
    }

});

zeit.cms.DropObjectWidget = gocept.Class.extend({

    construct: function(element, accept) {
        var self = this;
        self.element = element;
        self.input = MochiKit.DOM.getFirstElementByTagAndClassName(
            'input', null, self.element);
        self.details = MochiKit.DOM.getFirstElementByTagAndClassName(
            'div', 'object-reference', self.element);
        new MochiKit.DragAndDrop.Droppable(self.element, {
            accept: accept,
            activeclass: 'droppable-active',
            hoverclass: 'hover-content',
            ondrop: function(element, last_active_element, event) {
                    self.handleDrop(element);
                    }});
        MochiKit.Signal.connect(
            self.element, 'onclick', self, self.handleClick);
        self.update_details();
    },

    handleDrop: function(element) {
        var self = this;
        self.input.value = element.uniqueId;
        self.changed();
        self.update_details();
    },

    remove: function() {
        var self = this;
        self.input.value = '';
        self.changed();
        self.update_details();
    },

    changed: function() {
        var self = this;
        MochiKit.Signal.signal(
            self.input, 'onchange', {target: self.input});
    },

    update_details: function() {
        var self = this;
        if (self.input.value) {
            MochiKit.DOM.addElementClass(self.element, 'busy');
            var d = zeit.cms.load_object_details(self.input.value);
            d.addCallbacks(
                function(result) {
                    self.details.innerHTML = result;
                    self.details.insertBefore(
                        A({href: '#', rel: "remove", title: "Entfernen"}),
                        self.details.firstChild);
                    return result;
                },
                function(error) {
                    self.etails.innerHTML = 'Fehler';
                    return error;
                });
            d.addBoth(function(result) {
                MochiKit.DOM.removeElementClass(self.element, 'busy');
                MochiKit.DOM.removeElementClass(self.element, 'landing-zone');
                MochiKit.DOM.removeElementClass(self.element, 'visible');
                });
        } else {
            self.details.innerHTML =
                'Ziehen Sie Inhalte hierher um sie zu verknüpfen.';
            MochiKit.DOM.addElementClass(self.element, 'landing-zone');
            MochiKit.DOM.addElementClass(self.element, 'visible');
        }
    },

    handleClick: function(event) {
        var self = this;
        var target = event.target();
        var action;
        var argument;
        if (target.nodeName == 'A' &&
            target.rel) {
            action = target.rel;
            argument = target.getAttribute('href');
        }
        if (action) {
            self[action](argument);
            event.stop();
        }
    }

});

zeit.cms._load_object_details_cache = {};

zeit.cms.load_object_details = function(uniqueId) {
    var content = zeit.cms._load_object_details_cache[uniqueId];
    var d = new MochiKit.Async.Deferred();
    if (isUndefinedOrNull(content)) {
        d = MochiKit.Async.doSimpleXMLHttpRequest(
            application_url + '/@@redirect_to', {
            unique_id: uniqueId,
            view: '@@object-details'});
        d.addCallback(function(result) {
            // Store the result for later use. This is just magnitudes
            // faster than an HTTP request.
            zeit.cms._load_object_details_cache[uniqueId] =
                result.responseText;
            return result.responseText;
        });
    } else {
        d = new MochiKit.Async.Deferred();
        d.callback(content);
    }
    return d;
};
<|endoftext|>