<commit_before>'use strict';

const {
	app,
	BrowserWindow,
	globalShortcut,
	ipcMain
} = require('electron');

const electronWindow   = require('electron-window');
const path             = require('path');
const objc             = require('nodobjc');

const DEFAULT_WINDOW_HEIGHT = 600;
const DEFAULT_WINDOW_WIDTH  = 800;
const FRAME_WIDTH           = 10;
const TAB_HEIGHT            = 37;
const HEIGHT_COMPENSATION   = FRAME_WIDTH + TAB_HEIGHT;
const WIDTH_COMPENSATION    = FRAME_WIDTH + FRAME_WIDTH;

let isAppHidden      = false;
// let isAppLocked      = false;
let mainWindowId     = null;
let settingsWindowId = null;

objc.framework('Cocoa');

module.exports = {
	init() {
		this.negativeMenu = require('./negative-menu');
		this.negativeSettings = require('./negative-settings');

		this.negativeMenu.init(this);
		this.negativeSettings.init(() => {
			const negativeWindows = this.negativeSettings.get('windowSettings');

			if (!negativeWindows || negativeWindows.length === 0 || process.env.NEGATIVE_IGNORE_WINDOW_SETTINGS === 'true') {
				this.initNegativeWindow();
			} else {
				negativeWindows.forEach((_window) => this.initNegativeWindow(_window));
			}
		});

		this.initGlobalShortcuts();
		this.initIpcEvents();

		if (process.env.NODE_ENV === 'development') {
			require('./negative-test').init();
		}
	},

	quit() {
		const windowsKeys     = Object.keys(electronWindow.windows).filter((key) => key !== settingsWindowId);
		const windowCount     = windowsKeys.length;
		const negativeWindows = [];

		if (windowCount > 0) {
			ipcMain.on('window-settings-response', (evt, _window) => {
				negativeWindows.push(_window);

				if (negativeWindows.length === windowCount) {
					this.negativeSettings.set('windowSettings', negativeWindows);
					this.negativeSettings.save(() => app.quit());
				}
			});

			for (const key of windowsKeys) {
				const _window = electronWindow.windows[key];
				const windowSettings = {
					bounds: _window.getBounds()
				};

				// Always set the last focused window as the window to be focused
				// on next launch. All others should not be focused.
				windowSettings.isFocused = (key === mainWindowId);

				_window.webContents.send('window-settings-request', windowSettings);
			}
		} else {
			this.negativeSettings.set('windowSettings', []);
			this.negativeSettings.save(() => app.quit());
		}
	},

	initIpcEvents() {
		ipcMain.on('fit-window-to-image', (evt, dimensions) => {
			const focusedWindow = BrowserWindow.getFocusedWindow();

			if (focusedWindow && dimensions) {
				focusedWindow.setSize(dimensions[0] + WIDTH_COMPENSATION, dimensions[1] + HEIGHT_COMPENSATION, true);
			}
		});

		ipcMain.on('set-settings-request', (evt, settings) => {
			for (const key in settings) {
				if (settings.hasOwnProperty(key)) {
					this.negativeSettings.set(key, settings[key]);
				}
			}

			this.negativeSettings.save();

			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key) && settingsWindowId !== key) {
					const shouldShowTips = this.negativeSettings.get('shouldShowTips');
					const _window        = windows[key];

					_window.webContents.executeJavaScript(`negative.frameController.setShouldShowTips(${shouldShowTips})`);
				}
			}
		});

		ipcMain.on('get-settings-request', (evt) => {
			evt.sender.send('get-settings-response', this.negativeSettings.settings);
		});
	},

	initGlobalShortcuts() {
		globalShortcut.register('Command+Control+H', () => {
			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key) && settingsWindowId !== key) {
					const _window = windows[key];

					if (isAppHidden) {
						_window.show();
					} else {
						_window.hide();
					}
				}
			}

			isAppHidden = !isAppHidden;
		});

		// globalShortcut.register('Command+Control+L', function () {
		//	 let windows = electronWindow.windows;
		//
		//	 for (let key in windows) {
		//		 if (windows.hasOwnProperty(key) && settingsWindowId != key) {
		//			 let _window = windows[key];
		//
		//			 _window.setIgnoreMouseEvents(!isAppLocked);
		//		 }
		//	 }
		//
		//	 if (isAppLocked) {
		//		 changeFocusToWindow(windows[mainWindowId])
		//	 } else {
		//		 changeFocusToWindow(null)
		//	 }
		//
		//	 isAppLocked = !isAppLocked;
		// })
	},

	getNewNegativeWindow(_window) {
		const bounds = _window ? _window.bounds : {};

		const newWindow = electronWindow.createWindow({
			acceptFirstMouse: true,
			alwaysOnTop: true,
			enableLargerThanScreen: true,
			fullscreen: false,
			hasShadow: false,
			height: bounds.height || DEFAULT_WINDOW_HEIGHT,
			frame: false,
			title: 'Negative',
			transparent: true,
			webPreferences: {
				webgl: false,
				webaudio: false,
				experimentalFeatures: true,
				experimentalCanvasFeatures: true
			},
			width: bounds.width || DEFAULT_WINDOW_WIDTH,
			x: bounds.x,
			y: bounds.y
		});

		newWindow.setSheetOffset(TAB_HEIGHT);

		return newWindow;
	},

	initNegativeWindow(_window) {
		const newWindow = this.getNewNegativeWindow(_window);
		const isFocused = _window && _window.isFocused !== false;

		newWindow.webContents.on('dom-ready', () => {
			const delayBeforeWindowFocus = 100;

			setTimeout(() => {
				if (_window) {
					if (isFocused) {
						this.setWindowFocused(newWindow);
						newWindow.focus();
					} else {
						this.setWindowBlurred(newWindow);
						newWindow.blur();
					}
				} else {
					this.changeFocusToWindow(newWindow);
				}
			}, delayBeforeWindowFocus);
		});

		newWindow.showUrl(path.resolve(__dirname, '../view/index.html'), () => {
			const self = this;

			newWindow.on('focus', function () {
				self.changeFocusToWindow(this);
			});

			newWindow.on('blur', () => {
				this.changeFocusToWindow(null);
			});

			newWindow.on('closed', () => {
				if (Object.keys(electronWindow.windows).length === 0) {
					this.negativeMenu.refreshForNoWindow();
				}
			});
		});

		const undoManagers = _window && _window.undoManagers;

		if (!undoManagers) {
			// New empty window
			newWindow.webContents.executeJavaScript(`
				negative.tabsController.addTab(true);
				negative.tabsController.selectTabByIndex(0);
			`);
		} else {
			let addTabsString = '';
			undoManagers.forEach((undoManager) => {
				const undoManagerString = JSON.stringify(undoManager);
				addTabsString += `negative.addTabForUndoManager(${undoManagerString});`;
			});
			newWindow.webContents.executeJavaScript(`
				${addTabsString}
				negative.tabsController.deselectTab();
				negative.tabsController.selectTabByIndex(0);
			`);
		}
	},

	initSettingsWindow() {
		if (!settingsWindowId) {
			const settingsWindow = electronWindow.createWindow({
				acceptFirstMouse: true,
				alwaysOnTop: true,
				fullscreen: false,
				height: 92,
				resizable: false,
				title: 'Negative - Preferences',
				webPreferences: {
					webgl: false,
					webaudio: false,
					experimentalFeatures: false,
					experimentalCanvasFeatures: false
				},
				width: 320
			});

			settingsWindow.showUrl(path.resolve(__dirname, '../view/settings.html'), () => {
				settingsWindowId = `${settingsWindow.id}`;

				settingsWindow.on('closed', () => {
					settingsWindowId = null;

					if (Object.keys(electronWindow.windows).length === 0) {
						this.negativeMenu.refreshForNoWindow();
					}
				});

				settingsWindow.on('focus', () => this.negativeMenu.refreshForSettingsWindow());

				this.negativeMenu.refreshForSettingsWindow();
			});
		} else {
			electronWindow.windows[settingsWindowId].focus();
		}
	},

	changeFocusToWindow(newWindow) {
		const currentWindow = electronWindow.windows[mainWindowId];

		if (currentWindow) {
			let currentWindowScript = 'negative.frameController.unsetFocused();';

			if (newWindow) {
				currentWindowScript += 'negative.frameController.unsetPrimary();';
			}

			currentWindow.webContents.executeJavaScript(currentWindowScript);
		}

		if (newWindow) {
			newWindow.webContents.executeJavaScript(`
				negative.frameController.setFocused();
				negative.frameController.setPrimary();
				negative.refreshMenu();
			`);

			mainWindowId = `${newWindow.id}`;
		}
	},

	setWindowFocused(_window) {
		if (_window) {
			_window.webContents.executeJavaScript(`
				negative.frameController.setFocused();
				negative.frameController.setPrimary();
				negative.refreshMenu();
			`);

			mainWindowId = `${_window.id}`;
		}
	},

	setWindowBlurred(_window) {
		if (_window) {
			_window.webContents.executeJavaScript(`
				negative.frameController.unsetFocused();
				negative.frameController.unsetPrimary();
			`);
		}
	},

	captureRegionBehindWindow() {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			const bounds         = focusedWindow.getBounds();
			const newImageX      = bounds.x + FRAME_WIDTH;
			const newImageY      = bounds.y + TAB_HEIGHT;
			const newImageWidth  = bounds.width - WIDTH_COMPENSATION;
			const newImageHeight = bounds.height - HEIGHT_COMPENSATION;

			/* eslint-disable new-cap */
			const pool     = objc.NSAutoreleasePool('alloc')('init');
			const rect     = objc.NSMakeRect(newImageX, newImageY, newImageWidth, newImageHeight);
			const windowId = objc.NSApplication('sharedApplication')('keyWindow')('windowNumber');
			const cgImage  = objc.CGWindowListCreateImage(rect, objc.kCGWindowListOptionOnScreenBelowWindow, windowId, objc.kCGWindowImageDefault);
			const newRep   = objc.NSBitmapImageRep('alloc')('initWithCGImage', cgImage);
			/* eslint-enable new-cap */

			newRep('setSize', rect.size);

			const pngData = newRep('representationUsingType', objc.NSPNGFileType, 'properties', null);
			const base64  = pngData('base64EncodedStringWithOptions', 0);
			const uri     = `data:image/png;base64,${base64}`;

			focusedWindow.webContents.executeJavaScript(`
				negative.frameController.setImageAndSize('${uri}', ${newImageWidth}, ${newImageHeight});
				negative.saveForUndo({
					imageDimensions: [${newImageWidth}, ${newImageHeight}],
					imageSrc: '${uri}'
				});
			`);

			pool('drain');
		}
	},

	move(direction, amount) {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			const currentPosition = focusedWindow.getPosition();

			switch (direction) {
				case 'up':
					currentPosition[1] -= amount;
					break;
				case 'right':
					currentPosition[0] += amount;
					break;
				case 'down':
					currentPosition[1] += amount;
					break;
				case 'left':
					currentPosition[0] -= amount;
					break;
				default:
					return;
			}

			focusedWindow.setPosition(currentPosition[0], currentPosition[1]);
		}
	},

	executeJavaScriptInFocusedWindow(js) {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			focusedWindow.webContents.executeJavaScript(js);
		}
	},

	removeImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.removeImage();
			negative.saveForUndo({
				imageDimensions: null,
				imageSrc: null
			});
		`);
	},

	undo() {
		this.executeJavaScriptInFocusedWindow(`
			negative.undo();
		`);
	},

	redo() {
		this.executeJavaScriptInFocusedWindow(`
			negative.redo();
		`);
	},

	copy() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.copy();
		`);
	},

	paste() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.paste();
		`);
	},

	addTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.addTab(true);
		`);
	},

	closeTab() {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			focusedWindow.webContents.executeJavaScript(`
				negative.tabsController.closeTab(${focusedWindow.tabIndex});
			`);
		}
	},

	selectNextTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectNextTab();
		`);
	},

	selectNextTabAndResizeWindowToFitImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectNextTab();
			negative.tabsController.fitWindowToImage();
		`);
	},

	selectPreviousTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectPreviousTab();
		`);
	},

	selectPreviousTabAndResizeWindowToFitImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectPreviousTab();
			negative.tabsController.fitWindowToImage();
		`);
	},

	fitWindowToImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.fitWindowToImage();
		`);
	},

	zoomIn() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomIn();
		`);
	},

	zoomOut() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomOut();
		`);
	},

	zoomTo(zoomLevel) {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomTo(${zoomLevel});
		`);
	},

	reset() {
		const _reset = function _reset() {
			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key)) {
					windows[key].close();
				}
			}

			this.initNegativeWindow();
		}.bind(this);

		if (process.env.NEGATIVE_SKIP_RESET_DIALOG === 'true') {
			_reset();
		} else {
			ipcMain.once('confirm-reset-response', (event, shouldReset) => {
				if (shouldReset) {
					_reset();
				}
			});

			this.executeJavaScriptInFocusedWindow(`
				negative.confirmReset();
			`);
		}

	}
};
<commit_msg>Cleanup quit<commit_after>'use strict';

const {
	app,
	BrowserWindow,
	globalShortcut,
	ipcMain
} = require('electron');

const electronWindow   = require('electron-window');
const path             = require('path');
const objc             = require('nodobjc');

const DEFAULT_WINDOW_HEIGHT = 600;
const DEFAULT_WINDOW_WIDTH  = 800;
const FRAME_WIDTH           = 10;
const TAB_HEIGHT            = 37;
const HEIGHT_COMPENSATION   = FRAME_WIDTH + TAB_HEIGHT;
const WIDTH_COMPENSATION    = FRAME_WIDTH + FRAME_WIDTH;

let isAppHidden      = false;
// let isAppLocked      = false;
let mainWindowId     = null;
let settingsWindowId = null;

objc.framework('Cocoa');

module.exports = {
	init() {
		this.negativeMenu = require('./negative-menu');
		this.negativeSettings = require('./negative-settings');

		this.negativeMenu.init(this);
		this.negativeSettings.init(() => {
			const negativeWindows = this.negativeSettings.get('windowSettings');

			if (!negativeWindows || negativeWindows.length === 0 || process.env.NEGATIVE_IGNORE_WINDOW_SETTINGS === 'true') {
				this.initNegativeWindow();
			} else {
				negativeWindows.forEach((_window) => this.initNegativeWindow(_window));
			}
		});

		this.initGlobalShortcuts();
		this.initIpcEvents();

		if (process.env.NODE_ENV === 'development') {
			require('./negative-test').init();
		}
	},

	quit() {
		const windowsKeys     = Object.keys(electronWindow.windows).filter((key) => key !== settingsWindowId);
		const windowCount     = windowsKeys.length;
		const negativeWindows = [];

		if (windowCount > 0) {
			ipcMain.on('window-settings-response', (evt, _window) => {
				negativeWindows.push(_window);

				if (negativeWindows.length === windowCount) {
					this.negativeSettings.set('windowSettings', negativeWindows);
					this.negativeSettings.save(() => app.quit());
				}
			});

			for (const key of windowsKeys) {
				const _window = electronWindow.windows[key];
				const windowSettings = {
					bounds: _window.getBounds(),
					// Always set the last focused window as the window to be focused
					// on next launch. All others should not be focused.
					isFocused: (key === mainWindowId)
				};

				_window.webContents.send('window-settings-request', windowSettings);
			}
		} else {
			this.negativeSettings.set('windowSettings', []);
			this.negativeSettings.save(() => app.quit());
		}
	},

	initIpcEvents() {
		ipcMain.on('fit-window-to-image', (evt, dimensions) => {
			const focusedWindow = BrowserWindow.getFocusedWindow();

			if (focusedWindow && dimensions) {
				focusedWindow.setSize(dimensions[0] + WIDTH_COMPENSATION, dimensions[1] + HEIGHT_COMPENSATION, true);
			}
		});

		ipcMain.on('set-settings-request', (evt, settings) => {
			for (const key in settings) {
				if (settings.hasOwnProperty(key)) {
					this.negativeSettings.set(key, settings[key]);
				}
			}

			this.negativeSettings.save();

			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key) && settingsWindowId !== key) {
					const shouldShowTips = this.negativeSettings.get('shouldShowTips');
					const _window        = windows[key];

					_window.webContents.executeJavaScript(`negative.frameController.setShouldShowTips(${shouldShowTips})`);
				}
			}
		});

		ipcMain.on('get-settings-request', (evt) => {
			evt.sender.send('get-settings-response', this.negativeSettings.settings);
		});
	},

	initGlobalShortcuts() {
		globalShortcut.register('Command+Control+H', () => {
			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key) && settingsWindowId !== key) {
					const _window = windows[key];

					if (isAppHidden) {
						_window.show();
					} else {
						_window.hide();
					}
				}
			}

			isAppHidden = !isAppHidden;
		});

		// globalShortcut.register('Command+Control+L', function () {
		//	 let windows = electronWindow.windows;
		//
		//	 for (let key in windows) {
		//		 if (windows.hasOwnProperty(key) && settingsWindowId != key) {
		//			 let _window = windows[key];
		//
		//			 _window.setIgnoreMouseEvents(!isAppLocked);
		//		 }
		//	 }
		//
		//	 if (isAppLocked) {
		//		 changeFocusToWindow(windows[mainWindowId])
		//	 } else {
		//		 changeFocusToWindow(null)
		//	 }
		//
		//	 isAppLocked = !isAppLocked;
		// })
	},

	getNewNegativeWindow(_window) {
		const bounds = _window ? _window.bounds : {};

		const newWindow = electronWindow.createWindow({
			acceptFirstMouse: true,
			alwaysOnTop: true,
			enableLargerThanScreen: true,
			fullscreen: false,
			hasShadow: false,
			height: bounds.height || DEFAULT_WINDOW_HEIGHT,
			frame: false,
			title: 'Negative',
			transparent: true,
			webPreferences: {
				webgl: false,
				webaudio: false,
				experimentalFeatures: true,
				experimentalCanvasFeatures: true
			},
			width: bounds.width || DEFAULT_WINDOW_WIDTH,
			x: bounds.x,
			y: bounds.y
		});

		newWindow.setSheetOffset(TAB_HEIGHT);

		return newWindow;
	},

	initNegativeWindow(_window) {
		const newWindow = this.getNewNegativeWindow(_window);
		const isFocused = _window && _window.isFocused !== false;

		newWindow.webContents.on('dom-ready', () => {
			const delayBeforeWindowFocus = 100;

			setTimeout(() => {
				if (_window) {
					if (isFocused) {
						this.setWindowFocused(newWindow);
						newWindow.focus();
					} else {
						this.setWindowBlurred(newWindow);
						newWindow.blur();
					}
				} else {
					this.changeFocusToWindow(newWindow);
				}
			}, delayBeforeWindowFocus);
		});

		newWindow.showUrl(path.resolve(__dirname, '../view/index.html'), () => {
			const self = this;

			newWindow.on('focus', function () {
				self.changeFocusToWindow(this);
			});

			newWindow.on('blur', () => {
				this.changeFocusToWindow(null);
			});

			newWindow.on('closed', () => {
				if (Object.keys(electronWindow.windows).length === 0) {
					this.negativeMenu.refreshForNoWindow();
				}
			});
		});

		const undoManagers = _window && _window.undoManagers;

		if (!undoManagers) {
			// New empty window
			newWindow.webContents.executeJavaScript(`
				negative.tabsController.addTab(true);
				negative.tabsController.selectTabByIndex(0);
			`);
		} else {
			let addTabsString = '';
			undoManagers.forEach((undoManager) => {
				const undoManagerString = JSON.stringify(undoManager);
				addTabsString += `negative.addTabForUndoManager(${undoManagerString});`;
			});
			newWindow.webContents.executeJavaScript(`
				${addTabsString}
				negative.tabsController.deselectTab();
				negative.tabsController.selectTabByIndex(0);
			`);
		}
	},

	initSettingsWindow() {
		if (!settingsWindowId) {
			const settingsWindow = electronWindow.createWindow({
				acceptFirstMouse: true,
				alwaysOnTop: true,
				fullscreen: false,
				height: 92,
				resizable: false,
				title: 'Negative - Preferences',
				webPreferences: {
					webgl: false,
					webaudio: false,
					experimentalFeatures: false,
					experimentalCanvasFeatures: false
				},
				width: 320
			});

			settingsWindow.showUrl(path.resolve(__dirname, '../view/settings.html'), () => {
				settingsWindowId = `${settingsWindow.id}`;

				settingsWindow.on('closed', () => {
					settingsWindowId = null;

					if (Object.keys(electronWindow.windows).length === 0) {
						this.negativeMenu.refreshForNoWindow();
					}
				});

				settingsWindow.on('focus', () => this.negativeMenu.refreshForSettingsWindow());

				this.negativeMenu.refreshForSettingsWindow();
			});
		} else {
			electronWindow.windows[settingsWindowId].focus();
		}
	},

	changeFocusToWindow(newWindow) {
		const currentWindow = electronWindow.windows[mainWindowId];

		if (currentWindow) {
			let currentWindowScript = 'negative.frameController.unsetFocused();';

			if (newWindow) {
				currentWindowScript += 'negative.frameController.unsetPrimary();';
			}

			currentWindow.webContents.executeJavaScript(currentWindowScript);
		}

		if (newWindow) {
			newWindow.webContents.executeJavaScript(`
				negative.frameController.setFocused();
				negative.frameController.setPrimary();
				negative.refreshMenu();
			`);

			mainWindowId = `${newWindow.id}`;
		}
	},

	setWindowFocused(_window) {
		if (_window) {
			_window.webContents.executeJavaScript(`
				negative.frameController.setFocused();
				negative.frameController.setPrimary();
				negative.refreshMenu();
			`);

			mainWindowId = `${_window.id}`;
		}
	},

	setWindowBlurred(_window) {
		if (_window) {
			_window.webContents.executeJavaScript(`
				negative.frameController.unsetFocused();
				negative.frameController.unsetPrimary();
			`);
		}
	},

	captureRegionBehindWindow() {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			const bounds         = focusedWindow.getBounds();
			const newImageX      = bounds.x + FRAME_WIDTH;
			const newImageY      = bounds.y + TAB_HEIGHT;
			const newImageWidth  = bounds.width - WIDTH_COMPENSATION;
			const newImageHeight = bounds.height - HEIGHT_COMPENSATION;

			/* eslint-disable new-cap */
			const pool     = objc.NSAutoreleasePool('alloc')('init');
			const rect     = objc.NSMakeRect(newImageX, newImageY, newImageWidth, newImageHeight);
			const windowId = objc.NSApplication('sharedApplication')('keyWindow')('windowNumber');
			const cgImage  = objc.CGWindowListCreateImage(rect, objc.kCGWindowListOptionOnScreenBelowWindow, windowId, objc.kCGWindowImageDefault);
			const newRep   = objc.NSBitmapImageRep('alloc')('initWithCGImage', cgImage);
			/* eslint-enable new-cap */

			newRep('setSize', rect.size);

			const pngData = newRep('representationUsingType', objc.NSPNGFileType, 'properties', null);
			const base64  = pngData('base64EncodedStringWithOptions', 0);
			const uri     = `data:image/png;base64,${base64}`;

			focusedWindow.webContents.executeJavaScript(`
				negative.frameController.setImageAndSize('${uri}', ${newImageWidth}, ${newImageHeight});
				negative.saveForUndo({
					imageDimensions: [${newImageWidth}, ${newImageHeight}],
					imageSrc: '${uri}'
				});
			`);

			pool('drain');
		}
	},

	move(direction, amount) {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			const currentPosition = focusedWindow.getPosition();

			switch (direction) {
				case 'up':
					currentPosition[1] -= amount;
					break;
				case 'right':
					currentPosition[0] += amount;
					break;
				case 'down':
					currentPosition[1] += amount;
					break;
				case 'left':
					currentPosition[0] -= amount;
					break;
				default:
					return;
			}

			focusedWindow.setPosition(currentPosition[0], currentPosition[1]);
		}
	},

	executeJavaScriptInFocusedWindow(js) {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			focusedWindow.webContents.executeJavaScript(js);
		}
	},

	removeImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.removeImage();
			negative.saveForUndo({
				imageDimensions: null,
				imageSrc: null
			});
		`);
	},

	undo() {
		this.executeJavaScriptInFocusedWindow(`
			negative.undo();
		`);
	},

	redo() {
		this.executeJavaScriptInFocusedWindow(`
			negative.redo();
		`);
	},

	copy() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.copy();
		`);
	},

	paste() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.paste();
		`);
	},

	addTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.addTab(true);
		`);
	},

	closeTab() {
		const focusedWindow = BrowserWindow.getFocusedWindow();

		if (focusedWindow) {
			focusedWindow.webContents.executeJavaScript(`
				negative.tabsController.closeTab(${focusedWindow.tabIndex});
			`);
		}
	},

	selectNextTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectNextTab();
		`);
	},

	selectNextTabAndResizeWindowToFitImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectNextTab();
			negative.tabsController.fitWindowToImage();
		`);
	},

	selectPreviousTab() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectPreviousTab();
		`);
	},

	selectPreviousTabAndResizeWindowToFitImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.selectPreviousTab();
			negative.tabsController.fitWindowToImage();
		`);
	},

	fitWindowToImage() {
		this.executeJavaScriptInFocusedWindow(`
			negative.tabsController.fitWindowToImage();
		`);
	},

	zoomIn() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomIn();
		`);
	},

	zoomOut() {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomOut();
		`);
	},

	zoomTo(zoomLevel) {
		this.executeJavaScriptInFocusedWindow(`
			negative.frameController.zoomTo(${zoomLevel});
		`);
	},

	reset() {
		const _reset = function _reset() {
			const windows = electronWindow.windows;

			for (const key in windows) {
				if (windows.hasOwnProperty(key)) {
					windows[key].close();
				}
			}

			this.initNegativeWindow();
		}.bind(this);

		if (process.env.NEGATIVE_SKIP_RESET_DIALOG === 'true') {
			_reset();
		} else {
			ipcMain.once('confirm-reset-response', (event, shouldReset) => {
				if (shouldReset) {
					_reset();
				}
			});

			this.executeJavaScriptInFocusedWindow(`
				negative.confirmReset();
			`);
		}

	}
};
<|endoftext|>


<commit_before>define(["tablesort", "tablesort.numeric"], function (Tablesort) {
  return function(router) {
    function showUptime(el, now, d) {
      var uptime
      if (d.flags.online && "uptime" in d.statistics)
        uptime = Math.round(d.statistics.uptime / 3600)
      else if (!d.flags.online && "lastseen" in d)
        uptime = Math.round(-(now - d.lastseen) / 3600000)

      var s = ""

      if (uptime !== undefined)
        if (Math.abs(uptime) >= 24)
          s = Math.round(uptime / 24) + "d"
        else
          s = uptime + "h"

      el.textContent = s
      el.setAttribute("data-sort", uptime !== undefined ? -uptime : 0)
    }

    var self = this
    var el

    self.render = function (d) {
      el = document.createElement("div")
      d.appendChild(el)
    }

    self.setData = function (data) {
      if (data.nodes.all.length === 0)
        return

      var h2 = document.createElement("h2")
      h2.textContent = "Alle Knoten"
      el.appendChild(h2)

      var table = document.createElement("table")
      var thead = document.createElement("thead")

      var tr = document.createElement("tr")
      var th1 = document.createElement("th")
      th1.textContent = "Knoten"
      th1.classList.add("sort-default")
      tr.appendChild(th1)

      var th2 = document.createElement("th")
      th2.textContent = "Uptime"
      tr.appendChild(th2)

      var th3 = document.createElement("th")
      th3.textContent = "Clients"
      tr.appendChild(th3)

      thead.appendChild(tr)

      table.appendChild(thead)

      var tbody = document.createElement("tbody")

      data.nodes.all.forEach( function (d) {
        var row = document.createElement("tr")

        var td1 = document.createElement("td")
        var a = document.createElement("a")
        a.textContent = d.nodeinfo.hostname
        a.href = "#"
        a.onclick = router.node(d)
        a.classList.add("hostname")
        a.classList.add(d.flags.online ? "online" : "offline")
        td1.appendChild(a)
        row.appendChild(td1)

        if (has_location(d)) {
          var span = document.createElement("span")
          span.classList.add("icon")
          span.classList.add("ion-location")
          td1.appendChild(span)
        }

        var td2 = document.createElement("td")
        showUptime(td2, data.now, d)
        row.appendChild(td2)

        var td3 = document.createElement("td")
        td3.textContent = "clients" in d.statistics ? d.statistics.clients : ""
        row.appendChild(td3)

        tbody.appendChild(row)
      })

      table.appendChild(tbody)

      new Tablesort(table)

      el.appendChild(table)
   }
  }
})
<commit_msg>make nodelist.setData idempotent<commit_after>define(["tablesort", "virtual-dom", "tablesort.numeric"],
  function (Tablesort, V) {
  return function(router) {
    function showUptime(now, d) {
      var uptime
      if (d.flags.online && "uptime" in d.statistics)
        uptime = Math.round(d.statistics.uptime / 3600)
      else if (!d.flags.online && "lastseen" in d)
        uptime = Math.round(-(now - d.lastseen) / 3600000)

      var s = ""

      if (uptime !== undefined)
        if (Math.abs(uptime) >= 24)
          s = Math.round(uptime / 24) + "d"
        else
          s = uptime + "h"

      return {v: s, sort: uptime !== undefined ? -uptime : 0}
    }

    var self = this
    var el, tbody, sort

    self.render = function (d) {
      el = document.createElement("div")
      d.appendChild(el)
    }

    self.setData = function (data) {
      if (data.nodes.all.length === 0)
        return

      if (!tbody) {
        var h2 = document.createElement("h2")
        h2.textContent = "Alle Knoten"
        el.appendChild(h2)

        var table = document.createElement("table")
        el.appendChild(table)

        var thead = document.createElement("thead")

        var tr = document.createElement("tr")
        var th1 = document.createElement("th")
        th1.textContent = "Knoten"
        th1.classList.add("sort-default")
        tr.appendChild(th1)

        var th2 = document.createElement("th")
        th2.textContent = "Uptime"
        tr.appendChild(th2)

        var th3 = document.createElement("th")
        th3.textContent = "Clients"
        tr.appendChild(th3)

        thead.appendChild(tr)
        table.appendChild(thead)

        tbody = document.createElement("tbody")
        tbody.last = V.h("tbody")
        table.appendChild(tbody)

        sort = new Tablesort(table)
      }

      var items = data.nodes.all.map( function (d) {
        var td1Content = []
        var aClass = ["hostname", d.flags.online ? "online" : "offline"]

        td1Content.push(V.h("a", { className: aClass.join(" "),
                                   onclick: router.node(d),
                                   href: "#"
                                 }, d.nodeinfo.hostname))

        if (has_location(d))
          td1Content.push(V.h("span", {className: "icon ion-location"}))

        var uptime = showUptime(data.now, d)

        var td1 = V.h("td", td1Content)
        var td2 = V.h("td", {attributes: { "data-sort": uptime.sort }}, uptime.v)
        var td3 = V.h("td", "clients" in d.statistics ? d.statistics.clients : "")

        return V.h("tr", [td1, td2, td3])
      })

      var tbodyNew = V.h("tbody", items)
      tbody = V.patch(tbody, V.diff(tbody.last, tbodyNew))
      tbody.last = tbodyNew
      sort.refresh()
   }
  }
})
<|endoftext|>


<commit_before>'use babel';

import OpenGithubDiag from './dialogs/open-github-dialog';
import StatusView from './view/status-view';
import { CompositeDisposable } from 'atom';
import packageConfig from './config.json';
import githubAPI from './github-integrate/auth';
import Repository from './github-integrate/repository';

var TreeView, Directory;

export default {
  config: packageConfig,
  modalPanel: null,
  subscriptions: null,
  treeView: null,

  activate(state) {
    if (!TreeView) {
      TreeView = require('./view/tree-view');
      Directory = require('./directory');
    }

    atom.project.octophusdir = new Directory({
      parent: null,
      name: "/"
    });

    dir = atom.project.octophusdir;
    dir.folders = [new Directory({
      parent: dir,
      name: "folder1"
    })];
    dir.name = "user/repo:master";

    dir2 = new Directory({
      parent: null,
      name: "hello"
    });

    if (!this.treeView) {
      this.treeView = new TreeView();
    }
    this.treeView.detach();
    atom.project.octophustree = this.treeView;

    atom.commands.add('atom-workspace', {

      'octophus:toggle': () => this.toggle(),

      'octophus:open-repo': () => {
        if (!githubAPI.tokenReady) {
          atom.notifications.addError("Please set GitHub Token first in octophus!");
          return;
        }
        let dialog = new OpenGithubDiag();
        dialog.on('open-repo', (e, fullrepo) => {
          console.log(fullrepo);
          dialog.detach();
          this.statusView.attach();

          let user = fullrepo.split('/')[0];
          let repo = fullrepo.split('/')[1].split(':')[0];
          let brench = fullrepo.split(':')[1];
          let repository = new Repository(user, repo, brench);
          repository.getDirectory(function (ghDir) {
            name = ghDir.repo.name;
            dir = new Directory({
              parent: null,
              name: name,
              ghDir: ghDir
            });
            dir.open(false, function() {} );
            atom.project.octophustree.setRoot(dir);
          });
        });
        dialog.attach();
      }
    });

    atom.workspace.observeTextEditors((ed) => {
      const buffer = ed.buffer;
      let path = null;

      const listener = buffer.onDidSave((ev) => {
        path = ev.path;
        // TODO save file
      });
      const listener2 = buffer.onDidDestroy(() => {
        if (path) {
          // TODO closed tab
        }
        listener.dispose();
        listener2.dispose();
      });

    });

    atom.config.observe('octophus.githubToken', this.setGithubToken);
  },

  setGithubToken: (token) => {
    if (!token || token.length <= 20) {
      atom.notifications.addWarning("Please set GitHub Token in octophus first.");
      atom.notifications.addWarning("We need repo access for github. See https://github.com/settings/tokens/new");
      return;
    }
    githubAPI.tokenReady = true;
    githubAPI.authenticate({
      type: "token",
      token: token
    });
  },

  consumeStatusBar(statusBar) {
    this.statusView = new StatusView();
    this.statusView.initialize(statusBar);
    if (!githubAPI.tokenReady) {
      this.statusView.attach('token');
    }
  },

  deactivate() {
    this.treeView.detach();
    this.modalPanel.destroy();
    this.subscriptions.dispose();
  },

  serialize() {
    return;
  },

  toggle() {
    this.treeView.toggle();
    console.log('Octophus was toggled!');
    return;
  }

};
<commit_msg>fix wrap directory and file<commit_after>'use babel';

import OpenGithubDiag from './dialogs/open-github-dialog';
import StatusView from './view/status-view';
import { CompositeDisposable } from 'atom';
import packageConfig from './config.json';
import githubAPI from './github-integrate/auth';
import Repository from './github-integrate/repository';

var TreeView, Directory;

export default {
  config: packageConfig,
  modalPanel: null,
  subscriptions: null,
  treeView: null,

  activate(state) {
    if (!TreeView) {
      TreeView = require('./view/tree-view');
      Directory = require('./directory');
    }

    atom.project.octophusdir = new Directory({
      parent: null,
      name: "/"
    });

    dir = atom.project.octophusdir;
    dir.folders = [new Directory({
      parent: dir,
      name: "folder1"
    })];
    dir.name = "user/repo:master";

    dir2 = new Directory({
      parent: null,
      name: "hello"
    });

    if (!this.treeView) {
      this.treeView = new TreeView();
    }
    this.treeView.detach();
    atom.project.octophustree = this.treeView;

    atom.commands.add('atom-workspace', {

      'octophus:toggle': () => this.toggle(),

      'octophus:open-repo': () => {
        if (!githubAPI.tokenReady) {
          atom.notifications.addError("Please set GitHub Token first in octophus!");
          return;
        }
        let dialog = new OpenGithubDiag();
        dialog.on('open-repo', (e, fullrepo) => {
          console.log(fullrepo);
          dialog.detach();
          this.statusView.attach();

          let user = fullrepo.split('/')[0];
          let repo = fullrepo.split('/')[1].split(':')[0];
          let brench = fullrepo.split(':')[1];
          let repository = new Repository(user, repo, brench);
          repository.getDirectory(function (ghDir) {
            name = ghDir.repo.name;
            dir = new Directory({
              parent: null,
              name: name,
              ghDir: ghDir
            });
            dir.open(false, function() {atom.project.octophustree.setRoot(dir);} );
          });
        });
        dialog.attach();
      }
    });

    atom.workspace.observeTextEditors((ed) => {
      const buffer = ed.buffer;
      let path = null;

      const listener = buffer.onDidSave((ev) => {
        path = ev.path;
        // TODO save file
      });
      const listener2 = buffer.onDidDestroy(() => {
        if (path) {
          // TODO closed tab
        }
        listener.dispose();
        listener2.dispose();
      });

    });

    atom.config.observe('octophus.githubToken', this.setGithubToken);
  },

  setGithubToken: (token) => {
    if (!token || token.length <= 20) {
      atom.notifications.addWarning("Please set GitHub Token in octophus first.");
      atom.notifications.addWarning("We need repo access for github. See https://github.com/settings/tokens/new");
      return;
    }
    githubAPI.tokenReady = true;
    githubAPI.authenticate({
      type: "token",
      token: token
    });
  },

  consumeStatusBar(statusBar) {
    this.statusView = new StatusView();
    this.statusView.initialize(statusBar);
    if (!githubAPI.tokenReady) {
      this.statusView.attach('token');
    }
  },

  deactivate() {
    this.treeView.detach();
    this.modalPanel.destroy();
    this.subscriptions.dispose();
  },

  serialize() {
    return;
  },

  toggle() {
    this.treeView.toggle();
    console.log('Octophus was toggled!');
    return;
  }

};
<|endoftext|>


<commit_before>'use strict';

var openurl = require("openurl");
var request = require("request");

var getDocumentUrl = function(packageName, cb) {
  var docUrl = "https://www.npmjs.com/package/" + packageName;
  var registryUrl = "http://registry.npmjs.org/" + packageName;

  request(registryUrl, function(err, res, body) {
    if (err) return cb(err);

    var npmObj = JSON.parse(body);
    if (npmObj.error) {
      return cb(npmObj.error);
    }

    if (npmObj.homepage) {
      docUrl = npmObj.homepage;
    }

    cb(null, docUrl);
  });
};

module.exports = function(packageName) {
  getDocumentUrl(packageName, function(err, url) {
    if (err) {
      console.log(err);
      process.exit(1);
    }

    console.log('=> ' + url);
    openurl.open(url);
  });
};<commit_msg>fix bug: when package is not exists<commit_after>'use strict';

var openurl = require("openurl");
var request = require("request");

var getDocumentUrl = function(packageName, cb) {
  var docUrl = "https://www.npmjs.com/package/" + packageName;
  var registryUrl = "http://registry.npmjs.org/" + packageName;

  request(registryUrl, function(err, res, body) {
    if (err) return cb(err);

    if (body == '') {
      return cb('Can not found package: ' + packageName);
    }

    var npmObj = JSON.parse(body);
    if (npmObj.error) {
      return cb(npmObj.error);
    }

    if (npmObj.homepage) {
      docUrl = npmObj.homepage;
    }

    cb(null, docUrl);
  });
};

module.exports = function(packageName) {
  getDocumentUrl(packageName, function(err, url) {
    if (err) {
      console.log(err);
      process.exit(1);
    }

    console.log('=> ' + url);
    openurl.open(url);
  });
};<|endoftext|>


<commit_before>'use strict';

const api = require('./api');
const path = require('path');
const dotenv = require('dotenv');
const botkit = require('botkit');
const event = require('./events');
const helpers = require('./helpers');
const beepboop = require('beepboop-botkit');

//
// Configure environment
// Here we configure the applications environment.
//

dotenv.config({
    silent: true
});

//
// Create a bot controller.
// Here we create the bot controller.
//

let botkitController = botkit.slackbot({debug: false});

//
// Intergrate Beep Boop
// Here we overide the default controller with beepboops.
//

if (process.env.SLACK_TOKEN) {

    botkitController.spawn({token: process.env.SLACK_TOKEN}).startRTM(function (error, bot, payload) {
        if (error) throw error;
    });

} else {
    beepboop.start(botkitController, {debug: false});
}

//
// Commands
// Here we define any commands the bot will respond to.
//

botkitController.hears('help', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startPrivateConversation({user: message.user}, (error, conversation) => {

        if (!error) {

            conversation.say('`rant [id]` - Get a rant.');
            conversation.say('`latest` - Get the latest rant.');
            conversation.say('`search [term]` or `find [term]` - Get a rant matchimg the term.');
            conversation.say('`surprise` or `random` - Get a surprise (random) rant.');
            conversation.say('`weekly` - Get a weekly rant.');
        } else {
            conversation.say('I was unable to get the list of commands for you, sorry :cry:.');
        }
    });
});

botkitController.hears(['latest', 'recent', 'newest'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getRants('recent').then(rants => {

        const rant = rants[random];
        const random = helpers.random(0, 10);

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears('rant ([0-9]{4,})', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    if (message.match.indexOf(1) !== -1) {
        return false;
    }

    const id = message.match[1];

    bot.startTyping(message);

    api.getRant(id).then(rant => {

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears(['search (.*)', 'find (.*)', 'get (.*)'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    if (message.match.indexOf(1) !== -1) {
        return false;
    }

    const term = message.match[1];

    bot.startTyping(message);

    api.search(term).then(results => {

        const rant = results[random];
        const random = helpers.random(0, 10);

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears(['surprise', 'random'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getSurpriseRant().then(rant => {

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears('weekly', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getWeeklyRants().then(results => {

        const random = helpers.random(0, 10);
        const rant = results[random];

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

//
// Events
// Here we define any events the bot responds to.
//

botkitController.on('create_bot', (bot, config) => {

    if (!_bots[bot.config.token]) {

        bot.startRTM((error) => {

            if (!error) {

                trackBot(bot);

                bot.startPrivateConversation({user: config.createdBy}, (error, conversation) => {

                    if (error) {
                        throw new Error('Unable to start conversation.');
                    }

                    conversation.say('Hello, i am devRant bot, thanks for allowing me to be apart of your slack channel.');
                    conversation.say('To get started, `/invite` me to a channel.');
                    conversation.say('If you are unsure of anything, type `help` for a list of commands.');
                });
            } else {
                throw new Error('Unable to start RTM.');
            }
        });
    }
});
<commit_msg>Added json store back<commit_after>'use strict';

const api = require('./api');
const path = require('path');
const dotenv = require('dotenv');
const botkit = require('botkit');
const event = require('./events');
const helpers = require('./helpers');
const beepboop = require('beepboop-botkit');

//
// Configure environment
// Here we configure the applications environment.
//

dotenv.config({
    silent: true
});

//
// Create a bot controller.
// Here we create the bot controller.
//

let botkitController = botkit.slackbot({
    debug: false,
    json_file_store: './store'
});

//
// Intergrate Beep Boop
// Here we overide the default controller with beepboops.
//

if (process.env.SLACK_TOKEN) {

    botkitController.spawn({token: process.env.SLACK_TOKEN}).startRTM(function (error, bot, payload) {
        if (error) throw error;
    });

} else {
    beepboop.start(botkitController, {debug: false});
}

//
// Commands
// Here we define any commands the bot will respond to.
//

botkitController.hears('help', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startPrivateConversation({user: message.user}, (error, conversation) => {

        if (!error) {

            conversation.say('`rant [id]` - Get a rant.');
            conversation.say('`latest` - Get the latest rant.');
            conversation.say('`search [term]` or `find [term]` - Get a rant matchimg the term.');
            conversation.say('`surprise` or `random` - Get a surprise (random) rant.');
            conversation.say('`weekly` - Get a weekly rant.');
        } else {
            conversation.say('I was unable to get the list of commands for you, sorry :cry:.');
        }
    });
});

botkitController.hears(['latest', 'recent', 'newest'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getRants('recent').then(rants => {

        const rant = rants[random];
        const random = helpers.random(0, 10);

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears('rant ([0-9]{4,})', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    if (message.match.indexOf(1) !== -1) {
        return false;
    }

    const id = message.match[1];

    bot.startTyping(message);

    api.getRant(id).then(rant => {

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears(['search (.*)', 'find (.*)', 'get (.*)'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    if (message.match.indexOf(1) !== -1) {
        return false;
    }

    const term = message.match[1];

    bot.startTyping(message);

    api.search(term).then(results => {

        const rant = results[random];
        const random = helpers.random(0, 10);

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears(['surprise', 'random'], [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getSurpriseRant().then(rant => {

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

botkitController.hears('weekly', [event.DIRECT_MESSAGE, event.DIRECT_MENTION], (bot, message) => {

    bot.startTyping(message);

    api.getWeeklyRants().then(results => {

        const random = helpers.random(0, 10);
        const rant = results[random];

        const response = {
            attachments: [
                helpers.formatRant(rant)
            ]
        };

        bot.reply(message, response);

    }).catch((error) => {
        throw error;
    });
});

//
// Events
// Here we define any events the bot responds to.
//

botkitController.on('create_bot', (bot, config) => {

    if (!_bots[bot.config.token]) {

        bot.startRTM((error) => {

            if (!error) {

                trackBot(bot);

                bot.startPrivateConversation({user: config.createdBy}, (error, conversation) => {

                    if (error) {
                        throw new Error('Unable to start conversation.');
                    }

                    conversation.say('Hello, i am devRant bot, thanks for allowing me to be apart of your slack channel.');
                    conversation.say('To get started, `/invite` me to a channel.');
                    conversation.say('If you are unsure of anything, type `help` for a list of commands.');
                });
            } else {
                throw new Error('Unable to start RTM.');
            }
        });
    }
});
<|endoftext|>


<commit_before>EditableText = {};

EditableText.userCanEdit = function() {
  return (typeof this.userCanEdit !== 'undefined') ? this.userCanEdit : true;
}

EditableText.useTransactions = false;

EditableText.insert = function(Collection,doc) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('add ' + this.objectTypeText);
	}
	var new_id = tx.insert(Collection,doc,{instant:true});
	if (this.onAutoInsert && _.isString(this.onAutoInsert)) {
	  EditableText._executeCallback(this.onAutoInsert,this,[Collection.findOne({_id:new_id})]);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	var new_id = Collection.insert(doc);
	if (this.onAutoInsert && _.isString(this.onAutoInsert)) {
	  EditableText._executeCallback(this.onAutoInsert,this,[Collection.findOne({_id:new_id})]);
	}
  }
  return new_id;
}

EditableText.update = function(Collection,doc,modifier) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('update ' + this.objectTypeText);
	}
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
	// Important to send the id only, not the whole document ("self"),
	// as this update is fired from all sorts of contexts, some of which are incomplete --
	// this will force the transaction script to find the relevant document with its full context from the database
	tx.update(Collection,doc._id,modifier,{instant:true});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
    Collection.update({_id:doc._id},modifier);
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
  }
}

EditableText.remove = function(Collection,doc) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('remove ' + this.objectTypeText);
	}
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
	tx.remove(Collection,doc._id,{instant:true});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
    Collection.remove({_id:doc._id});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
  }
}

EditableText._executeCallback = function(callback,thisVal,args) {
  if (callback && _.isString(callback)) {
	var callback = EditableText._drillDown(window,callback);
	if (callback && _.isFunction(callback)) {
	  callback.apply(thisVal,args);
	}
  }
}

EditableText._drillDown = function(obj,key) {
  var pieces = key.split('.');
  if (pieces.length > 1) {
	var newObj = obj ? obj[pieces[0]] : {};
	pieces.shift();
	return EditableText._drillDown(newObj,pieces.join('.'));
  }
  else {
	if (obj) {
	  return obj[key];
	}
	else {
	  return; // undefined
	}
  }
}

EditableText._blockFocusoutForWYSIWYG = false;

EditableText.blockOkayEvent = false;

Template.editableText.helpers({
  context : function() {
	return this.context || this.document || this.doc || this.object || this.obj || this.data || this.dataContext || Blaze._parentData(1);
  }
});

// Returns an event map that handles the "escape" and "return" keys and
// "blur" events on a text input (given by selector) and interprets them
// as "ok" or "cancel".

EditableText.okCancelEvents = function (selector, callbacks) {
  var ok = callbacks.ok || function () {};
  var cancel = callbacks.cancel || function () {};
  var events = {};
  events['keydown '+selector+', focusout '+selector] =
	function (evt,tmpl) {
	  evt.stopImmediatePropagation();
	  var charCode = evt.which || evt.keyCode;
	  if (evt.type === "keydown") {
		switch (charCode) {
		  case 27 : // escape = cancel
			cancel.call(this, evt, tmpl);
			break;
		}
	  }
	  if (evt.type === "keydown" && (charCode === 13 && !evt.shiftKey) || evt.type === "focusout") {
		evt.preventDefault();
		// blur/return/enter = ok/submit if non-empty
		var value = $.trim(String($(evt.target).val() || "")); // evt.target.value
		if (value || this.removeEmpty || this.acceptEmpty) {
		  if (!EditableText.blockOkayEvent) {
			EditableText.blockOkayEvent = true;
			ok.call(this, value, evt, tmpl);
			Meteor.defer(function() {
			  EditableText.blockOkayEvent = false;
			});
		  }
		  else { // In case this value is set to true for some strange reason
			EditableText.blockOkayEvent = false;
		  }
		}
		else {
		  cancel.call(this, evt, tmpl);
		}
	  }
	};
  return events;
};

EditableText.activateInput = function (input,dontSelect) {
  if (input !== null) {
	input.focus();
	if (typeof dontSelect !== 'undefined' && dontSelect) {
	  return;
	}
	input.select();
  }
  else {
	console.log('Input was null');
  }
};

EditableText.sanitizeHTML = function(html) {
  // Find all instances of href=" and if the first four letters aren't "http", put in "http://"
  var currentIndex = 0,newIndex;
  do {
	newIndex = html.substr(currentIndex).indexOf('href="');
	if (newIndex !== -1) {
	  // Check whether the next four characters are http
	  currentIndex += newIndex;
	  if (html.substr(currentIndex+6,4) !== 'http') {
		currentIndex += 6;
	    html = [html.slice(0,currentIndex), 'http://', html.slice(currentIndex)].join('');
	  }
	  else {
		currentIndex += 4;
	  }
	}
  }
  while (newIndex !== -1);
  return html;
}

EditableText.extractNumber = function(raw) {
  if (typeof raw !== 'undefined') {
	var numbers = raw.match(/[0-9]+/);
	if (numbers) {
      return $.trim(parseInt(numbers[0], 10));
	}
  }
  return "";
}

/*EditableText.firstToLower = function (text) {
  return (text) ? text.charAt(0).toLowerCase() + text.slice(1) : "";
};*/

Blaze.body.events({
  'click .editable-text-trigger, mousedown .editable-text-trigger, dblclick .editable-text-trigger' : function(evt) {
	$(evt.currentTarget).find('.editable-text').trigger(evt.type);
  }
});

Template.editable_text_widget.helpers({
  value : function() {
	return this.value || this.context[this.field];
  },
  editing : function() {
	return Blaze._templateInstance().selected.get();
  },
  isWysiwyg : function() {
	return this.wysiwyg;
  },
  wysiwygContent : function() {
	var value = this.value || this.context[this.field];
	return value && new Spacebars.SafeString(value.replace(/\n/g,'<br />')) || "";
  },
  inputValue : function() {
	var value = this.value || this.context[this.field];
	return value && value.toString().replace(/"/g,""") || "";
  },
  substitute : function() {
	return !(this.value || this.context[this.field]) && (this.substitute && Spacebars.SafeString(this.substitute.toString()));
  },
  title : function() {
	return this.title || ((this.eventType === 'dblclick') ? 'double click' : 'click') + ' to edit';
  },
  canEditText : function() {
	return EditableText.userCanEdit.call(this);
  },
  content : function() {
	var value = this.value || this.context[this.field];
	var val = (_.isString(value)) ? new Spacebars.SafeString(value) : ((value || value === 0) ? value.toString() : "");
	return val;
  }
});

var okCancelEvents = {};

okCancelEvents.ok = function (value,evt,tmpl) {
  evt.stopImmediatePropagation();
  evt.stopPropagation();
  var currentValue = this.value || this.context[this.field], newValue;
  var type = this.type || 'string';
  switch (type) {
	case 'number' :
	  var rawNewValue = EditableText.extractNumber(value);
	  newValue = rawNewValue && parseInt(rawNewValue) || 0;
	  currentValue = currentValue && parseInt(currentValue) || 0;
	  break;
	default :
	  newValue = $.trim(value).replace(/mml:/g,"");
	  currentValue = currentValue || '';
	  break;
  }
  var updatedValue = {};
  updatedValue[this.field] = newValue;
  // Sometimes we don't want the actual field's value, we want whatever was passed in to the template as the 'value' keyword to be used to compare against
  if (newValue !== currentValue || (!newValue && this.removeEmpty)) {
	if (!newValue && this.removeEmpty) {
	  EditableText.remove.call(this,Mongo.Collection.get(this.collection),this.context);
	}
	else {
	  // Make the update
	  EditableText.update.call(this,Mongo.Collection.get(this.collection),this.context,{$set: updatedValue});
	}
  }
  tmpl.selected.set(false);
}

okCancelEvents.cancel = function (evt,tmpl) {
  evt.stopImmediatePropagation();
  tmpl.selected.set(false);
}

Template.editable_text_widget.events(EditableText.okCancelEvents('.wide-text-edit', okCancelEvents));
Template.editable_text_widget.events(EditableText.okCancelEvents('.text-area-edit', okCancelEvents));
Template.editable_text_widget.events({
  'mousedown .wysiwyg-content .editable-text a' : function(evt) {
	if (confirm("Click 'OK' to follow this link.\n\nClick 'Cancel' to edit.")) {
	  evt.stopPropagation();
	  window.open($(evt.target).attr('href'),'_blank');
	}
  },
  'mousedown .editable-text, click .editable-text, dblclick .editable-text' : function(evt,tmpl) {
	evt.stopPropagation();
	// This is the click event that opens the field for editing
	var collection = this.collection;
	var execute = this.execute;
	var eventType = this.eventType || ((this.wysiwyg) ? 'mousedown' : 'click');
	if (eventType !== evt.type) {
	  return;
	}
	var acceptEmpty = this.acceptEmpty;
	var textarea = this.textarea;
	var wysiwyg = this.wysiwyg;
	var removeEmpty = this.removeEmpty;
	var type = this.type || 'string';
	var objectTypeText = this.objectTypeText || '';
	var collectionKey = collection;
	var obj = this.context;
	if (this.autoInsert && obj && !obj._id && _.isObject(obj) && EditableText.userCanEdit.call(this)) { // This is quite involved -- you need an object with all context info, but no _id field for auto-creation to occur
	  if (typeof this.value !== 'undefined' && this.value !== obj[this.field]) {
		obj[this.field] = this.value;
	  }
	  // Create an object
	  var object_id = EditableText.insert.call(this,Mongo.Collection.get(collectionKey),obj);
	  Tracker.flush();
	  // TODO -- this new object needs to get selected (and the focus), wherever it lands in the DOM
	  // However, we have no easy way of getting its template instance, other than iterating over
	  // all template instances, looking for the one where the context._id matches the object_id value
	  // Unfortunately, I have no idea where the template instances are stored
	  // So using a filthy hack instead ...
	  // Getting the element(s) in the DOM with the right class and firing click event
	  $('.et-' + object_id).trigger((tmpl.data.wysiwyg) ? 'mousedown' : 'click');
	  return;
	}
	if (obj && !tmpl.selected.get()) {
	  if (EditableText.userCanEdit.call(this)) {
		document.activeElement.blur(); // Make sure the focusout event fires first when switching editable text objects, so that the first one gets saved properly
		tmpl.selected.set(true);
		Tracker.flush();
		if (!wysiwyg) {
		  EditableText.activateInput(tmpl.$((textarea) ? 'textarea' :  'input'),textarea);
		}
	  }
	}
  },
  'mousedown .wysiwyg-container' : function(evt) {
	EditableText._blockFocusoutForWYSIWYG = true;
	Meteor.defer(function() {
	  EditableText._blockFocusoutForWYSIWYG = false;
	});
  },
  'keydown .wysiwyg-container' : function(evt,tmpl) {
	if (evt.which === 27) {
	  evt.stopImmediatePropagation();
	  tmpl.selected.set(false);
	}
  },
  'paste .wysiwyg' : function(evt) {
	// To remove all html on paste, use this:
	// document.execCommand('insertText', false, evt.clipboardData.getData('text/plain'));
	// evt.preventDefault();
	// To remove all but selected elements and attributes, use this:
	Meteor.defer(function() {
	  $(evt.target).children().each(function() {
		EditableText.cleanHTML(this,EditableText.allowedHTMLElements);
	  });
	});
	// To accept all, do nothing
  },
  'click .wysiwyg-container .wysiwyg-save-button, focusout .wysiwyg-container .wysiwyg' : function(evt,tmpl) {
	evt.stopPropagation();
	evt.stopImmediatePropagation();
	if (evt.type === 'focusout' && EditableText._blockFocusoutForWYSIWYG) { // Need to stop clicks on the toolbar from firing the autosave due to loss of focus
	  return;
	}
	var self = (evt.type === 'focusout') ? this : tmpl.data;
	var value = EditableText.sanitizeHTML($.trim(tmpl.$('.wysiwyg').cleanHtml()).replace(/\n/g,""));
	okCancelEvents.ok.call(self,value,evt,tmpl);
	if (evt.type === 'click') {
	  tmpl.selected.set(false);
	}
  }
});

Template.editable_text_widget.created = function() {
  this.selected = new ReactiveVar();
}

Template.editable_text_widget.rendered = function() {
  // When we start editing, get the wysiwyg and attach its toolbar
  var self = this;
  this.autorun(function() {
	if (self.selected.get() && self.data.wysiwyg) {
	  Meteor.defer(function() {
        var wysiwyg = self.$('.wysiwyg');
	    wysiwyg.wysiwyg({toolbarSelector:".wysiwyg-toolbar"});
		Meteor.setTimeout(function() {
		  wysiwyg.focusEnd();
		},100);
	  });
	}
  });
}<commit_msg>Bug fix<commit_after>EditableText = {};

EditableText.userCanEdit = function() {
  return (typeof this.userCanEdit !== 'undefined') ? this.userCanEdit : true;
}

EditableText.useTransactions = false;

EditableText.insert = function(Collection,doc) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('add ' + this.objectTypeText);
	}
	var new_id = tx.insert(Collection,doc,{instant:true});
	if (this.onAutoInsert && _.isString(this.onAutoInsert)) {
	  EditableText._executeCallback(this.onAutoInsert,this,[Collection.findOne({_id:new_id})]);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	var new_id = Collection.insert(doc);
	if (this.onAutoInsert && _.isString(this.onAutoInsert)) {
	  EditableText._executeCallback(this.onAutoInsert,this,[Collection.findOne({_id:new_id})]);
	}
  }
  return new_id;
}

EditableText.update = function(Collection,doc,modifier) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('update ' + this.objectTypeText);
	}
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
	// Important to send the id only, not the whole document ("self"),
	// as this update is fired from all sorts of contexts, some of which are incomplete --
	// this will force the transaction script to find the relevant document with its full context from the database
	tx.update(Collection,doc._id,modifier,{instant:true});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
    Collection.update({_id:doc._id},modifier);
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
  }
}

EditableText.remove = function(Collection,doc) {
  if (typeof tx !== 'undefined' && ((EditableText.useTransactions && this.useTransaction !== false) || this.useTransaction)) {
	if (this.objectTypeText) {
	  tx.start('remove ' + this.objectTypeText);
	}
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
	tx.remove(Collection,doc._id,{instant:true});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
	if (this.objectTypeText) {
	  tx.commit();
	}
  }
  else {
	if (this.executeBefore && _.isString(this.executeBefore)) {
	  EditableText._executeCallback(this.executeBefore,this);
	}
    Collection.remove({_id:doc._id});
	if (this.executeAfter && _.isString(this.executeAfter)) {
	  EditableText._executeCallback(this.executeAfter,this);
	}
  }
}

EditableText._executeCallback = function(callback,thisVal,args) {
  if (callback && _.isString(callback)) {
	var callback = EditableText._drillDown(window,callback);
	if (callback && _.isFunction(callback)) {
	  callback.apply(thisVal,args);
	}
  }
}

EditableText._drillDown = function(obj,key) {
  var pieces = key.split('.');
  if (pieces.length > 1) {
	var newObj = obj ? obj[pieces[0]] : {};
	pieces.shift();
	return EditableText._drillDown(newObj,pieces.join('.'));
  }
  else {
	if (obj) {
	  return obj[key];
	}
	else {
	  return; // undefined
	}
  }
}

EditableText._blockFocusoutForWYSIWYG = false;

EditableText.blockOkayEvent = false;

Template.editableText.helpers({
  context : function() {
	return this.context || this.document || this.doc || this.object || this.obj || this.data || this.dataContext || Blaze._parentData(1);
  }
});

// Returns an event map that handles the "escape" and "return" keys and
// "blur" events on a text input (given by selector) and interprets them
// as "ok" or "cancel".

EditableText.okCancelEvents = function (selector, callbacks) {
  var ok = callbacks.ok || function () {};
  var cancel = callbacks.cancel || function () {};
  var events = {};
  events['keydown '+selector+', focusout '+selector] =
	function (evt,tmpl) {
	  evt.stopImmediatePropagation();
	  var charCode = evt.which || evt.keyCode;
	  if (evt.type === "keydown") {
		switch (charCode) {
		  case 27 : // escape = cancel
			cancel.call(this, evt, tmpl);
			break;
		}
	  }
	  if (evt.type === "keydown" && (charCode === 13 && !evt.shiftKey) || evt.type === "focusout") {
		evt.preventDefault();
		// blur/return/enter = ok/submit if non-empty
		var value = $.trim(String($(evt.target).val() || "")); // evt.target.value
		if (value || this.removeEmpty || this.acceptEmpty) {
		  if (!EditableText.blockOkayEvent) {
			EditableText.blockOkayEvent = true;
			ok.call(this, value, evt, tmpl);
			Meteor.defer(function() {
			  EditableText.blockOkayEvent = false;
			});
		  }
		  else { // In case this value is set to true for some strange reason
			EditableText.blockOkayEvent = false;
		  }
		}
		else {
		  cancel.call(this, evt, tmpl);
		}
	  }
	};
  return events;
};

EditableText.activateInput = function (input,dontSelect) {
  if (input !== null) {
	input.focus();
	if (typeof dontSelect !== 'undefined' && dontSelect) {
	  return;
	}
	input.select();
  }
  else {
	console.log('Input was null');
  }
};

EditableText.sanitizeHTML = function(html) {
  // Find all instances of href=" and if the first four letters aren't "http", put in "http://"
  var currentIndex = 0,newIndex;
  do {
	newIndex = html.substr(currentIndex).indexOf('href="');
	if (newIndex !== -1) {
	  // Check whether the next four characters are http
	  currentIndex += newIndex;
	  if (html.substr(currentIndex+6,4) !== 'http') {
		currentIndex += 6;
	    html = [html.slice(0,currentIndex), 'http://', html.slice(currentIndex)].join('');
	  }
	  else {
		currentIndex += 4;
	  }
	}
  }
  while (newIndex !== -1);
  return html;
}

EditableText.extractNumber = function(raw) {
  if (typeof raw !== 'undefined') {
	var numbers = raw.match(/[0-9]+/);
	if (numbers) {
      return $.trim(parseInt(numbers[0], 10));
	}
  }
  return "";
}

/*EditableText.firstToLower = function (text) {
  return (text) ? text.charAt(0).toLowerCase() + text.slice(1) : "";
};*/

Blaze.body.events({
  'click .editable-text-trigger, mousedown .editable-text-trigger, dblclick .editable-text-trigger' : function(evt) {
	$(evt.currentTarget).find('.editable-text').trigger(evt.type);
  }
});

Template.editable_text_widget.helpers({
  value : function() {
	return this.value || this.context[this.field];
  },
  editing : function() {
	return Blaze._templateInstance().selected.get();
  },
  isWysiwyg : function() {
	return this.wysiwyg;
  },
  wysiwygContent : function() {
	var value = this.value || this.context[this.field];
	return value && new Spacebars.SafeString(value.replace(/\n/g,'<br />')) || "";
  },
  inputValue : function() {
	var value = this.value || this.context[this.field];
	return value && value.toString().replace(/"/g,""") || "";
  },
  substitute : function() {
	return !(this.value || this.context[this.field]) && (this.substitute && Spacebars.SafeString(this.substitute.toString()));
  },
  title : function() {
	return this.title || ((this.eventType === 'dblclick') ? 'double click' : 'click') + ' to edit';
  },
  canEditText : function() {
	return EditableText.userCanEdit.call(this);
  },
  content : function() {
	var value = this.value || this.context[this.field];
	var val = (_.isString(value)) ? new Spacebars.SafeString(value) : ((value || value === 0) ? value.toString() : "");
	return val;
  }
});

var okCancelEvents = {};

okCancelEvents.ok = function (value,evt,tmpl) {
  evt.stopImmediatePropagation();
  evt.stopPropagation();
  var currentValue = this.value || this.context[this.field], newValue;
  var type = this.type || 'string';
  switch (type) {
	case 'number' :
	  var rawNewValue = EditableText.extractNumber(value);
	  newValue = rawNewValue && parseInt(rawNewValue) || 0;
	  currentValue = currentValue && parseInt(currentValue) || 0;
	  break;
	default :
	  newValue = $.trim(value).replace(/mml:/g,"");
	  currentValue = currentValue || '';
	  break;
  }
  var updatedValue = {};
  updatedValue[this.field] = newValue;
  // Sometimes we don't want the actual field's value, we want whatever was passed in to the template as the 'value' keyword to be used to compare against
  if (newValue !== currentValue || (!newValue && this.removeEmpty)) {
	if (!newValue && this.removeEmpty) {
	  EditableText.remove.call(this,Mongo.Collection.get(this.collection),this.context);
	}
	else {
	  // Make the update
	  EditableText.update.call(this,Mongo.Collection.get(this.collection),this.context,{$set: updatedValue});
	}
  }
  tmpl.selected.set(false);
}

okCancelEvents.cancel = function (evt,tmpl) {
  evt.stopImmediatePropagation();
  tmpl.selected.set(false);
}

Template.editable_text_widget.events(EditableText.okCancelEvents('.wide-text-edit', okCancelEvents));
Template.editable_text_widget.events(EditableText.okCancelEvents('.text-area-edit', okCancelEvents));
Template.editable_text_widget.events({
  'mousedown .wysiwyg-content .editable-text a' : function(evt) {
	if (confirm("Click 'OK' to follow this link.\n\nClick 'Cancel' to edit.")) {
	  evt.stopPropagation();
	  window.open($(evt.target).attr('href'),'_blank');
	}
  },
  'mousedown .editable-text, click .editable-text, dblclick .editable-text' : function(evt,tmpl) {
	evt.stopPropagation();
	// This is the click event that opens the field for editing
	var collection = this.collection;
	var execute = this.execute;
	var eventType = this.eventType || ((this.wysiwyg) ? 'mousedown' : 'click');
	if (eventType !== evt.type) {
	  return;
	}
	var acceptEmpty = this.acceptEmpty;
	var textarea = this.textarea;
	var wysiwyg = this.wysiwyg;
	var removeEmpty = this.removeEmpty;
	var type = this.type || 'string';
	var objectTypeText = this.objectTypeText || '';
	var collectionKey = collection;
	var obj = this.context;
	if (this.autoInsert && obj && !obj._id && _.isObject(obj) && EditableText.userCanEdit.call(this)) { // This is quite involved -- you need an object with all context info, but no _id field for auto-creation to occur
	  if (typeof this.value !== 'undefined' && this.value !== obj[this.field]) {
		obj[this.field] = this.value;
	  }
	  // Create an object
	  var object_id = EditableText.insert.call(this,Mongo.Collection.get(collectionKey),obj);
	  Tracker.flush();
	  // TODO -- this new object needs to get selected (and the focus), wherever it lands in the DOM
	  // However, we have no easy way of getting its template instance, other than iterating over
	  // all template instances, looking for the one where the context._id matches the object_id value
	  // Unfortunately, I have no idea where the template instances are stored
	  // So using a filthy hack instead ...
	  // Getting the element(s) in the DOM with the right class and firing click event
	  $('.et-' + object_id).trigger((tmpl.data.wysiwyg) ? 'mousedown' : 'click');
	  return;
	}
	if (obj && !tmpl.selected.get()) {
	  if (EditableText.userCanEdit.call(this)) {
		document.activeElement.blur(); // Make sure the focusout event fires first when switching editable text objects, so that the first one gets saved properly
		tmpl.selected.set(true);
		Tracker.flush();
		if (!wysiwyg) {
		  EditableText.activateInput(tmpl.$((textarea) ? 'textarea' :  'input'),this.textarea);
		}
	  }
	}
  },
  'mousedown .wysiwyg-container' : function(evt) {
	EditableText._blockFocusoutForWYSIWYG = true;
	Meteor.defer(function() {
	  EditableText._blockFocusoutForWYSIWYG = false;
	});
  },
  'keydown .wysiwyg-container' : function(evt,tmpl) {
	if (evt.which === 27) {
	  evt.stopImmediatePropagation();
	  tmpl.selected.set(false);
	}
  },
  'paste .wysiwyg' : function(evt) {
	// To remove all html on paste, use this:
	// document.execCommand('insertText', false, evt.clipboardData.getData('text/plain'));
	// evt.preventDefault();
	// To remove all but selected elements and attributes, use this:
	Meteor.defer(function() {
	  $(evt.target).children().each(function() {
		EditableText.cleanHTML(this,EditableText.allowedHTMLElements);
	  });
	});
	// To accept all, do nothing
  },
  'click .wysiwyg-container .wysiwyg-save-button, focusout .wysiwyg-container .wysiwyg' : function(evt,tmpl) {
	evt.stopPropagation();
	evt.stopImmediatePropagation();
	if (evt.type === 'focusout' && EditableText._blockFocusoutForWYSIWYG) { // Need to stop clicks on the toolbar from firing the autosave due to loss of focus
	  return;
	}
	var self = (evt.type === 'focusout') ? this : tmpl.data;
	var value = EditableText.sanitizeHTML($.trim(tmpl.$('.wysiwyg').cleanHtml()).replace(/\n/g,""));
	okCancelEvents.ok.call(self,value,evt,tmpl);
	if (evt.type === 'click') {
	  tmpl.selected.set(false);
	}
  }
});

Template.editable_text_widget.created = function() {
  this.selected = new ReactiveVar();
}

Template.editable_text_widget.rendered = function() {
  // When we start editing, get the wysiwyg and attach its toolbar
  var self = this;
  this.autorun(function() {
	if (self.selected.get() && self.data.wysiwyg) {
	  Meteor.defer(function() {
        var wysiwyg = self.$('.wysiwyg');
	    wysiwyg.wysiwyg({toolbarSelector:".wysiwyg-toolbar"});
		Meteor.setTimeout(function() {
		  wysiwyg.focusEnd();
		},100);
	  });
	}
  });
}<|endoftext|>


<commit_before>'use babel'

/* @flow */

import { Emitter, CompositeDisposable } from 'atom'
import debounce from 'sb-debounce'
import type { Disposable, TextEditor } from 'atom'

export default class EditorLinter {
  editor: TextEditor;
  emitter: Emitter;
  subscriptions: CompositeDisposable;
  changeSubscription: Disposable;

  constructor(editor: TextEditor) {
    if (typeof editor !== 'object' || typeof editor.getText !== 'function') {
      throw new Error('Given editor is not really an editor')
    }

    this.editor = editor
    this.emitter = new Emitter()
    this.subscriptions = new CompositeDisposable

    this.subscriptions.add(this.editor.onDidDestroy(() =>
      this.dispose()
    ))
    this.subscriptions.add(this.editor.onDidSave(() =>
      this.emitter.emit('should-lint', false)
    ))

    // TODO: Atom invokes onDid{Change, StopChanging} callbacks immediately. Workaround it
    atom.config.observe('linter.lintOnFlyInterval', (interval) => {
      if (this.changeSubscription) {
        this.changeSubscription.dispose()
      }
      this.changeSubscription = this.editor.onDidChange(debounce(() => {
        this.emitter.emit('should-lint', true)
      }, interval))
    })
  }
  lint(onChange: boolean = false) {
    this.emitter.emit('should-lint', onChange)
  }
  onShouldLint(callback: Function): Disposable {
    return this.emitter.on('should-lint', callback)
  }
  onDidDestroy(callback: Function): Disposable {
    return this.emitter.on('did-destroy', callback)
  }
  dispose() {
    this.emitter.emit('did-destroy')
    this.subscriptions.dispose()
    if (this.changeSubscription) {
      this.changeSubscription.dispose()
    }
    this.emitter.dispose()
  }
}
<commit_msg>:bug: Debounce lint triggering on save Debouncing puts us at the end of the save queue, which means we are not triggered after all the beautify packages Reference: steelbrain/linter#1009<commit_after>'use babel'

/* @flow */

import { Emitter, CompositeDisposable } from 'atom'
import debounce from 'sb-debounce'
import type { Disposable, TextEditor } from 'atom'

export default class EditorLinter {
  editor: TextEditor;
  emitter: Emitter;
  subscriptions: CompositeDisposable;
  changeSubscription: Disposable;

  constructor(editor: TextEditor) {
    if (typeof editor !== 'object' || typeof editor.getText !== 'function') {
      throw new Error('Given editor is not really an editor')
    }

    this.editor = editor
    this.emitter = new Emitter()
    this.subscriptions = new CompositeDisposable

    this.subscriptions.add(this.editor.onDidDestroy(() =>
      this.dispose()
    ))
    this.subscriptions.add(this.editor.onDidSave(debounce(() =>
      this.emitter.emit('should-lint', false)
    ), 16, true))

    // TODO: Atom invokes onDid{Change, StopChanging} callbacks immediately. Workaround it
    atom.config.observe('linter.lintOnFlyInterval', (interval) => {
      if (this.changeSubscription) {
        this.changeSubscription.dispose()
      }
      this.changeSubscription = this.editor.onDidChange(debounce(() => {
        this.emitter.emit('should-lint', true)
      }, interval))
    })
  }
  lint(onChange: boolean = false) {
    this.emitter.emit('should-lint', onChange)
  }
  onShouldLint(callback: Function): Disposable {
    return this.emitter.on('should-lint', callback)
  }
  onDidDestroy(callback: Function): Disposable {
    return this.emitter.on('did-destroy', callback)
  }
  dispose() {
    this.emitter.emit('did-destroy')
    this.subscriptions.dispose()
    if (this.changeSubscription) {
      this.changeSubscription.dispose()
    }
    this.emitter.dispose()
  }
}
<|endoftext|>


<commit_before>/*
 * gaze
 * https://github.com/shama/gaze
 *
 * Copyright (c) 2014 Kyle Robinson Young
 * Licensed under the MIT license.
 */

'use strict';

try { var PathWatcher = require('./pathwatcher'); } catch (err) { }
var statpoll = require('./statpoll.js');
var helper = require('./helper');
var fs = require('fs');
var path = require('path');

// on purpose globals
var watched = Object.create(null);
var renameWaiting = null;
var renameWaitingFile = null;

var platform = module.exports = function(file, cb) {
  // Ignore non-existent files
  if (!fs.existsSync(file)) return;

  // Mark every folder
  file = markDir(file);

  // Also watch all folders, needed to catch change for detecting added files
  if (!helper.isDir(file)) {
    platform(path.dirname(file), cb);
  }

  // Already watched, move on
  if (watched[file]) return false;

  // Helper for when to use statpoll
  function useStatPoll() {
    statpoll(file, function(event) {
      var filepath = file;
      if (process.platform === 'linux') {
        var go = linuxWorkarounds(event, filepath, cb);
        if (go === false) { return; }
      }
      cb(null, event, filepath);
    });
  }

  // if we haven't emfiled or in watch mode, use faster watch
  if ((platform.mode === 'auto' && emfiled === false) || platform.mode === 'watch') {
    // Delay adding files to watch
    // Fixes the duplicate handle race condition when renaming files
    // ie: (The handle(26) returned by watching [filename] is the same with an already watched path([filename]))
    watched[file] = true;
    setTimeout(function() {
      if (!fs.existsSync(file)) {
        delete watched[file];
        return;
      }
      // Workaround for lack of rename support on linux
      if (process.platform === 'linux' && renameWaiting) {
        clearTimeout(renameWaiting);
        cb(null, 'rename', renameWaitingFile, file);
        renameWaiting = renameWaitingFile = null;
        return;
      }
      try {
        watched[file] = PathWatcher.watch(file, function(event, newFile) {
          var filepath = file;
          if (process.platform === 'linux') {
            var go = linuxWorkarounds(event, filepath, cb);
            if (go === false) { return; }
          }
          cb(null, event, filepath, newFile);
        });
      } catch (error) {
        // If we hit EMFILE, use stat poll
        if (error.code === 'EMFILE' || error.message.slice(0, 6) === 'EMFILE') {
          useStatPoll();
        }
        cb(error);
      }
    }, 10);
  } else {
    useStatPoll();
  }
};

platform.mode = 'auto';

// Run the stat poller
// NOTE: Run at minimum of 500ms to adequately capture change event
// to folders when adding files
platform.tick = statpoll.tick.bind(statpoll);

// Close up a single watcher
platform.close = function(filepath, cb) {
  if (watched[filepath]) {
    try {
      watched[filepath].close();
      delete watched[filepath];
      // Always be hopeful
      emfiled = false;
    } catch (error) {
      return cb(error);
    }
  } else {
    statpoll.close(filepath);
  }
  if (typeof cb === 'function') {
    cb(null);
  }
};

// Close up all watchers
platform.closeAll = function() {
  watched = Object.create(null);
  emfiled = false;
  statpoll.closeAll();
  PathWatcher.closeAllWatchers();
};

// Return all watched file paths
platform.getWatchedPaths = function() {
  return Object.keys(watched).concat(statpoll.getWatchedPaths());
};

// Mark folders if not marked
function markDir(file) {
  if (file.slice(-1) !== path.sep) {
    if (fs.lstatSync(file).isDirectory()) {
      file += path.sep;
    }
  }
  return file;
}

// Workarounds for lack of rename support on linux and folders emit before files
// https://github.com/atom/node-pathwatcher/commit/004a202dea89f4303cdef33912902ed5caf67b23
var linuxQueue = Object.create(null);
var linuxQueueInterval = null;
function linuxProcessQueue(cb) {
  var len = Object.keys(linuxQueue).length;
  if (len === 1) {
    var key = Object.keys(linuxQueue).slice(0, 1)[0];
    cb(null, key, linuxQueue[key]);
  } else if (len > 1) {
    if (linuxQueue['delete'] && linuxQueue['change']) {
      renameWaitingFile = linuxQueue['delete'];
      renameWaiting = setTimeout(function(filepath) {
        cb(null, 'delete', filepath);
        renameWaiting = renameWaitingFile = null;
      }, 100, linuxQueue['delete']);
      cb(null, 'change', linuxQueue['change']);
    } else {
      // TODO: This might not be needed
      for (var i in linuxQueue) {
        if (linuxQueue.hasOwnProperty(i)) {
          cb(null, i, linuxQueue[i]);
        }
      }
    }
  }
  linuxQueue = Object.create(null);
}
function linuxWorkarounds(event, filepath, cb) {
  linuxQueue[event] = filepath;
  clearTimeout(linuxQueueInterval);
  linuxQueueInterval = setTimeout(function() {
    linuxProcessQueue(cb);
  }, 100);
  return false;
}
<commit_msg>Remove last remnants of emfiled<commit_after>/*
 * gaze
 * https://github.com/shama/gaze
 *
 * Copyright (c) 2014 Kyle Robinson Young
 * Licensed under the MIT license.
 */

'use strict';

try { var PathWatcher = require('./pathwatcher'); } catch (err) { }
var statpoll = require('./statpoll.js');
var helper = require('./helper');
var fs = require('fs');
var path = require('path');

// on purpose globals
var watched = Object.create(null);
var renameWaiting = null;
var renameWaitingFile = null;

var platform = module.exports = function(file, cb) {
  // Ignore non-existent files
  if (!fs.existsSync(file)) return;

  // Mark every folder
  file = markDir(file);

  // Also watch all folders, needed to catch change for detecting added files
  if (!helper.isDir(file)) {
    platform(path.dirname(file), cb);
  }

  // Already watched, move on
  if (watched[file]) return false;

  // Helper for when to use statpoll
  function useStatPoll() {
    statpoll(file, function(event) {
      var filepath = file;
      if (process.platform === 'linux') {
        var go = linuxWorkarounds(event, filepath, cb);
        if (go === false) { return; }
      }
      cb(null, event, filepath);
    });
  }

  // By default try using native OS watchers
  if (platform.mode === 'auto' || platform.mode === 'watch') {
    // Delay adding files to watch
    // Fixes the duplicate handle race condition when renaming files
    // ie: (The handle(26) returned by watching [filename] is the same with an already watched path([filename]))
    watched[file] = true;
    setTimeout(function() {
      if (!fs.existsSync(file)) {
        delete watched[file];
        return;
      }
      // Workaround for lack of rename support on linux
      if (process.platform === 'linux' && renameWaiting) {
        clearTimeout(renameWaiting);
        cb(null, 'rename', renameWaitingFile, file);
        renameWaiting = renameWaitingFile = null;
        return;
      }
      try {
        watched[file] = PathWatcher.watch(file, function(event, newFile) {
          var filepath = file;
          if (process.platform === 'linux') {
            var go = linuxWorkarounds(event, filepath, cb);
            if (go === false) { return; }
          }
          cb(null, event, filepath, newFile);
        });
      } catch (error) {
        // If we hit EMFILE, use stat poll
        if (error.code === 'EMFILE' || error.message.slice(0, 6) === 'EMFILE') {
          useStatPoll();
        }
        cb(error);
      }
    }, 10);
  } else {
    useStatPoll();
  }
};

platform.mode = 'auto';

// Run the stat poller
// NOTE: Run at minimum of 500ms to adequately capture change event
// to folders when adding files
platform.tick = statpoll.tick.bind(statpoll);

// Close up a single watcher
platform.close = function(filepath, cb) {
  if (watched[filepath]) {
    try {
      watched[filepath].close();
      delete watched[filepath];
    } catch (error) {
      return cb(error);
    }
  } else {
    statpoll.close(filepath);
  }
  if (typeof cb === 'function') {
    cb(null);
  }
};

// Close up all watchers
platform.closeAll = function() {
  watched = Object.create(null);
  statpoll.closeAll();
  PathWatcher.closeAllWatchers();
};

// Return all watched file paths
platform.getWatchedPaths = function() {
  return Object.keys(watched).concat(statpoll.getWatchedPaths());
};

// Mark folders if not marked
function markDir(file) {
  if (file.slice(-1) !== path.sep) {
    if (fs.lstatSync(file).isDirectory()) {
      file += path.sep;
    }
  }
  return file;
}

// Workarounds for lack of rename support on linux and folders emit before files
// https://github.com/atom/node-pathwatcher/commit/004a202dea89f4303cdef33912902ed5caf67b23
var linuxQueue = Object.create(null);
var linuxQueueInterval = null;
function linuxProcessQueue(cb) {
  var len = Object.keys(linuxQueue).length;
  if (len === 1) {
    var key = Object.keys(linuxQueue).slice(0, 1)[0];
    cb(null, key, linuxQueue[key]);
  } else if (len > 1) {
    if (linuxQueue['delete'] && linuxQueue['change']) {
      renameWaitingFile = linuxQueue['delete'];
      renameWaiting = setTimeout(function(filepath) {
        cb(null, 'delete', filepath);
        renameWaiting = renameWaitingFile = null;
      }, 100, linuxQueue['delete']);
      cb(null, 'change', linuxQueue['change']);
    } else {
      // TODO: This might not be needed
      for (var i in linuxQueue) {
        if (linuxQueue.hasOwnProperty(i)) {
          cb(null, i, linuxQueue[i]);
        }
      }
    }
  }
  linuxQueue = Object.create(null);
}
function linuxWorkarounds(event, filepath, cb) {
  linuxQueue[event] = filepath;
  clearTimeout(linuxQueueInterval);
  linuxQueueInterval = setTimeout(function() {
    linuxProcessQueue(cb);
  }, 100);
  return false;
}
<|endoftext|>


<commit_before>'use babel'

import {TextEditor, Emitter, CompositeDisposable} from 'atom'

export default class EditorLinter {
  constructor(editor) {
    if (!(editor instanceof TextEditor)) {
      throw new Error('Given editor is not really an editor')
    }

    this.editor = editor
    this.emitter = new Emitter()
    this.markers = new Map()
    this.gutter = null
    this.subscriptions = new CompositeDisposable

    this.subscriptions.add(atom.config.observe('linter.underlineIssues', underlineIssues =>
      this.underlineIssues = underlineIssues
    ))
    this.subscriptions.add(this.editor.onDidDestroy(() =>
      this.destroy()
    ))
    this.subscriptions.add(this.editor.onDidSave(() =>
      this.emitter.emit('should-lint', false)
    ))
    this.subscriptions.add(this.editor.onDidChangeCursorPosition(({oldBufferPosition, newBufferPosition}) => {
      if (newBufferPosition.row !== oldBufferPosition.row) {
        this.emitter.emit('should-update-line-messages')
      }
      this.emitter.emit('should-update-bubble')
    }))
    this.subscriptions.add(atom.config.observe('linter.gutterEnabled', () =>
      this.handleGutter()
    ))
    // Using onDidChange here 'cause the same function is invoked above
    this.subscriptions.add(atom.config.onDidChange('linter.gutterPosition', () =>
      this.handleGutter()
    ))

    // Atom invokes the onDidStopChanging callback immediately on creation. So we wait a moment
    setImmediate(() => {
      this.subscriptions.add(this.editor.onDidStopChanging(() =>
        this.emitter.emit('should-lint', true)
      ))
    })
  }

  handleGutter() {
    const status = atom.config.get('linter.gutterEnabled')

    if (this.gutter !== null) this.removeGutter()
    if (status) this.addGutter()
  }

  addGutter() {
    // TODO: Add existing messages
    // TODO: Make the position work
    const position = atom.config.get('linter.gutterPosition')
    this.gutter = this.editor.addGutter({
      name: 'linter',
      priority: 100
    })
  }

  removeGutter() {
    if (this.gutter !== null) {
      this.gutter.destroy()
      this.gutter = null
    }
  }

  addMessage(message) {
    const marker = this.editor.markBufferRange(message.range, {invalidate: 'inside'})
    this.markers.set(message, marker)
    this.editor.decorateMarker(marker, {
      type: 'line-number',
      class: `linter-highlight ${message.class}`
    })
    if (this.underlineIssues) {
      this.editor.decorateMarker(marker, {
        type: 'highlight',
        class: `linter-highlight ${message.class}`
      })
    }
  }

  removeMessage(message) {
    if (this.markers.has(message)) {
      const marker = this.markers.get(message)
      marker.destroy()
      this.markers.delete(marker)
    }
  }

  lint(onChange = false) {
    this.emitter.emit('should-lint', onChange)
  }

  onShouldUpdateBubble(callback) {
    return this.emitter.on('should-update-bubble', callback)
  }

  onShouldUpdateLineMessages(callback) {
    return this.emitter.on('should-update-line-messages', callback)
  }

  onShouldLint(callback) {
    return this.emitter.on('should-lint', callback)
  }

  onDidDestroy(callback) {
    return this.emitter.on('did-destroy', callback)
  }

  destroy() {
    this.emitter.emit('did-destroy')
    this.dispose()
  }

  dispose() {
    if (this.markers.size) {
      this.markers.forEach(marker => marker.destroy())
    }
    this.removeGutter()
    this.subscriptions.dispose()
  }
}
<commit_msg>:new: Make the position config work<commit_after>'use babel'

import {TextEditor, Emitter, CompositeDisposable} from 'atom'

export default class EditorLinter {
  constructor(editor) {
    if (!(editor instanceof TextEditor)) {
      throw new Error('Given editor is not really an editor')
    }

    this.editor = editor
    this.emitter = new Emitter()
    this.markers = new Map()
    this.gutter = null
    this.subscriptions = new CompositeDisposable

    this.subscriptions.add(atom.config.observe('linter.underlineIssues', underlineIssues =>
      this.underlineIssues = underlineIssues
    ))
    this.subscriptions.add(this.editor.onDidDestroy(() =>
      this.destroy()
    ))
    this.subscriptions.add(this.editor.onDidSave(() =>
      this.emitter.emit('should-lint', false)
    ))
    this.subscriptions.add(this.editor.onDidChangeCursorPosition(({oldBufferPosition, newBufferPosition}) => {
      if (newBufferPosition.row !== oldBufferPosition.row) {
        this.emitter.emit('should-update-line-messages')
      }
      this.emitter.emit('should-update-bubble')
    }))
    this.subscriptions.add(atom.config.observe('linter.gutterEnabled', () =>
      this.handleGutter()
    ))
    // Using onDidChange here 'cause the same function is invoked above
    this.subscriptions.add(atom.config.onDidChange('linter.gutterPosition', () =>
      this.handleGutter()
    ))

    // Atom invokes the onDidStopChanging callback immediately on creation. So we wait a moment
    setImmediate(() => {
      this.subscriptions.add(this.editor.onDidStopChanging(() =>
        this.emitter.emit('should-lint', true)
      ))
    })
  }

  handleGutter() {
    const status = atom.config.get('linter.gutterEnabled')

    if (this.gutter !== null) this.removeGutter()
    if (status) this.addGutter()
  }

  addGutter() {
    // TODO: Add existing messages
    const position = atom.config.get('linter.gutterPosition')
    this.gutter = this.editor.addGutter({
      name: 'linter',
      priority: position === 'Left' ? -100 : 100
    })
  }

  removeGutter() {
    if (this.gutter !== null) {
      this.gutter.destroy()
      this.gutter = null
    }
  }

  addMessage(message) {
    const marker = this.editor.markBufferRange(message.range, {invalidate: 'inside'})
    this.markers.set(message, marker)
    this.editor.decorateMarker(marker, {
      type: 'line-number',
      class: `linter-highlight ${message.class}`
    })
    if (this.underlineIssues) {
      this.editor.decorateMarker(marker, {
        type: 'highlight',
        class: `linter-highlight ${message.class}`
      })
    }
  }

  removeMessage(message) {
    if (this.markers.has(message)) {
      const marker = this.markers.get(message)
      marker.destroy()
      this.markers.delete(marker)
    }
  }

  lint(onChange = false) {
    this.emitter.emit('should-lint', onChange)
  }

  onShouldUpdateBubble(callback) {
    return this.emitter.on('should-update-bubble', callback)
  }

  onShouldUpdateLineMessages(callback) {
    return this.emitter.on('should-update-line-messages', callback)
  }

  onShouldLint(callback) {
    return this.emitter.on('should-lint', callback)
  }

  onDidDestroy(callback) {
    return this.emitter.on('did-destroy', callback)
  }

  destroy() {
    this.emitter.emit('did-destroy')
    this.dispose()
  }

  dispose() {
    if (this.markers.size) {
      this.markers.forEach(marker => marker.destroy())
    }
    this.removeGutter()
    this.subscriptions.dispose()
  }
}
<|endoftext|>