<commit_before>'use strict';
const os = require('os');
const path = require('path');

const { app, shell, BrowserWindow, Menu } = require('electron');

const fs = require('fs-extra');

const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');

const appName = app.getName();

class AppMenu {
	getHistorySubmenu() {
		return [{
			label: 'Back',
			accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('back');
				}
			}
		}, {
			label: 'Forward',
			accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('forward');
				}
			}
		}];
	}

	getViewSubmenu() {
		return [{
			label: 'Reload',
			accelerator: 'CommandOrControl+R',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('reload-current-viewer');
				}
			}
		}, {
			label: 'Hard Reload',
			accelerator: 'CommandOrControl+Shift+R',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('hard-reload');
				}
			}
		}, {
			type: 'separator'
		}, {
			role: 'togglefullscreen'
		}, {
			label: 'Zoom In',
			accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomIn');
				}
			}
		}, {
			label: 'Zoom Out',
			accelerator: 'CommandOrControl+-',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomOut');
				}
			}
		}, {
			label: 'Actual Size',
			accelerator: 'CommandOrControl+0',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomActualSize');
				}
			}
		}, {
			type: 'separator'
		}, {
			label: 'Toggle Tray Icon',
			click(item, focusedWindow) {
				if (focusedWindow) {
					focusedWindow.webContents.send('toggletray');
				}
			}
		}, {
			label: 'Toggle Sidebar',
			accelerator: 'CommandOrControl+S',
			click(item, focusedWindow) {
				if (focusedWindow) {
					const newValue = !ConfigUtil.getConfigItem('showSidebar');
					focusedWindow.webContents.send('toggle-sidebar', newValue);
					ConfigUtil.setConfigItem('showSidebar', newValue);
				}
			}
		}, {
			label: 'Toggle DevTools for Zulip App',
			accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
			click(item, focusedWindow) {
				if (focusedWindow) {
					focusedWindow.webContents.toggleDevTools();
				}
			}
		}, {
			label: 'Toggle DevTools for Active Tab',
			accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('tab-devtools');
				}
			}
		}];
	}

	getHelpSubmenu() {
		return [{
			label: `${appName} Website`,
			click() {
				shell.openExternal('https://zulipchat.com/help/');
			}
		}, {
			label: `${appName + ' Desktop'} - ${app.getVersion()}`,
			enabled: false
		}, {
			label: 'Report an Issue...',
			click() {
				const body = `
					<!-- Please succinctly describe your issue and steps to reproduce it. -->
					-
					${app.getName()} ${app.getVersion()}
					Electron ${process.versions.electron}
					${process.platform} ${process.arch} ${os.release()}`;
				shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
			}
		}];
	}

	getWindowSubmenu(tabs, activeTabIndex) {
		const initialSubmenu = [{
			role: 'minimize'
		}, {
			role: 'close'
		}];

		if (tabs.length > 0) {
			const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
			initialSubmenu.push({
				type: 'separator'
			});
			for (let i = 0; i < tabs.length; i++) {
				initialSubmenu.push({
					label: tabs[i].webview.props.name,
					accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
					checked: tabs[i].props.index === activeTabIndex,
					click(item, focusedWindow) {
						if (focusedWindow) {
							AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
						}
					},
					type: 'radio'
				});
			}
		}

		return initialSubmenu;
	}

	getDarwinTpl(props) {
		const { tabs, activeTabIndex } = props;

		return [{
			label: `${app.getName()}`,
			submenu: [{
				label: 'About Zulip',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-about');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Settings',
				accelerator: 'Cmd+,',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-settings');
					}
				}
			}, {
				label: 'Keyboard Shortcuts',
				accelerator: 'Cmd+K',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('shortcut');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Reset App Settings',
				accelerator: 'Command+Shift+D',
				click() {
					AppMenu.resetAppSettings();
				}
			}, {
				label: 'Log Out',
				accelerator: 'Cmd+L',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('log-out');
					}
				}
			}, {
				type: 'separator'
			}, {
				role: 'services',
				submenu: []
			}, {
				type: 'separator'
			}, {
				role: 'hide'
			}, {
				role: 'hideothers'
			}, {
				role: 'unhide'
			}, {
				type: 'separator'
			}, {
				role: 'quit'
			}]
		}, {
			label: 'Edit',
			submenu: [{
				role: 'undo'
			}, {
				role: 'redo'
			}, {
				type: 'separator'
			}, {
				role: 'cut'
			}, {
				role: 'copy'
			}, {
				role: 'paste'
			}, {
				role: 'pasteandmatchstyle'
			}, {
				role: 'delete'
			}, {
				role: 'selectall'
			}]
		}, {
			label: 'View',
			submenu: this.getViewSubmenu()
		}, {
			label: 'History',
			submenu: this.getHistorySubmenu()
		}, {
			label: 'Window',
			submenu: this.getWindowSubmenu(tabs, activeTabIndex)
		}, {
			role: 'help',
			submenu: this.getHelpSubmenu()
		}];
	}

	getOtherTpl(props) {
		const { tabs, activeTabIndex } = props;

		return [{
			label: 'File',
			submenu: [{
				label: 'About Zulip',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-about');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Settings',
				accelerator: 'Ctrl+,',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-settings');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Keyboard Shortcuts',
				accelerator: 'Ctrl+K',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('shortcut');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Reset App Settings',
				accelerator: 'Ctrl+Shift+D',
				click() {
					AppMenu.resetAppSettings();
				}
			}, {
				label: 'Log Out',
				accelerator: 'Ctrl+L',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('log-out');
					}
				}
			}, {
				type: 'separator'
			}, {
				role: 'quit',
				accelerator: 'Ctrl+Q'
			}]
		}, {
			label: 'Edit',
			submenu: [{
				role: 'undo'
			}, {
				role: 'redo'
			}, {
				type: 'separator'
			}, {
				role: 'cut'
			}, {
				role: 'copy'
			}, {
				role: 'paste'
			}, {
				role: 'pasteandmatchstyle'
			}, {
				role: 'delete'
			}, {
				type: 'separator'
			}, {
				role: 'selectall'
			}]
		}, {
			label: 'View',
			submenu: this.getViewSubmenu()
		}, {
			label: 'History',
			submenu: this.getHistorySubmenu()
		}, {
			label: 'Window',
			submenu: this.getWindowSubmenu(tabs, activeTabIndex)
		}, {
			role: 'help',
			submenu: this.getHelpSubmenu()
		}];
	}

	static sendAction(action, ...params) {
		const win = BrowserWindow.getAllWindows()[0];

		if (process.platform === 'darwin') {
			win.restore();
		}

		win.webContents.send(action, ...params);
	}

	static resetAppSettings() {
		// We save App's settings/configurations in following files
		const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];

		settingFiles.forEach(settingFileName => {
			const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
			fs.access(getSettingFilesPath, error => {
				if (error) {
					console.log(error);
				} else {
					fs.unlink(getSettingFilesPath, () => {
						AppMenu.sendAction('clear-app-data');
					});
				}
			});
		});
	}

	setMenu(props) {
		const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
		const menu = Menu.buildFromTemplate(tpl);
		Menu.setApplicationMenu(menu);
	}
}

module.exports = new AppMenu();
<commit_msg>menu: Rename Settings to Desktop App Settings.<commit_after>'use strict';
const os = require('os');
const path = require('path');

const { app, shell, BrowserWindow, Menu } = require('electron');

const fs = require('fs-extra');

const ConfigUtil = require(__dirname + '/../renderer/js/utils/config-util.js');

const appName = app.getName();

class AppMenu {
	getHistorySubmenu() {
		return [{
			label: 'Back',
			accelerator: process.platform === 'darwin' ? 'Command+Left' : 'Alt+Left',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('back');
				}
			}
		}, {
			label: 'Forward',
			accelerator: process.platform === 'darwin' ? 'Command+Right' : 'Alt+Right',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('forward');
				}
			}
		}];
	}

	getViewSubmenu() {
		return [{
			label: 'Reload',
			accelerator: 'CommandOrControl+R',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('reload-current-viewer');
				}
			}
		}, {
			label: 'Hard Reload',
			accelerator: 'CommandOrControl+Shift+R',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('hard-reload');
				}
			}
		}, {
			type: 'separator'
		}, {
			role: 'togglefullscreen'
		}, {
			label: 'Zoom In',
			accelerator: process.platform === 'darwin' ? 'Command+Plus' : 'Control+=',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomIn');
				}
			}
		}, {
			label: 'Zoom Out',
			accelerator: 'CommandOrControl+-',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomOut');
				}
			}
		}, {
			label: 'Actual Size',
			accelerator: 'CommandOrControl+0',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('zoomActualSize');
				}
			}
		}, {
			type: 'separator'
		}, {
			label: 'Toggle Tray Icon',
			click(item, focusedWindow) {
				if (focusedWindow) {
					focusedWindow.webContents.send('toggletray');
				}
			}
		}, {
			label: 'Toggle Sidebar',
			accelerator: 'CommandOrControl+S',
			click(item, focusedWindow) {
				if (focusedWindow) {
					const newValue = !ConfigUtil.getConfigItem('showSidebar');
					focusedWindow.webContents.send('toggle-sidebar', newValue);
					ConfigUtil.setConfigItem('showSidebar', newValue);
				}
			}
		}, {
			label: 'Toggle DevTools for Zulip App',
			accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
			click(item, focusedWindow) {
				if (focusedWindow) {
					focusedWindow.webContents.toggleDevTools();
				}
			}
		}, {
			label: 'Toggle DevTools for Active Tab',
			accelerator: process.platform === 'darwin' ? 'Alt+Command+U' : 'Ctrl+Shift+U',
			click(item, focusedWindow) {
				if (focusedWindow) {
					AppMenu.sendAction('tab-devtools');
				}
			}
		}];
	}

	getHelpSubmenu() {
		return [{
			label: `${appName} Website`,
			click() {
				shell.openExternal('https://zulipchat.com/help/');
			}
		}, {
			label: `${appName + ' Desktop'} - ${app.getVersion()}`,
			enabled: false
		}, {
			label: 'Report an Issue...',
			click() {
				const body = `
					<!-- Please succinctly describe your issue and steps to reproduce it. -->
					-
					${app.getName()} ${app.getVersion()}
					Electron ${process.versions.electron}
					${process.platform} ${process.arch} ${os.release()}`;
				shell.openExternal(`https://github.com/zulip/zulip-electron/issues/new?body=${encodeURIComponent(body)}`);
			}
		}];
	}

	getWindowSubmenu(tabs, activeTabIndex) {
		const initialSubmenu = [{
			role: 'minimize'
		}, {
			role: 'close'
		}];

		if (tabs.length > 0) {
			const ShortcutKey = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
			initialSubmenu.push({
				type: 'separator'
			});
			for (let i = 0; i < tabs.length; i++) {
				initialSubmenu.push({
					label: tabs[i].webview.props.name,
					accelerator: tabs[i].props.role === 'function' ? '' : `${ShortcutKey} + ${tabs[i].props.index + 1}`,
					checked: tabs[i].props.index === activeTabIndex,
					click(item, focusedWindow) {
						if (focusedWindow) {
							AppMenu.sendAction('switch-server-tab', tabs[i].props.index);
						}
					},
					type: 'radio'
				});
			}
		}

		return initialSubmenu;
	}

	getDarwinTpl(props) {
		const { tabs, activeTabIndex } = props;

		return [{
			label: `${app.getName()}`,
			submenu: [{
				label: 'About Zulip',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-about');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Desktop App Settings',
				accelerator: 'Cmd+,',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-settings');
					}
				}
			}, {
				label: 'Keyboard Shortcuts',
				accelerator: 'Cmd+K',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('shortcut');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Reset App Settings',
				accelerator: 'Command+Shift+D',
				click() {
					AppMenu.resetAppSettings();
				}
			}, {
				label: 'Log Out',
				accelerator: 'Cmd+L',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('log-out');
					}
				}
			}, {
				type: 'separator'
			}, {
				role: 'services',
				submenu: []
			}, {
				type: 'separator'
			}, {
				role: 'hide'
			}, {
				role: 'hideothers'
			}, {
				role: 'unhide'
			}, {
				type: 'separator'
			}, {
				role: 'quit'
			}]
		}, {
			label: 'Edit',
			submenu: [{
				role: 'undo'
			}, {
				role: 'redo'
			}, {
				type: 'separator'
			}, {
				role: 'cut'
			}, {
				role: 'copy'
			}, {
				role: 'paste'
			}, {
				role: 'pasteandmatchstyle'
			}, {
				role: 'delete'
			}, {
				role: 'selectall'
			}]
		}, {
			label: 'View',
			submenu: this.getViewSubmenu()
		}, {
			label: 'History',
			submenu: this.getHistorySubmenu()
		}, {
			label: 'Window',
			submenu: this.getWindowSubmenu(tabs, activeTabIndex)
		}, {
			role: 'help',
			submenu: this.getHelpSubmenu()
		}];
	}

	getOtherTpl(props) {
		const { tabs, activeTabIndex } = props;

		return [{
			label: 'File',
			submenu: [{
				label: 'About Zulip',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-about');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Desktop App Settings',
				accelerator: 'Ctrl+,',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('open-settings');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Keyboard Shortcuts',
				accelerator: 'Ctrl+K',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('shortcut');
					}
				}
			}, {
				type: 'separator'
			}, {
				label: 'Reset App Settings',
				accelerator: 'Ctrl+Shift+D',
				click() {
					AppMenu.resetAppSettings();
				}
			}, {
				label: 'Log Out',
				accelerator: 'Ctrl+L',
				click(item, focusedWindow) {
					if (focusedWindow) {
						AppMenu.sendAction('log-out');
					}
				}
			}, {
				type: 'separator'
			}, {
				role: 'quit',
				accelerator: 'Ctrl+Q'
			}]
		}, {
			label: 'Edit',
			submenu: [{
				role: 'undo'
			}, {
				role: 'redo'
			}, {
				type: 'separator'
			}, {
				role: 'cut'
			}, {
				role: 'copy'
			}, {
				role: 'paste'
			}, {
				role: 'pasteandmatchstyle'
			}, {
				role: 'delete'
			}, {
				type: 'separator'
			}, {
				role: 'selectall'
			}]
		}, {
			label: 'View',
			submenu: this.getViewSubmenu()
		}, {
			label: 'History',
			submenu: this.getHistorySubmenu()
		}, {
			label: 'Window',
			submenu: this.getWindowSubmenu(tabs, activeTabIndex)
		}, {
			role: 'help',
			submenu: this.getHelpSubmenu()
		}];
	}

	static sendAction(action, ...params) {
		const win = BrowserWindow.getAllWindows()[0];

		if (process.platform === 'darwin') {
			win.restore();
		}

		win.webContents.send(action, ...params);
	}

	static resetAppSettings() {
		// We save App's settings/configurations in following files
		const settingFiles = ['window-state.json', 'domain.json', 'settings.json'];

		settingFiles.forEach(settingFileName => {
			const getSettingFilesPath = path.join(app.getPath('appData'), appName, settingFileName);
			fs.access(getSettingFilesPath, error => {
				if (error) {
					console.log(error);
				} else {
					fs.unlink(getSettingFilesPath, () => {
						AppMenu.sendAction('clear-app-data');
					});
				}
			});
		});
	}

	setMenu(props) {
		const tpl = process.platform === 'darwin' ? this.getDarwinTpl(props) : this.getOtherTpl(props);
		const menu = Menu.buildFromTemplate(tpl);
		Menu.setApplicationMenu(menu);
	}
}

module.exports = new AppMenu();
<|endoftext|>


<commit_before>(function() {
    'use strict';

    angular.module('chaise.authen', ['chaise.utils', 'chaise.storage'])

    .factory('Session', ['ConfigUtils', 'messageMap', 'logService', 'modalUtils', 'StorageService', 'UriUtils', '$cookies', '$interval', '$log', '$q', '$rootScope', '$sce', '$uibModalStack', '$window',
        function (ConfigUtils, messageMap, logService, modalUtils, StorageService, UriUtils, $cookies, $interval, $log, $q, $rootScope, $sce, $uibModalStack, $window) {
        // authn API no longer communicates through ermrest, removing the need to check for ermrest location
        var serviceURL = $window.location.origin;

        var LOCAL_STORAGE_KEY = 'session';              // name of object session information is stored under
        var PROMPT_EXPIRATION_KEY = 'promptExpiration'; // name of key for prompt expiration value
        var PREVIOUS_SESSION_KEY = 'previousSession';   // name of key for previous session boolean

        // Private variable to store current session object
        var _session = null;

        var _changeCbs = {};

        var _counter = 0;

        var _executeListeners = function() {
            for (var k in _changeCbs) {
                _changeCbs[k]();
            }
        };

        /**
         * Functions that interact with the StorageService tokens
         * There are 2 keys stored under the LOCAL_STORAGE_KEY object, PROMPT_EXPIRATION_KEY and PREVIOUS_SESSION_KEY
         */

        // verifies value exists for `keyName`
        var _tokenExists = function(keyName) {
            var sessionStorage = StorageService.getStorage(LOCAL_STORAGE_KEY);

            return (sessionStorage && sessionStorage[keyName]);
        };

        // creates an expiration token with `keyName`
        var _createToken = function (keyName) {
            var data = {};
            var hourFromNow = new Date();
            hourFromNow.setHours(hourFromNow.getHours() + 1);

            data[keyName] = hourFromNow.getTime();

            StorageService.updateStorage(LOCAL_STORAGE_KEY, data);
        };

        // removes the key/value pair at `keyName`
        var _removeToken = function (keyName) {
            if (_tokenExists(keyName)) {
                StorageService.deleteStorageValue(LOCAL_STORAGE_KEY, keyName);
            }
        };

        // checks if the expiration token with `keyName` has expired
        var _expiredToken = function (keyName) {
            var sessionStorage = StorageService.getStorage(LOCAL_STORAGE_KEY);

            return (sessionStorage && new Date().getTime() > sessionStorage[keyName]);
        };

        // extends the expiration token with `keyName` if it hasn't expired
        var _extendToken = function (keyName) {
            if (_tokenExists(keyName) && !_expiredToken(keyName)) {
                _createToken(keyName);
            }
        };

        // creates a boolean token with `keyName`
        var _createBool = function (keyName) {
            var data = {};

            data[keyName] = true;

            StorageService.updateStorage(LOCAL_STORAGE_KEY, data);
        };

        var loginWindowCb = function (params, referrerId, cb, type, rejectCb){
            if(type.indexOf('modal')!== -1){
                if (_session) {
                    params.title = messageMap.sessionExpired.title;
                } else {
                    params.title = messageMap.noSession.title;
                }
                var closed = false;
                var onModalCloseSuccess = function () {
                    onModalClose();
                }

                var onModalClose = function(response) {
                    $interval.cancel(intervalId);
                    $cookies.remove("chaise-" + referrerId, { path: "/" });
                    closed = true;

                    $uibModalStack.dismissAll("no login");
                    if (rejectCb) rejectCb(response);
                };

                var modalInstance = modalUtils.showModal({
                    windowClass: "modal-login-instruction",
                    templateUrl: UriUtils.chaiseDeploymentPath() + "common/templates/loginDialog.modal.html",
                    controller: 'LoginDialogController',
                    controllerAs: 'ctrl',
                    resolve: {
                        params: params
                    },
                    openedClass: 'modal-login',
                    backdrop: 'static',
                    keyboard: false
                }, onModalCloseSuccess, onModalClose, false);
            }

            /* if browser is IE then add explicit handler to watch for changes in localstorage for a particular
             * variable
             */
            if (UriUtils.isBrowserIE()) {
                $cookies.put("chaise-" + referrerId, true, { path: "/" });
                var intervalId;
                var watchChangeInReferrerId = function () {
                    if (!$cookies.get("chaise-" + referrerId)) {
                        $interval.cancel(intervalId);
                        if (typeof cb== 'function') {
                            if(type.indexOf('modal')!== -1){
                                intervalId = $interval(watchChangeInReferrerId, 50);
                                modalInstance.close("Done");
                                cb();
                                closed = true;
                            }
                            else{
                                cb();
                            }
                        }
                        return;
                    }
                }
            }
            else {
                window.addEventListener('message', function(args) {
                    if (args && args.data && (typeof args.data == 'string')) {
                        _createBool(PREVIOUS_SESSION_KEY);
                        _removeToken(PROMPT_EXPIRATION_KEY);
                        var obj = UriUtils.queryStringToJSON(args.data);
                        if (obj.referrerid == referrerId && (typeof cb== 'function')) {
                            if(type.indexOf('modal')!== -1){
                                modalInstance.close("Done");
                                cb();
                                closed = true;
                            }
                            else{
                                cb();
                            }
                        }
                    }
                });
            }
        };

        var logInHelper = function(logInTypeCb, win, cb, type, rejectCb, logAction){
            var referrerId = (new Date().getTime());

            var url = serviceURL + '/authn/preauth?referrer='+UriUtils.fixedEncodeURIComponent($window.location.origin+"/"+ UriUtils.chaiseDeploymentPath() + "login?referrerid=" + referrerId);
            var config = {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'Accept': 'application/json'
                },
                skipHTTP401Handling: true
            };

            // TODO in the case of loginInAModal, we're not doing the actual login,
            // but we're still sending the request. Since we need to change this anyways,
            // I decided to not add any log action in that case and instead we should just
            // fix that function.
            if (logAction) {
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logAction, "", "")
                }
            }
            ConfigUtils.getHTTPService().get(url, config).then(function(response){
                var data = response.data;

                var login_url = "";
                if (data['redirect_url'] !== undefined) {
                    login_url = data['redirect_url'];
                } else if (data['login_form'] !== undefined) {
                    // we want to use the old login flow to login
                    // (login in the same window so when login does occur, it changes the same page instead of the page in the window that pops up)
                    win = $window;
                    // prevents the dialog from popping up shortly before the page redirects to login
                    type = "";
                    var referrer = $window.location.href;
                    var login_form = data['login_form'];
                    login_url = '../login?referrer=' + UriUtils.fixedEncodeURIComponent(referrer);
                    var method = login_form['method'];
                    var action = UriUtils.fixedEncodeURIComponent(login_form['action']);
                    var text = '';
                    var hidden = '';
                    for (var i = 0; i < login_form['input_fields'].length; i++) {
                        var field = login_form['input_fields'][i];
                        if (field.type === 'text') {
                            text = UriUtils.fixedEncodeURIComponent(field.name);
                        } else {
                            hidden = UriUtils.fixedEncodeURIComponent(field.name);
                        }
                    }
                    login_url += '&method=' + method + '&action=' + action + '&text=' + text + '&hidden=' + hidden + '&?referrerid=' + referrerId;
                }

                var params = {
                    login_url: login_url
                };
                if(win){
                        win.location=params.login_url;
                }
                logInTypeCb(params,referrerId, cb, type, rejectCb);
            }, function(error) {
                throw ERMrest.responseToError(error);
            });
        };

        /**
         * uses the reloadCb function to reload the page no matter what after a user logs in
         * creates a race condition with the calback registered for the LoginInAModal function
         */
        var popupLogin = function (logAction) {
            var reloadCb = function(){
                window.location.reload();
            };

            var x = window.innerWidth/2 - 800/2;
            var y = window.innerHeight/2 - 600/2;

            var win = window.open("", '_blank','width=800,height=600,left=' + x + ',top=' + y);

            logInHelper(loginWindowCb, win, reloadCb, 'popUp', null, logAction);
        };

        var shouldReloadPageAfterLogin = function(newSession) {
            if (_session === null) return true;
            return false;
        };

        return {

            /**
             * Will return a promise that is resolved with the session.
             * It will also call the _executeListeners() functions and sets the _session.
             * If we couldn't fetch the session, it will resolve with `null`.
             *
             * TODO needs to be revisited for 401.
             * We want to reload the page and stop the code execution for 401,
             *  but currently the code will continue executing and then reloads.
             *
             * @param  {string=} context undefined or "401"
             */
            getSession: function(context) {
                var config = {
                    skipHTTP401Handling: true,
                    headers: {}
                };
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logService.logActions.SESSION_RETRIEVE, "", "")
                }
                return ConfigUtils.getHTTPService().get(serviceURL + "/authn/session", config).then(function(response) {
                    if (context === "401" && shouldReloadPageAfterLogin(response.data)) {
                        window.location.reload();
                        return response.data;
                    }

                    _session = response.data;
                    _executeListeners();
                    return _session;
                }).catch(function(err) {
                    $log.warn(ERMrest.responseToError(err));

                    _session = null;
                    _executeListeners();
                    return _session;
                });
            },

            /**
             * Will return a promise that is resolved with the session.
             * Meant for validating the server session and verify if it's still active or not
             */
            validateSession: function () {
                var config = {
                    skipHTTP401Handling: true,
                    headers: {}
                };
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logService.logActions.SESSION_VALIDATE, "", "")
                }
                return ConfigUtils.getHTTPService().get(serviceURL + "/authn/session", config).then(function(response) {
                    _session = response.data;
                    return _session;
                }).catch(function(err) {
                    $log.warn(ERMrest.responseToError(err));

                    _session = null;
                    return _session;
                });
            },

            getSessionValue: function() {
                return _session;
            },

            // if there's a previous login token AND
            // the prompt expiration token does not exist OR it has expired
            showPreviousSessionAlert: function() {
                return (_tokenExists(PREVIOUS_SESSION_KEY) && (!_tokenExists(PROMPT_EXPIRATION_KEY) || _expiredToken(PROMPT_EXPIRATION_KEY)));
            },

            subscribeOnChange: function(fn) {
                // To avoid same ids for an instance we add counter
                var id = new Date().getTime() + (++_counter);

                if (typeof fn  == 'function') {
                    _changeCbs[id] = fn;
                }
                return id;
            },

            unsubscribeOnChange: function(id) {
                delete _changeCbs[id];
            },

            createPromptExpirationToken: function() {
                _createToken(PROMPT_EXPIRATION_KEY);
            },

            extendPromptExpirationToken: function() {
                _extendToken(PROMPT_EXPIRATION_KEY);
            },

            loginInAPopUp: popupLogin,

            /**
             * TODO technically this function should even ask for preauthn in logInHelper
             * This function opens a modal dialog which has a link for login
             * the callback for this function has a race condition because the login link in the modal uses `loginInAPopUp`
             * that function uses the embedded `reloadCb` as the callback for the actual login window closing.
             * these 2 callbacks trigger in the order of `popupCb` first then `modalCb` where modalCb is ignored more often than not
             * @param {Function} notifyErmrestCB - runs after the login process has been complete
             */
            loginInAModal: function(notifyErmrestCB, notifyErmrestRejectCB, logAction) {
                logInHelper(loginWindowCb, "", notifyErmrestCB, 'modal', notifyErmrestRejectCB, logAction);
            },

            logout: function() {
                var chaiseConfig = ConfigUtils.getConfigJSON();
                var logoutURL = chaiseConfig['logoutURL'] ? chaiseConfig['logoutURL'] : '/';
                var url = serviceURL + "/authn/session";

                url += '?logout_url=' + UriUtils.fixedEncodeURIComponent(logoutURL);

                var config = {
                    skipHTTP401Handling: true,
                    headers: {}
                };
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logService.logActions.LOGOUT_NAVBAR, "", "")
                }
                ConfigUtils.getHTTPService().delete(url, config).then(function(response) {
                    StorageService.deleteStorageNamespace(LOCAL_STORAGE_KEY);
                    $window.location = response.data.logout_url;
                }, function(error) {
                    // if the logout fails for some reason, send the user to the logout url as defined above
                    $window.location = logoutURL;
                });
            }
        }
    }])

    var pathname = window.location.pathname;

    // If app is not search, viewer and login then attach the unauthorised 401 http handler to ermrestjs

    if (pathname.indexOf('/search/') == -1 && pathname.indexOf('/viewer/') == -1 && pathname.indexOf('/login') == -1) {

        angular.module('chaise.authen')

        .run(['ERMrest', '$injector', '$q', function runRecordEditApp(ERMrest, $injector, $q) {

            var Session = $injector.get("Session");

            // Bind callback function by invoking setHTTP401Handler handler passing the callback
            // This callback will be called whenever 401 HTTP error is encountered unless there is
            // already login flow in progress
            ERMrest.setHTTP401Handler(function() {
                var defer = $q.defer();

                // Call login in a new modal window to perform authentication
                // and return a promise to notify ermrestjs that the user has loggedin
                Session.loginInAModal(function() {

                    // Once the user has logged in fetch the new session and set the session value
                    // and resolve the promise to notify ermrestjs that the user has logged in
                    // and it can continue firing other queued calls
                    Session.getSession("401").then(function(_session) {
                        defer.resolve();
                    }, function(exception) {
                        defer.reject(exception);
                    });

                }, function (response) {
                    // returns to rejectCB in ermrestJS/http.js
                    defer.reject(response);
                });

                return defer.promise;
            });

        }]);

    }

})();
<commit_msg>moved function so it could be called within authen js, track the previous session and update it if the user is different now, properly close the modal and return into the cb() function which will continue the data submission workflow<commit_after>(function() {
    'use strict';

    angular.module('chaise.authen', ['chaise.utils', 'chaise.storage'])

    .factory('Session', ['ConfigUtils', 'messageMap', 'logService', 'modalUtils', 'StorageService', 'UriUtils', '$cookies', '$interval', '$log', '$q', '$rootScope', '$sce', '$uibModalStack', '$window',
        function (ConfigUtils, messageMap, logService, modalUtils, StorageService, UriUtils, $cookies, $interval, $log, $q, $rootScope, $sce, $uibModalStack, $window) {
        // authn API no longer communicates through ermrest, removing the need to check for ermrest location
        var serviceURL = $window.location.origin;

        var LOCAL_STORAGE_KEY = 'session';              // name of object session information is stored under
        var PROMPT_EXPIRATION_KEY = 'promptExpiration'; // name of key for prompt expiration value
        var PREVIOUS_SESSION_KEY = 'previousSession';   // name of key for previous session boolean

        // Private variable to store current session object
        var _session = null;
        var _prevSession = null;

        var _changeCbs = {};

        var _counter = 0;

        var _executeListeners = function() {
            for (var k in _changeCbs) {
                _changeCbs[k]();
            }
        };

        /**
         * Functions that interact with the StorageService tokens
         * There are 2 keys stored under the LOCAL_STORAGE_KEY object, PROMPT_EXPIRATION_KEY and PREVIOUS_SESSION_KEY
         */

        // verifies value exists for `keyName`
        var _tokenExists = function(keyName) {
            var sessionStorage = StorageService.getStorage(LOCAL_STORAGE_KEY);

            return (sessionStorage && sessionStorage[keyName]);
        };

        // creates an expiration token with `keyName`
        var _createToken = function (keyName) {
            var data = {};
            var hourFromNow = new Date();
            hourFromNow.setHours(hourFromNow.getHours() + 1);

            data[keyName] = hourFromNow.getTime();

            StorageService.updateStorage(LOCAL_STORAGE_KEY, data);
        };

        // removes the key/value pair at `keyName`
        var _removeToken = function (keyName) {
            if (_tokenExists(keyName)) {
                StorageService.deleteStorageValue(LOCAL_STORAGE_KEY, keyName);
            }
        };

        // checks if the expiration token with `keyName` has expired
        var _expiredToken = function (keyName) {
            var sessionStorage = StorageService.getStorage(LOCAL_STORAGE_KEY);

            return (sessionStorage && new Date().getTime() > sessionStorage[keyName]);
        };

        // extends the expiration token with `keyName` if it hasn't expired
        var _extendToken = function (keyName) {
            if (_tokenExists(keyName) && !_expiredToken(keyName)) {
                _createToken(keyName);
            }
        };

        // creates a boolean token with `keyName`
        var _createBool = function (keyName) {
            var data = {};

            data[keyName] = true;

            StorageService.updateStorage(LOCAL_STORAGE_KEY, data);
        };

        var modalInstance = null;
        var loginWindowCb = function (params, referrerId, cb, type, rejectCb){
            if(type.indexOf('modal')!== -1){
                if (_session) {
                    params.title = messageMap.sessionExpired.title;
                } else {
                    params.title = messageMap.noSession.title;
                }
                var closed = false;

                var cleanupModal = function (message) {
                    $interval.cancel(intervalId);
                    $cookies.remove("chaise-" + referrerId, { path: "/" });
                    closed = true;

                    $uibModalStack.dismissAll(message);
                }
                var onModalCloseSuccess = function () {
                    cleanupModal("login refreshed");
                    cb();
                }

                var onModalClose = function(response) {
                    cleanupModal("no login");
                    if (rejectCb) rejectCb(response);
                };

                modalInstance = modalUtils.showModal({
                    windowClass: "modal-login-instruction",
                    templateUrl: UriUtils.chaiseDeploymentPath() + "common/templates/loginDialog.modal.html",
                    controller: 'LoginDialogController',
                    controllerAs: 'ctrl',
                    resolve: {
                        params: params
                    },
                    openedClass: 'modal-login',
                    backdrop: 'static',
                    keyboard: false
                }, onModalCloseSuccess, onModalClose, false);
            }

            /* if browser is IE then add explicit handler to watch for changes in localstorage for a particular
             * variable
             */
            if (UriUtils.isBrowserIE()) {
                $cookies.put("chaise-" + referrerId, true, { path: "/" });
                var intervalId;
                var watchChangeInReferrerId = function () {
                    if (!$cookies.get("chaise-" + referrerId)) {
                        $interval.cancel(intervalId);
                        if (typeof cb== 'function') {
                            if(type.indexOf('modal')!== -1){
                                intervalId = $interval(watchChangeInReferrerId, 50);
                                modalInstance.close("Done");
                                cb();
                                closed = true;
                            }
                            else{
                                cb();
                            }
                        }
                        return;
                    }
                }
            }
            else {
                window.addEventListener('message', function(args) {
                    if (args && args.data && (typeof args.data == 'string')) {
                        _createBool(PREVIOUS_SESSION_KEY);
                        _removeToken(PROMPT_EXPIRATION_KEY);
                        var obj = UriUtils.queryStringToJSON(args.data);
                        if (obj.referrerid == referrerId && (typeof cb== 'function')) {
                            if(type.indexOf('modal')!== -1){
                                modalInstance.close("Done");
                                cb();
                                closed = true;
                            }
                            else{
                                cb();
                            }
                        }
                    }
                });
            }
        };

        var logInHelper = function(logInTypeCb, win, cb, type, rejectCb, logAction){
            var referrerId = (new Date().getTime());

            var url = serviceURL + '/authn/preauth?referrer='+UriUtils.fixedEncodeURIComponent($window.location.origin+"/"+ UriUtils.chaiseDeploymentPath() + "login?referrerid=" + referrerId);
            var config = {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'Accept': 'application/json'
                },
                skipHTTP401Handling: true
            };

            // TODO in the case of loginInAModal, we're not doing the actual login,
            // but we're still sending the request. Since we need to change this anyways,
            // I decided to not add any log action in that case and instead we should just
            // fix that function.
            if (logAction) {
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logAction, "", "")
                }
            }
            ConfigUtils.getHTTPService().get(url, config).then(function(response){
                var data = response.data;

                var login_url = "";
                if (data['redirect_url'] !== undefined) {
                    login_url = data['redirect_url'];
                } else if (data['login_form'] !== undefined) {
                    // we want to use the old login flow to login
                    // (login in the same window so when login does occur, it changes the same page instead of the page in the window that pops up)
                    win = $window;
                    // prevents the dialog from popping up shortly before the page redirects to login
                    type = "";
                    var referrer = $window.location.href;
                    var login_form = data['login_form'];
                    login_url = '../login?referrer=' + UriUtils.fixedEncodeURIComponent(referrer);
                    var method = login_form['method'];
                    var action = UriUtils.fixedEncodeURIComponent(login_form['action']);
                    var text = '';
                    var hidden = '';
                    for (var i = 0; i < login_form['input_fields'].length; i++) {
                        var field = login_form['input_fields'][i];
                        if (field.type === 'text') {
                            text = UriUtils.fixedEncodeURIComponent(field.name);
                        } else {
                            hidden = UriUtils.fixedEncodeURIComponent(field.name);
                        }
                    }
                    login_url += '&method=' + method + '&action=' + action + '&text=' + text + '&hidden=' + hidden + '&?referrerid=' + referrerId;
                }

                var params = {
                    login_url: login_url
                };
                if(win){
                        win.location=params.login_url;
                }
                logInTypeCb(params,referrerId, cb, type, rejectCb);
            }, function(error) {
                throw ERMrest.responseToError(error);
            });
        };

        /**
         * uses the reloadCb function to reload the page no matter what after a user logs in
         * creates a race condition with the calback registered for the LoginInAModal function
         */
        var popupLogin = function (logAction) {
            var reloadCb = function(){
                if (!shouldReloadPageAfterLogin()) {
                    _getSession().then(function (newSession) {
                        modalInstance.close();
                    });
                } else {
                    window.location.reload();
                }
            };

            var x = window.innerWidth/2 - 800/2;
            var y = window.innerHeight/2 - 600/2;

            var win = window.open("", '_blank','width=800,height=600,left=' + x + ',top=' + y);

            logInHelper(loginWindowCb, win, reloadCb, 'popUp', null, logAction);
        };

        var shouldReloadPageAfterLogin = function() {
            if (_session === null) return true;
            return false;
        };

        /**
         * Will return a promise that is resolved with the session.
         * It will also call the _executeListeners() functions and sets the _session.
         * If we couldn't fetch the session, it will resolve with `null`.
         *
         * TODO needs to be revisited for 401.
         * We want to reload the page and stop the code execution for 401,
         *  but currently the code will continue executing and then reloads.
         *
         * @param  {string=} context undefined or "401"
         */
        var _getSession = function(context) {
            var config = {
                skipHTTP401Handling: true,
                headers: {}
            };
            config.headers[ERMrest.contextHeaderName] = {
                action: logService.getActionString(logService.logActions.SESSION_RETRIEVE, "", "")
            }
            return ConfigUtils.getHTTPService().get(serviceURL + "/authn/session", config).then(function(response) {
                if (context === "401" && shouldReloadPageAfterLogin()) {
                    window.location.reload();
                    return response.data;
                }

                var sameSessionAsPrevious = false;
                if (_prevSession) sameSessionAsPrevious = _prevSession.client.id == response.data.client.id;

                _session = response.data;
                // keep track of previous session, so when a timeout occurs, we can compare the sessions
                // when a new session is fetched after timeout, check if the identities are the same
                // if not the same, update in case timeout occurs again
                if (!_prevSession || !sameSessionAsPrevious) {
                    _prevSession = response.data
                }
                _executeListeners();
                return _session;
            }).catch(function(err) {
                $log.warn(ERMrest.responseToError(err));

                _session = null;
                _executeListeners();
                return _session;
            });
        }

        return {
            getSession: _getSession,

            /**
             * Will return a promise that is resolved with the session.
             * Meant for validating the server session and verify if it's still active or not
             */
            validateSession: function () {
                var config = {
                    skipHTTP401Handling: true,
                    headers: {}
                };
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logService.logActions.SESSION_VALIDATE, "", "")
                }
                return ConfigUtils.getHTTPService().get(serviceURL + "/authn/session", config).then(function(response) {
                    _session = response.data;
                    return _session;
                }).catch(function(err) {
                    $log.warn(ERMrest.responseToError(err));

                    _session = null;
                    return _session;
                });
            },

            getSessionValue: function() {
                return _session;
            },

            // if there's a previous login token AND
            // the prompt expiration token does not exist OR it has expired
            showPreviousSessionAlert: function() {
                return (_tokenExists(PREVIOUS_SESSION_KEY) && (!_tokenExists(PROMPT_EXPIRATION_KEY) || _expiredToken(PROMPT_EXPIRATION_KEY)));
            },

            subscribeOnChange: function(fn) {
                // To avoid same ids for an instance we add counter
                var id = new Date().getTime() + (++_counter);

                if (typeof fn  == 'function') {
                    _changeCbs[id] = fn;
                }
                return id;
            },

            unsubscribeOnChange: function(id) {
                delete _changeCbs[id];
            },

            createPromptExpirationToken: function() {
                _createToken(PROMPT_EXPIRATION_KEY);
            },

            extendPromptExpirationToken: function() {
                _extendToken(PROMPT_EXPIRATION_KEY);
            },

            loginInAPopUp: popupLogin,

            /**
             * TODO technically this function should even ask for preauthn in logInHelper
             * This function opens a modal dialog which has a link for login
             * the callback for this function has a race condition because the login link in the modal uses `loginInAPopUp`
             * that function uses the embedded `reloadCb` as the callback for the actual login window closing.
             * these 2 callbacks trigger in the order of `popupCb` first then `modalCb` where modalCb is ignored more often than not
             * @param {Function} notifyErmrestCB - runs after the login process has been complete
             */
            loginInAModal: function(notifyErmrestCB, notifyErmrestRejectCB, logAction) {
                logInHelper(loginWindowCb, "", notifyErmrestCB, 'modal', notifyErmrestRejectCB, logAction);
            },

            logout: function() {
                var chaiseConfig = ConfigUtils.getConfigJSON();
                var logoutURL = chaiseConfig['logoutURL'] ? chaiseConfig['logoutURL'] : '/';
                var url = serviceURL + "/authn/session";

                url += '?logout_url=' + UriUtils.fixedEncodeURIComponent(logoutURL);

                var config = {
                    skipHTTP401Handling: true,
                    headers: {}
                };
                config.headers[ERMrest.contextHeaderName] = {
                    action: logService.getActionString(logService.logActions.LOGOUT_NAVBAR, "", "")
                }
                ConfigUtils.getHTTPService().delete(url, config).then(function(response) {
                    StorageService.deleteStorageNamespace(LOCAL_STORAGE_KEY);
                    $window.location = response.data.logout_url;
                }, function(error) {
                    // if the logout fails for some reason, send the user to the logout url as defined above
                    $window.location = logoutURL;
                });
            }
        }
    }])

    var pathname = window.location.pathname;

    // If app is not search, viewer and login then attach the unauthorised 401 http handler to ermrestjs

    if (pathname.indexOf('/search/') == -1 && pathname.indexOf('/viewer/') == -1 && pathname.indexOf('/login') == -1) {

        angular.module('chaise.authen')

        .run(['ERMrest', '$injector', '$q', function runRecordEditApp(ERMrest, $injector, $q) {

            var Session = $injector.get("Session");

            // Bind callback function by invoking setHTTP401Handler handler passing the callback
            // This callback will be called whenever 401 HTTP error is encountered unless there is
            // already login flow in progress
            ERMrest.setHTTP401Handler(function() {
                var defer = $q.defer();

                // Call login in a new modal window to perform authentication
                // and return a promise to notify ermrestjs that the user has loggedin
                Session.loginInAModal(function() {

                    // Once the user has logged in fetch the new session and set the session value
                    // and resolve the promise to notify ermrestjs that the user has logged in
                    // and it can continue firing other queued calls
                    Session.getSession("401").then(function(_session) {
                        defer.resolve();
                    }, function(exception) {
                        defer.reject(exception);
                    });

                }, function (response) {
                    // returns to rejectCB in ermrestJS/http.js
                    defer.reject(response);
                });

                return defer.promise;
            });

        }]);

    }

})();
<|endoftext|>


<commit_before>/**
 * @file Popover component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TriggerPop from '../_TriggerPop';
import Theme from '../Theme';

import Position from '../_statics/Position';

import Event from '../_vendors/Event';
import Util from '../_vendors/Util';
import PopManagement from '../_vendors/PopManagement';

class Popover extends Component {

    static Position = Position;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.closeTimeout = null;

        this.clearCloseTimeout = ::this.clearCloseTimeout;
        this.mouseOverHandler = ::this.mouseOverHandler;
        this.mouseOutHandler = ::this.mouseOutHandler;
        this.renderHandler = ::this.renderHandler;
        this.destroyHandler = ::this.destroyHandler;

    }

    clearCloseTimeout() {
        if (this.closeTimeout) {
            clearTimeout(this.closeTimeout);
            this.closeTimeout = null;
        }
    }

    mouseOverHandler() {
        this.clearCloseTimeout();
    }

    mouseOutHandler(e) {
        const {onRequestClose} = this.props;
        this.clearCloseTimeout();
        this.closeTimeout = setTimeout(() => {
            onRequestClose && onRequestClose(e);
        }, 1000 / 6);
    }

    renderHandler(el) {
        const {triggerEl} = this.props;
        Event.addEvent(triggerEl, 'mouseover', this.mouseOverHandler);
        Event.addEvent(triggerEl, 'mouseout', this.mouseOutHandler);
        Event.addEvent(el, 'mouseover', this.mouseOverHandler);
        Event.addEvent(el, 'mouseout', this.mouseOutHandler);
    }

    destroyHandler(el) {
        const {triggerEl} = this.props;
        Event.removeEvent(triggerEl, 'mouseover', this.mouseOverHandler);
        Event.removeEvent(triggerEl, 'mouseout', this.mouseOutHandler);
        Event.removeEvent(el, 'mouseover', this.mouseOverHandler);
        Event.removeEvent(el, 'mousemove', this.mouseOutHandler);
    }

    /**
     * public
     */
    resetPosition() {
        this.refs.popover.resetPosition();
    }

    componentWillReceiveProps(nextProps) {

        const {visible, isEscClose} = nextProps;

        if (isEscClose && visible) {
            PopManagement.push(this);
        }

    }

    componentWillUnmount() {

        this.clearCloseTimeout();

        PopManagement.pop(this);

    }

    render() {

        const {

                className, contentClassName,

                // not passing down these props
                onRequestClose,

                ...restProps

            } = this.props,

            popoverClassName = classNames('popover', {
                [className]: className
            }),

            popoverContentClassName = classNames('popover-content', {
                [contentClassName]: contentClassName
            });

        return (
            <TriggerPop {...restProps}
                        ref="popover"
                        className={popoverClassName}
                        contentClassName={popoverContentClassName}
                        onRender={this.renderHandler}
                        onDestroy={this.destroyHandler}/>
        );
    }

}

Popover.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The CSS class name of the content element.
     */
    contentClassName: PropTypes.string,

    modalClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * This is the DOM element that will be used to set the position of the popover.
     */
    triggerEl: PropTypes.object,

    /**
     * If true,the popover is visible.
     */
    visible: PropTypes.bool,

    /**
     * If true,the popover will have a triangle on the top of the DOM element.
     */
    hasTriangle: PropTypes.bool,

    triangle: PropTypes.element,

    /**
     * The popover theme.Can be primary,highlight,success,warning,error.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The popover alignment.The value can be Popover.Position.LEFT or Popover.Position.RIGHT.
     */
    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * If true, popover will have animation effects.
     */
    isAnimated: PropTypes.bool,

    /**
     * The depth of Paper component.
     */
    depth: PropTypes.number,

    isBlurClose: PropTypes.bool,
    isEscClose: PropTypes.bool,
    shouldPreventContainerScroll: PropTypes.bool,
    isTriggerPositionFixed: PropTypes.bool,
    showModal: PropTypes.bool,

    /**
     * The function of popover render.
     */
    onRender: PropTypes.func,

    /**
     * The function of popover rendered.
     */
    onRendered: PropTypes.func,

    /**
     * The function of popover destroy.
     */
    onDestroy: PropTypes.func,

    /**
     * The function of popover destroyed.
     */
    onDestroyed: PropTypes.func,

    /**
     * Callback function fired when the popover is requested to be closed.
     */
    onRequestClose: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

Popover.defaultProps = {

    depth: 6,

    visible: false,
    hasTriangle: true,
    theme: Theme.DEFAULT,
    position: Position.BOTTOM,
    isAnimated: true,

    isBlurClose: true,
    isEscClose: true,
    shouldPreventContainerScroll: true,
    isTriggerPositionFixed: false,
    showModal: false

};

export default Popover;<commit_msg>update Popover<commit_after>/**
 * @file Popover component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TriggerPop from '../_TriggerPop';
import Theme from '../Theme';

import Position from '../_statics/Position';

import Event from '../_vendors/Event';
import Util from '../_vendors/Util';

class Popover extends Component {

    static Position = Position;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.closeTimeout = null;

        this.clearCloseTimeout = ::this.clearCloseTimeout;
        this.mouseOverHandler = ::this.mouseOverHandler;
        this.mouseOutHandler = ::this.mouseOutHandler;
        this.renderHandler = ::this.renderHandler;
        this.destroyHandler = ::this.destroyHandler;

    }

    clearCloseTimeout() {
        if (this.closeTimeout) {
            clearTimeout(this.closeTimeout);
            this.closeTimeout = null;
        }
    }

    mouseOverHandler() {
        this.clearCloseTimeout();
    }

    mouseOutHandler(e) {
        const {onRequestClose} = this.props;
        this.clearCloseTimeout();
        this.closeTimeout = setTimeout(() => {
            onRequestClose && onRequestClose(e);
        }, 1000 / 6);
    }

    renderHandler(el) {
        const {triggerEl} = this.props;
        Event.addEvent(triggerEl, 'mouseover', this.mouseOverHandler);
        Event.addEvent(triggerEl, 'mouseout', this.mouseOutHandler);
        Event.addEvent(el, 'mouseover', this.mouseOverHandler);
        Event.addEvent(el, 'mouseout', this.mouseOutHandler);
    }

    destroyHandler(el) {
        const {triggerEl} = this.props;
        Event.removeEvent(triggerEl, 'mouseover', this.mouseOverHandler);
        Event.removeEvent(triggerEl, 'mouseout', this.mouseOutHandler);
        Event.removeEvent(el, 'mouseover', this.mouseOverHandler);
        Event.removeEvent(el, 'mousemove', this.mouseOutHandler);
    }

    /**
     * public
     */
    resetPosition() {
        this.refs.popover.resetPosition();
    }

    componentWillUnmount() {
        this.clearCloseTimeout();
    }

    render() {

        const {

                className, contentClassName,

                // not passing down these props
                onRequestClose,

                ...restProps

            } = this.props,

            popoverClassName = classNames('popover', {
                [className]: className
            }),

            popoverContentClassName = classNames('popover-content', {
                [contentClassName]: contentClassName
            });

        return (
            <TriggerPop {...restProps}
                        ref="popover"
                        className={popoverClassName}
                        contentClassName={popoverContentClassName}
                        onRender={this.renderHandler}
                        onDestroy={this.destroyHandler}/>
        );
    }

}

Popover.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The CSS class name of the content element.
     */
    contentClassName: PropTypes.string,

    modalClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * This is the DOM element that will be used to set the position of the popover.
     */
    triggerEl: PropTypes.object,

    /**
     * If true,the popover is visible.
     */
    visible: PropTypes.bool,

    /**
     * If true,the popover will have a triangle on the top of the DOM element.
     */
    hasTriangle: PropTypes.bool,

    triangle: PropTypes.element,

    /**
     * The popover theme.Can be primary,highlight,success,warning,error.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The popover alignment.The value can be Popover.Position.LEFT or Popover.Position.RIGHT.
     */
    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * If true, popover will have animation effects.
     */
    isAnimated: PropTypes.bool,

    /**
     * The depth of Paper component.
     */
    depth: PropTypes.number,

    isBlurClose: PropTypes.bool,
    shouldPreventContainerScroll: PropTypes.bool,
    isTriggerPositionFixed: PropTypes.bool,
    showModal: PropTypes.bool,

    /**
     * The function of popover render.
     */
    onRender: PropTypes.func,

    /**
     * The function of popover rendered.
     */
    onRendered: PropTypes.func,

    /**
     * The function of popover destroy.
     */
    onDestroy: PropTypes.func,

    /**
     * The function of popover destroyed.
     */
    onDestroyed: PropTypes.func,

    /**
     * Callback function fired when the popover is requested to be closed.
     */
    onRequestClose: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

Popover.defaultProps = {

    depth: 6,

    visible: false,
    hasTriangle: true,
    theme: Theme.DEFAULT,
    position: Position.BOTTOM,
    isAnimated: true,

    isBlurClose: true,
    shouldPreventContainerScroll: true,
    isTriggerPositionFixed: false,
    showModal: false

};

export default Popover;<|endoftext|>


<commit_before>/* @flow */

import * as conversion from '../conversion'
import logger from '../logger'
import { invalidPath } from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'
import RemoteCozy from './cozy'

import type { Metadata } from '../metadata'
import type { RemoteDoc } from './document'

const log = logger({
  prefix: 'Remote watcher',
  date: true
})

export const DEFAULT_HEARTBEAT: number = 1000 * 60 * 3 // 3 minutes
export const HEARTBEAT: number = parseInt(process.env.COZY_DESKTOP_HEARTBEAT) || DEFAULT_HEARTBEAT

const SIDE = 'remote'

// Get changes from the remote Cozy and prepare them for merge
export default class RemoteWatcher {
  pouch: Pouch
  prep: Prep
  remoteCozy: RemoteCozy
  intervalID: ?number
  runningResolve: ?() => void

  constructor (pouch: Pouch, prep: Prep, remoteCozy: RemoteCozy) {
    this.pouch = pouch
    this.prep = prep
    this.remoteCozy = remoteCozy
  }

  start () {
    const started = this.watch()
    const running = started.then(() => {
      return new Promise((resolve, reject) => {
        this.runningResolve = resolve
        this.intervalID = setInterval(() => {
          this.watch().catch(err => reject(err))
        }, HEARTBEAT)
      })
    })
    return {
      started: started,
      running: running
    }
  }

  stop () {
    if (this.intervalID) {
      clearInterval(this.intervalID)
      this.intervalID = null
    }
    if (this.runningResolve) {
      this.runningResolve()
    }
  }

  async watch () {
    try {
      const seq = await this.pouch.getRemoteSeqAsync()
      const changes = await this.remoteCozy.changes(seq)

      if (changes.ids.length === 0) return

      await this.pullMany(changes.ids)
      await this.pouch.setRemoteSeqAsync(changes.last_seq)
      log.lineBreak()
    } catch (err) {
      if (err.message === 'Client has been revoked') {
        throw err
      }
      log.error(err)
    }
  }

  // Pull multiple files/dirs metadata at once, given their ids
  async pullMany (ids: string[]) {
    let failedIds = []

    for (let id of ids) {
      try {
        await this.pullOne(id)
      } catch (err) {
        log.error(err)
        failedIds.push(id)
      }
    }

    if (failedIds.length > 0) {
      throw new Error(
        `Some documents could not be pulled: ${failedIds.join(', ')}`
      )
    }
  }

  // Pull a single file/dir metadata, given its id
  async pullOne (id: string): Promise<*> {
    const doc: ?RemoteDoc = await this.remoteCozy.findMaybe(id)

    if (doc != null) {
      return this.onChange(doc)
    }
  }

  async onChange (doc: RemoteDoc) {
    log.info(`${doc.path}: something happened on remote`)
    log.inspect(doc)

    const was: ?Metadata = await this.pouch.byRemoteIdMaybeAsync(doc._id)

    if (doc.path && doc.path.startsWith('/.cozy_trash/')) {
      if (was == null) {
        log.info(`${doc.path}: was deleted both remotely and locally`)
        return
      } else {
        log.info(`${doc.path}: was deleted remotely`)
        return this.prep.deleteDocAsync(SIDE, was)
      }
    } else if (['directory', 'file'].includes(doc.type)) {
      return this.putDoc(doc, was)
    } else {
      log.error(`Document ${doc._id} is not a file or a directory`)
      return
    }
  }

  // Transform the doc and save it in pouchdb
  //
  // In both CouchDB and PouchDB, the filepath includes the name field.
  // And the _id/_rev from CouchDB are saved in the remote field in PouchDB.
  async putDoc (remote: RemoteDoc, was: ?Metadata): Promise<*> {
    let doc: Metadata = conversion.createMetadata(remote)
    const docType = doc.docType
    if (invalidPath(doc)) {
      log.error('Invalid path / local id')
      log.error(doc)
      throw new Error('Invalid path/name')
    } else if (!was) {
      log.info(`${doc.path}: ${docType} was added remotely`)
      return this.prep.addDocAsync(SIDE, doc)
    } else if (was.path === doc.path) {
      log.info(`${doc.path}: ${docType} was updated remotely`)
      return this.prep.updateDocAsync(SIDE, doc)
    } else if ((doc.checksum != null) && (was.checksum === doc.checksum)) {
      log.info(`${doc.path}: ${docType} was moved remotely`)
      return this.prep.moveDocAsync(SIDE, doc, was)
    } else if ((doc.docType === 'folder') || (was.remote._rev === doc.remote._rev)) {
      log.info(`${doc.path}: ${docType} was possibly modified and renamed remotely while cozy-desktop was stopped`)
      await this.prep.deleteDocAsync(SIDE, was)
      return this.prep.addDocAsync(SIDE, doc)
    } else {
      // TODO: add unit test
      log.info(`${doc.path}: ${docType} was possibly renamed remotely while updated locally`)
      await this.removeRemote(was)
      return this.prep.addDocAsync(SIDE, doc)
    }
  }

  // Remove the association between a document and its remote
  // It's useful when a file has diverged (updated/renamed both in local and
  // remote) while cozy-desktop was not running.
  removeRemote (doc: Metadata) {
    log.debug(`${doc.path}: Dissociating from remote...`)
    delete doc.remote
    if (doc.sides) delete doc.sides.remote
    return this.pouch.db.put(doc)
  }
}
<commit_msg>remote/watcher: logging<commit_after>/* @flow */

import * as conversion from '../conversion'
import logger from '../logger'
import { invalidPath } from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'
import RemoteCozy from './cozy'

import type { Metadata } from '../metadata'
import type { RemoteDoc } from './document'

const log = logger({
  prefix: 'Remote watcher',
  date: true
})

export const DEFAULT_HEARTBEAT: number = 1000 * 60 * 3 // 3 minutes
export const HEARTBEAT: number = parseInt(process.env.COZY_DESKTOP_HEARTBEAT) || DEFAULT_HEARTBEAT

const SIDE = 'remote'

// Get changes from the remote Cozy and prepare them for merge
export default class RemoteWatcher {
  pouch: Pouch
  prep: Prep
  remoteCozy: RemoteCozy
  intervalID: ?number
  runningResolve: ?() => void

  constructor (pouch: Pouch, prep: Prep, remoteCozy: RemoteCozy) {
    this.pouch = pouch
    this.prep = prep
    this.remoteCozy = remoteCozy
  }

  start () {
    const started = this.watch()
    const running = started.then(() => {
      return new Promise((resolve, reject) => {
        this.runningResolve = resolve
        this.intervalID = setInterval(() => {
          this.watch().catch(err => reject(err))
        }, HEARTBEAT)
      })
    })
    return {
      started: started,
      running: running
    }
  }

  stop () {
    if (this.intervalID) {
      clearInterval(this.intervalID)
      this.intervalID = null
    }
    if (this.runningResolve) {
      this.runningResolve()
    }
  }

  async watch () {
    try {
      const seq = await this.pouch.getRemoteSeqAsync()
      const changes = await this.remoteCozy.changes(seq)

      if (changes.ids.length === 0) return

      await this.pullMany(changes.ids)
      await this.pouch.setRemoteSeqAsync(changes.last_seq)
      log.lineBreak()
    } catch (err) {
      if (err.message === 'Client has been revoked') {
        throw err
      }
      log.error(err)
    }
  }

  // Pull multiple files/dirs metadata at once, given their ids
  async pullMany (ids: string[]) {
    let failedIds = []

    for (let id of ids) {
      try {
        await this.pullOne(id)
      } catch (err) {
        log.error(err)
        failedIds.push(id)
      }
    }

    if (failedIds.length > 0) {
      throw new Error(
        `Some documents could not be pulled: ${failedIds.join(', ')}`
      )
    }
  }

  // Pull a single file/dir metadata, given its id
  async pullOne (id: string): Promise<*> {
    const doc: ?RemoteDoc = await this.remoteCozy.findMaybe(id)

    if (doc != null) {
      return this.onChange(doc)
    }
  }

  async onChange (doc: RemoteDoc) {
    log.debug(`${doc.path}: something happened on remote`)
    log.inspect(doc)

    const was: ?Metadata = await this.pouch.byRemoteIdMaybeAsync(doc._id)

    if (doc.path && doc.path.startsWith('/.cozy_trash/')) {
      if (was == null) {
        log.debug(`${doc.path}: was deleted both remotely and locally`)
        return
      } else {
        log.info(`${was.path}: was deleted remotely`)
        return this.prep.deleteDocAsync(SIDE, was)
      }
    } else if (['directory', 'file'].includes(doc.type)) {
      return this.putDoc(doc, was)
    } else {
      log.error(`Document ${doc._id} is not a file or a directory`)
      return
    }
  }

  // Transform the doc and save it in pouchdb
  //
  // In both CouchDB and PouchDB, the filepath includes the name field.
  // And the _id/_rev from CouchDB are saved in the remote field in PouchDB.
  async putDoc (remote: RemoteDoc, was: ?Metadata): Promise<*> {
    let doc: Metadata = conversion.createMetadata(remote)
    const docType = doc.docType
    if (invalidPath(doc)) {
      log.error('Invalid path / local id')
      log.error(doc)
      throw new Error('Invalid path/name')
    } else if (!was) {
      log.info(`${doc.path}: ${docType} was added remotely`)
      return this.prep.addDocAsync(SIDE, doc)
    } else if (was.path === doc.path) {
      log.info(`${doc.path}: ${docType} was updated remotely`)
      return this.prep.updateDocAsync(SIDE, doc)
    } else if ((doc.checksum != null) && (was.checksum === doc.checksum)) {
      log.info(`${doc.path}: ${docType} was moved remotely`)
      return this.prep.moveDocAsync(SIDE, doc, was)
    } else if ((doc.docType === 'folder') || (was.remote._rev === doc.remote._rev)) {
      log.info(`${doc.path}: ${docType} was possibly modified and renamed remotely while cozy-desktop was stopped`)
      await this.prep.deleteDocAsync(SIDE, was)
      return this.prep.addDocAsync(SIDE, doc)
    } else {
      // TODO: add unit test
      log.info(`${doc.path}: ${docType} was possibly renamed remotely while updated locally`)
      await this.removeRemote(was)
      return this.prep.addDocAsync(SIDE, doc)
    }
  }

  // Remove the association between a document and its remote
  // It's useful when a file has diverged (updated/renamed both in local and
  // remote) while cozy-desktop was not running.
  removeRemote (doc: Metadata) {
    log.debug(`${doc.path}: Dissociating from remote...`)
    delete doc.remote
    if (doc.sides) delete doc.sides.remote
    return this.pouch.db.put(doc)
  }
}
<|endoftext|>


<commit_before>'use strict'

function Blinky(options) {
    // default options
    var defaults = {
        target:          $('body'),
        numberOfColumns: chance.natural({ min: 5, max: 50 }),
        blinking:        true
    }

    // merge given options with the defaults
    $.extend(defaults, options);

    // set options to properties
    this.target          = defaults.target;
    this.numberOfColumns = defaults.numberOfColumns;
    this.blinking        = defaults.blinking;

    // private properties
    this._elements                = [];
    this._numberOfRows            = 0;
    this._lengthOfElementInPixel  = 0;
    this._widthDifferenceInPixel  = 0;
    this._heightDifferenceInPixel = 0;

    // draw rectangles
    this.draw();

    // add listener for resize-event to target
    this.target.resize($.proxy(this.draw, this));
}

Blinky.prototype.calculateLength = function() {
    var width  = this.target.width();
    var height = this.target.height();

    this._lengthOfElementInPixel  = Math.ceil(width / this.numberOfColumns);
    this._widthDifferenceInPixel  = this._lengthOfElementInPixel * this.numberOfColumns - width;
    this._numberOfRows            = Math.ceil(height / this._lengthOfElementInPixel);
    this._heightDifferenceInPixel = this._lengthOfElementInPixel * this._numberOfRows - height;

    if (this._widthDifferenceInPixel > this._lengthOfElementInPixel) {
        this.numberOfColumns   += 1;
        this.calculateLength();
    }
}

Blinky.prototype.draw = function() {
    this.calculateLength();

    // remove elements
    if (this._elements.length > 0) {
        $(this._elements).each(function(index, el) {
            el.jq.remove();
        });

        this._elements = [];
    }

    // create some elements
    var numberOfElements = this._numberOfRows * this.numberOfColumns;

    for (var i = 0; i <= numberOfElements - 1; i++) {
        var isLastColumn = false;
        var isLastRow    = false;

        if ( (i + 1) % this.numberOfColumns == 0) {
            isLastColumn = true;
        }

        if (i + 1 > numberOfElements - this.numberOfColumns) {
            isLastRow = true;
        }

        // create element
        var element = new Element(this._lengthOfElementInPixel, this._lengthOfElementInPixel, this.blinking);

        // correct width
        if (isLastColumn) {
            element.jq.width(this._lengthOfElementInPixel - this._widthDifferenceInPixel);
        }

        // correct height
        if (isLastRow) {
            element.jq.height(this._lengthOfElementInPixel - this._heightDifferenceInPixel);
        }

        element.jq.appendTo(this.target);
        this._elements.push(element);
    }
}

function Element(width, height, blinking) {
    this._width    = width;
    this._height   = height;
    this.jq  = $('<div />')
        .addClass('item')
        .width(this._width)
        .height(this._height)
        .css('cursor', 'pointer')
        .css('float', 'left')
    ;

    // change background color
    this.changeBackgroundColor();

    // set inverval for changing background color
    if (blinking) {
        setInterval($.proxy(this.changeBackgroundColor, this), chance.natural({min: 500, max: 1500}));
    }

    // click listener
    this.jq.click($.proxy(function() {
        this.changeBackgroundColor();
    }, this));
}

Element.prototype.changeBackgroundColor = function() {
    this.jq.css('backgroundColor', chance.color());
}

// register Blinky as jQuery plugin
$.fn.extend({
    blinky: function(options) {
        this.each(function() {
            options = $.extend(options, { target: $(this) });
            new Blinky(options);
        });

        return this;
    }
});<commit_msg>use strict mode and jshint<commit_after>/* jshint nonew: false */
/* global $, chance */

(function () {
    "use strict";

    function Blinky(options) {
        // default options
        var defaults = {
            target:          $('body'),
            numberOfColumns: chance.natural({ min: 5, max: 50 }),
            blinking:        true
        };

        // merge given options with the defaults
        $.extend(defaults, options);

        // set options to properties
        this.target          = defaults.target;
        this.numberOfColumns = defaults.numberOfColumns;
        this.blinking        = defaults.blinking;

        // private properties
        this.elements                = [];
        this.numberOfRows            = 0;
        this.lengthOfElementInPixel  = 0;
        this.widthDifferenceInPixel  = 0;
        this.heightDifferenceInPixel = 0;

        // draw rectangles
        this.draw();

        // add listener for resize-event to target
        this.target.resize($.proxy(this.draw, this));
    }

    Blinky.prototype.calculateLength = function () {
        var width  = this.target.width(),
            height = this.target.height();

        this.lengthOfElementInPixel  = Math.ceil(width / this.numberOfColumns);
        this.widthDifferenceInPixel  = this.lengthOfElementInPixel * this.numberOfColumns - width;
        this.numberOfRows            = Math.ceil(height / this.lengthOfElementInPixel);
        this.heightDifferenceInPixel = this.lengthOfElementInPixel * this.numberOfRows - height;

        if (this.widthDifferenceInPixel > this.lengthOfElementInPixel) {
            this.numberOfColumns   += 1;
            this.calculateLength();
        }
    };

    Blinky.prototype.draw = function () {
        this.calculateLength();

        // remove elements
        if (this.elements.length > 0) {
            $(this.elements).each(function () {
                this.jq.remove();
            });

            this.elements = [];
        }

        // create some elements
        var numberOfElements = this.numberOfRows * this.numberOfColumns;

        for (var i = 0; i <= numberOfElements - 1; i++) {
            var isLastColumn = false;
            var isLastRow    = false;

            if ( (i + 1) % this.numberOfColumns === 0) {
                isLastColumn = true;
            }

            if (i + 1 > numberOfElements - this.numberOfColumns) {
                isLastRow = true;
            }

            // create element
            var element = new Element(this.lengthOfElementInPixel, this.lengthOfElementInPixel, this.blinking);

            // correct width
            if (isLastColumn) {
                element.jq.width(this.lengthOfElementInPixel - this.widthDifferenceInPixel);
            }

            // correct height
            if (isLastRow) {
                element.jq.height(this.lengthOfElementInPixel - this.heightDifferenceInPixel);
            }

            element.jq.appendTo(this.target);
            this.elements.push(element);
        }
    };

    function Element(width, height, blinking) {
        this._width    = width;
        this._height   = height;
        this.jq  = $('<div />')
            .addClass('item')
            .width(this._width)
            .height(this._height)
            .css('cursor', 'pointer')
            .css('float', 'left')
        ;

        // change background color
        this.changeBackgroundColor();

        // set inverval for changing background color
        if (blinking) {
            setInterval($.proxy(this.changeBackgroundColor, this), chance.natural({min: 500, max: 1500}));
        }

        // click listener
        this.jq.click($.proxy(function() {
            this.changeBackgroundColor();
        }, this));
    }

    Element.prototype.changeBackgroundColor = function() {
        this.jq.css('backgroundColor', chance.color());
    };

    // register Blinky as jQuery plugin
    $.fn.extend({
        blinky: function(options) {
            this.each(function() {
                options = $.extend(options, { target: $(this) });
                new Blinky(options);
            });

            return this;
        }
    });

}());<|endoftext|>