<commit_before>/*
 * moleculer
 * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer)
 * MIT Licensed
 */

"use strict";

const fs = require("fs");
const kleur = require("kleur");
const path = require("path");
const watch = require("recursive-watch");
const _ = require("lodash");

const { clearRequireCache, makeDirs, isFunction, isString } = require("../utils");

/* istanbul ignore next */
module.exports = function HotReloadMiddleware(broker) {

	const cache = new Map();

	let projectFiles = new Map();
	let prevProjectFiles = new Map();
	let hotReloadModules = [];
	let extraFiles = null;

	function hotReloadService(service) {
		const relPath = path.relative(process.cwd(), service.__filename);

		broker.logger.info(`Hot reload '${service.name}' service...`, kleur.grey(relPath));

		return broker.destroyService(service)
			.then(() => {
				if (fs.existsSync(service.__filename)) {
					return broker.loadService(service.__filename);
				}
			});

	}

	/**
	 * Detect service dependency graph & watch all dependent files & services.
	 *
	 */
	function watchProjectFiles() {

		if (!broker.started || !process.mainModule) return;

		cache.clear();
		prevProjectFiles = projectFiles;
		projectFiles = new Map();

		// Read the main module
		const mainModule = process.mainModule || require.main;

		// Process the whole module tree
		processModule(mainModule, null, 0, null);

		if (extraFiles != null) {
			Object.entries(extraFiles).forEach(([fName, restartType]) => {
				const watchItem = getWatchItem(fName);
				if (restartType == "broker")
					watchItem.brokerRestart = true;
				else if (restartType == "allServices")
					watchItem.allServices = true;
				else if (isString(restartType))
					watchItem.services.push(restartType);
				else if (Array.isArray(restartType))
					watchItem.services.push(...restartType);
			});
		}

		const needToReload = new Set();

		// Debounced Service reloader function
		const reloadServices = _.debounce(() => {
			needToReload.forEach(svc => {
				if (svc.__filename && !fs.existsSync(svc.__filename))
					needToReload.delete(svc);
			});
			broker.logger.info(kleur.bgMagenta().white().bold(`Reload ${needToReload.size} service(s)`));

			needToReload.forEach(svc => {
				if (typeof svc == "string")
					return broker.loadService(svc);

				return hotReloadService(svc);
			});
			needToReload.clear();

		}, 500);

		// Close previous watchers
		stopAllFileWatcher(prevProjectFiles);

		// Watching project files
		broker.logger.debug("");
		broker.logger.debug(kleur.yellow().bold("Watching the following project files:"));

		projectFiles.forEach((watchItem, fName) => {
			// Delete if file doesn't exist anymore
			if (!fs.existsSync(fName))
				projectFiles.delete(fName);
		});

		projectFiles.forEach((watchItem, fName) => {
			const relPath = path.relative(process.cwd(), fName);
			if (watchItem.brokerRestart)
				broker.logger.debug(`  ${relPath}:`, kleur.grey("restart broker."));
			else if (watchItem.allServices)
				broker.logger.debug(`  ${relPath}:`, kleur.grey("reload all services."));
			else if (watchItem.services.length > 0) {
				broker.logger.debug(`  ${relPath}:`, kleur.grey(`reload ${watchItem.services.length} service(s) & ${watchItem.others.length} other(s).`)/*, watchItem.services, watchItem.others*/);
				watchItem.services.forEach(svcFullname => broker.logger.debug(kleur.grey(`    ${svcFullname}`)));
				watchItem.others.forEach(filename => broker.logger.debug(kleur.grey(`    ${path.relative(process.cwd(), filename)}`)));
			}
			// Create watcher
			watchItem.watcher = fs.watch(fName, (eventType) => {
				const relPath = path.relative(process.cwd(), fName);
				broker.logger.info(kleur.magenta().bold(`The '${relPath}' file is changed. (Event: ${eventType})`));

				// Clear from require cache
				clearRequireCache(fName);
				if (watchItem.others.length > 0) {
					watchItem.others.forEach(f => clearRequireCache(f));
				}

				if (watchItem.brokerRestart && broker.runner && isFunction(broker.runner.restartBroker)) {
					broker.logger.info(kleur.bgMagenta().white().bold("Action: Restart broker..."));
					stopAllFileWatcher(projectFiles);
					// Clear the whole require cache
					require.cache.length = 0;
					return broker.runner.restartBroker();

				} else if (watchItem.allServices) {
					// Reload all services
					broker.services.forEach(svc => {
						if (svc.__filename)
							needToReload.add(svc);
					});
					reloadServices();

				} else if (watchItem.services.length > 0) {
					// Reload certain services
					broker.services.forEach(svc => {
						if (watchItem.services.indexOf(svc.fullName) !== -1)
							needToReload.add(svc);
					});

					if (needToReload.size == 0) {
						// It means, it's a crashed reloaded service, so we
						// didn't find it in the loaded services because
						// the previous hot-reload failed. We should load it
						// broker.loadService
						needToReload.add(relPath);
					}
					reloadServices();
				}
			});
		});

		if (projectFiles.size == 0)
			broker.logger.debug(kleur.grey("  No files."));

	}

	const debouncedWatchProjectFiles = _.debounce(watchProjectFiles, 2000);

	/**
	 * Stop all file watchers
	 */
	function stopAllFileWatcher(items) {
		items.forEach((watchItem) => {
			if (watchItem.watcher) {
				watchItem.watcher.close();
				watchItem.watcher = null;
			}
		});
	}

	/**
	 * Get a watch item
	 *
	 * @param {String} fName
	 * @returns {Object}
	 */
	function getWatchItem(fName) {
		let watchItem = projectFiles.get(fName);
		if (watchItem)
			return watchItem;

		watchItem = {
			services: [],
			allServices: false,
			brokerRestart: false,
			others: []
		};
		projectFiles.set(fName, watchItem);

		return watchItem;
	}

	function isMoleculerConfig(fName) {
		return fName.endsWith("moleculer.config.js")
			|| fName.endsWith("moleculer.config.ts")
			|| fName.endsWith("moleculer.config.json");
	}

	/**
	 * Process module children modules.
	 *
	 * @param {*} mod
	 * @param {*} service
	 * @param {Number} level
	 */
	function processModule(mod, service = null, level = 0, parents = null) {
		const fName = mod.filename;

		// Skip node_modules files, if there is parent project file
		if ((service || parents) && fName.indexOf("node_modules") !== -1)
			if (hotReloadModules.find(modulePath => fName.indexOf(modulePath) !== -1) == null)
				return;

		// Avoid circular dependency in project files
		if (parents && parents.indexOf(fName) !== -1)
			return;

		// console.log(fName);

		// Cache files to avoid cyclic dependencies in node_modules
		if (fName.indexOf("node_modules") !== -1) {
			if (cache.get(fName)) return;
			cache.set(fName, mod);
		}

		if (!service) {
			service = broker.services.find(svc => svc.__filename == fName);
		}

		if (service) {
			// It is a service dependency. We should reload this service if this file has changed.
			const watchItem = getWatchItem(fName);
			if (!watchItem.services.includes(service.fullName))
				watchItem.services.push(service.fullName);

			watchItem.others = _.uniq([].concat(watchItem.others, parents || []));

		} else if (isMoleculerConfig(fName)) {
			const watchItem = getWatchItem(fName);
			watchItem.brokerRestart = true;
		} else {
			// It is not a service dependency, it is a global middleware. We should reload all services if this file has changed.
			if (parents) {
				const watchItem = getWatchItem(fName);
				watchItem.allServices = true;
				watchItem.others = _.uniq([].concat(watchItem.others, parents || []));
			}
		}

		if (mod.children && mod.children.length > 0) {
			if (service) {
				parents = parents ? parents.concat([fName]) : [fName];
			} else if (isMoleculerConfig(fName)) {
				parents = [];
				// const watchItem = getWatchItem(fName);
				// watchItem.brokerRestart = true;
			} else if (parents) {
				parents.push(fName);
			}
			mod.children.forEach(m => processModule(m, service, service ? level + 1 : 0, parents));
		}
	}

	const folderWatchers = [];

	function watchProjectFolders() {
		// Debounced Service loader function
		const needToLoad = new Set();
		const loadServices = _.debounce(() => {
			broker.logger.info(kleur.bgMagenta().white().bold(`Load ${needToLoad.size} service(s)...`));

			needToLoad.forEach(filename => {
				try {
					broker.loadService(filename);
				} catch(err) {
					broker.logger.error(`Failed to load service '${filename}'`, err);
					clearRequireCache(filename);
				}
			});
			needToLoad.clear();

		}, 500);

		if (broker.runner && Array.isArray(broker.runner.folders)) {
			const folders = broker.runner.folders;
			if (folders.length > 0) {
				folderWatchers.length = 0;

				broker.logger.debug("");
				broker.logger.debug(kleur.yellow().bold("Watching the following folder(s):"));

				folders.forEach(folder => {
					makeDirs(folder);
					broker.logger.debug(`  ${path.relative(process.cwd(), folder)}/`);
					folderWatchers.push({
						path: folder,
						watcher: watch(folder, (filename) => {
							if (filename.endsWith(".service.js") || filename.endsWith(".service.ts")) {
								broker.logger.debug(`There is changes in '${folder}' folder: `, path.basename(filename));
								const isLoaded = broker.services.some(svc => svc.__filename == filename);
								const fileExists = fs.existsSync(filename);
								if (!isLoaded && fileExists) {
									// This is a new file. We should wait for the file fully copied.
									needToLoad.add(filename);
									loadServices();
								}
							}
						})
					});
				});
			}
		}
	}

	function stopProjectFolderWatchers() {
		broker.logger.debug("");
		broker.logger.debug("Stop watching folders.");
		folderWatchers.forEach(item => item.watcher && item.watcher());
	}

	/**
	 * Expose middleware
	 */
	return {
		name: "HotReload",

		// After broker started
		started(broker) {
			if (broker.options.hotReload == null) {
				return;
			} else if (typeof broker.options.hotReload === "object") {
				if (Array.isArray(broker.options.hotReload.modules)) {
					hotReloadModules = broker.options.hotReload.modules.map(moduleName => `/node_modules/${moduleName}/`);
				}
				if (broker.options.hotReload.extraFiles) {
					/**
					 * **Example:**
					 * ```js
					 * hotReload: {
					 * 	extraFiles: {
					 * 		"./configuration.json": "broker", // reload the broker
					 * 		"./common.js": "allServices", // reload all services
					 * 		"./database": "v1.posts", // reload a service
					 * 		"./database": ["v1.posts", "users"], // reload multiple services
					 * 	}
					 * }
					 * ```
					 */
					extraFiles = broker.options.hotReload.extraFiles;
				}
			} else if (broker.options.hotReload !== true) {
				return;
			}

			watchProjectFiles();

			watchProjectFolders();
		},

		serviceStarted() {
			// Re-watch new services if broker has already started and a new service started.
			if (broker.started) {
				debouncedWatchProjectFiles();
			}
		},

		stopped() {
			stopProjectFolderWatchers();
		}
	};

};
<commit_msg>Fix hot-reload (allow removal of service on file deletion)<commit_after>/*
 * moleculer
 * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer)
 * MIT Licensed
 */

"use strict";

const fs = require("fs");
const kleur = require("kleur");
const path = require("path");
const watch = require("recursive-watch");
const _ = require("lodash");

const { clearRequireCache, makeDirs, isFunction, isString } = require("../utils");

/* istanbul ignore next */
module.exports = function HotReloadMiddleware(broker) {

	const cache = new Map();

	let projectFiles = new Map();
	let prevProjectFiles = new Map();
	let hotReloadModules = [];
	let extraFiles = null;

	function hotReloadService(service) {
		const relPath = path.relative(process.cwd(), service.__filename);

		broker.logger.info(`Hot reload '${service.name}' service...`, kleur.grey(relPath));

		return broker.destroyService(service)
			.then(() => {
				if (fs.existsSync(service.__filename)) {
					return broker.loadService(service.__filename);
				}
			});

	}

	/**
	 * Detect service dependency graph & watch all dependent files & services.
	 *
	 */
	function watchProjectFiles() {

		if (!broker.started || !process.mainModule) return;

		cache.clear();
		prevProjectFiles = projectFiles;
		projectFiles = new Map();

		// Read the main module
		const mainModule = process.mainModule || require.main;

		// Process the whole module tree
		processModule(mainModule, null, 0, null);

		if (extraFiles != null) {
			Object.entries(extraFiles).forEach(([fName, restartType]) => {
				const watchItem = getWatchItem(fName);
				if (restartType == "broker")
					watchItem.brokerRestart = true;
				else if (restartType == "allServices")
					watchItem.allServices = true;
				else if (isString(restartType))
					watchItem.services.push(restartType);
				else if (Array.isArray(restartType))
					watchItem.services.push(...restartType);
			});
		}

		const needToReload = new Set();

		// Debounced Service reloader function
		const reloadServices = _.debounce(() => {
			const needToReloadDedup = _.uniqWith([...needToReload], (a, b) => {
				const ac = typeof a == "string" ? a : a.__filename;
				const bc = typeof b == "string" ? b : b.__filename;
				return ac == bc;
			});

			broker.logger.info(kleur.bgMagenta().white().bold(`Reload ${needToReloadDedup.length} service(s)`));

			needToReloadDedup.forEach(svc => {
				if (typeof svc == "string")
					if (fs.existsSync(svc))
						return broker.loadService(svc);
					else
						return;

				return hotReloadService(svc);
			});
			needToReload.clear();

		}, 500);

		// Close previous watchers
		stopAllFileWatcher(prevProjectFiles);

		// Watching project files
		broker.logger.debug("");
		broker.logger.debug(kleur.yellow().bold("Watching the following project files:"));

		projectFiles.forEach((watchItem, fName) => {
			// Delete if file doesn't exist anymore
			if (!fs.existsSync(fName))
				projectFiles.delete(fName);
		});

		projectFiles.forEach((watchItem, fName) => {
			const relPath = path.relative(process.cwd(), fName);
			if (watchItem.brokerRestart)
				broker.logger.debug(`  ${relPath}:`, kleur.grey("restart broker."));
			else if (watchItem.allServices)
				broker.logger.debug(`  ${relPath}:`, kleur.grey("reload all services."));
			else if (watchItem.services.length > 0) {
				broker.logger.debug(`  ${relPath}:`, kleur.grey(`reload ${watchItem.services.length} service(s) & ${watchItem.others.length} other(s).`)/*, watchItem.services, watchItem.others*/);
				watchItem.services.forEach(svcFullname => broker.logger.debug(kleur.grey(`    ${svcFullname}`)));
				watchItem.others.forEach(filename => broker.logger.debug(kleur.grey(`    ${path.relative(process.cwd(), filename)}`)));
			}
			// Create watcher
			watchItem.watcher = fs.watch(fName, (eventType) => {
				const relPath = path.relative(process.cwd(), fName);
				broker.logger.info(kleur.magenta().bold(`The '${relPath}' file is changed. (Event: ${eventType})`));

				// Clear from require cache
				clearRequireCache(fName);
				if (watchItem.others.length > 0) {
					watchItem.others.forEach(f => clearRequireCache(f));
				}

				if (watchItem.brokerRestart && broker.runner && isFunction(broker.runner.restartBroker)) {
					broker.logger.info(kleur.bgMagenta().white().bold("Action: Restart broker..."));
					stopAllFileWatcher(projectFiles);
					// Clear the whole require cache
					require.cache.length = 0;
					return broker.runner.restartBroker();

				} else if (watchItem.allServices) {
					// Reload all services
					broker.services.forEach(svc => {
						if (svc.__filename)
							needToReload.add(svc);
					});
					reloadServices();

				} else if (watchItem.services.length > 0) {
					// Reload certain services
					broker.services.forEach(svc => {
						if (watchItem.services.indexOf(svc.fullName) !== -1)
							needToReload.add(svc);
					});

					if (needToReload.size == 0) {
						// It means, it's a crashed reloaded service, so we
						// didn't find it in the loaded services because
						// the previous hot-reload failed. We should load it
						// broker.loadService
						needToReload.add(relPath);
					}
					reloadServices();
				}
			});
		});

		if (projectFiles.size == 0)
			broker.logger.debug(kleur.grey("  No files."));

	}

	const debouncedWatchProjectFiles = _.debounce(watchProjectFiles, 2000);

	/**
	 * Stop all file watchers
	 */
	function stopAllFileWatcher(items) {
		items.forEach((watchItem) => {
			if (watchItem.watcher) {
				watchItem.watcher.close();
				watchItem.watcher = null;
			}
		});
	}

	/**
	 * Get a watch item
	 *
	 * @param {String} fName
	 * @returns {Object}
	 */
	function getWatchItem(fName) {
		let watchItem = projectFiles.get(fName);
		if (watchItem)
			return watchItem;

		watchItem = {
			services: [],
			allServices: false,
			brokerRestart: false,
			others: []
		};
		projectFiles.set(fName, watchItem);

		return watchItem;
	}

	function isMoleculerConfig(fName) {
		return fName.endsWith("moleculer.config.js")
			|| fName.endsWith("moleculer.config.ts")
			|| fName.endsWith("moleculer.config.json");
	}

	/**
	 * Process module children modules.
	 *
	 * @param {*} mod
	 * @param {*} service
	 * @param {Number} level
	 */
	function processModule(mod, service = null, level = 0, parents = null) {
		const fName = mod.filename;

		// Skip node_modules files, if there is parent project file
		if ((service || parents) && fName.indexOf("node_modules") !== -1)
			if (hotReloadModules.find(modulePath => fName.indexOf(modulePath) !== -1) == null)
				return;

		// Avoid circular dependency in project files
		if (parents && parents.indexOf(fName) !== -1)
			return;

		// console.log(fName);

		// Cache files to avoid cyclic dependencies in node_modules
		if (fName.indexOf("node_modules") !== -1) {
			if (cache.get(fName)) return;
			cache.set(fName, mod);
		}

		if (!service) {
			service = broker.services.find(svc => svc.__filename == fName);
		}

		if (service) {
			// It is a service dependency. We should reload this service if this file has changed.
			const watchItem = getWatchItem(fName);
			if (!watchItem.services.includes(service.fullName))
				watchItem.services.push(service.fullName);

			watchItem.others = _.uniq([].concat(watchItem.others, parents || []));

		} else if (isMoleculerConfig(fName)) {
			const watchItem = getWatchItem(fName);
			watchItem.brokerRestart = true;
		} else {
			// It is not a service dependency, it is a global middleware. We should reload all services if this file has changed.
			if (parents) {
				const watchItem = getWatchItem(fName);
				watchItem.allServices = true;
				watchItem.others = _.uniq([].concat(watchItem.others, parents || []));
			}
		}

		if (mod.children && mod.children.length > 0) {
			if (service) {
				parents = parents ? parents.concat([fName]) : [fName];
			} else if (isMoleculerConfig(fName)) {
				parents = [];
				// const watchItem = getWatchItem(fName);
				// watchItem.brokerRestart = true;
			} else if (parents) {
				parents.push(fName);
			}
			mod.children.forEach(m => processModule(m, service, service ? level + 1 : 0, parents));
		}
	}

	const folderWatchers = [];

	function watchProjectFolders() {
		// Debounced Service loader function
		const needToLoad = new Set();
		const loadServices = _.debounce(() => {
			broker.logger.info(kleur.bgMagenta().white().bold(`Load ${needToLoad.size} service(s)...`));

			needToLoad.forEach(filename => {
				try {
					broker.loadService(filename);
				} catch(err) {
					broker.logger.error(`Failed to load service '${filename}'`, err);
					clearRequireCache(filename);
				}
			});
			needToLoad.clear();

		}, 500);

		if (broker.runner && Array.isArray(broker.runner.folders)) {
			const folders = broker.runner.folders;
			if (folders.length > 0) {
				folderWatchers.length = 0;

				broker.logger.debug("");
				broker.logger.debug(kleur.yellow().bold("Watching the following folder(s):"));

				folders.forEach(folder => {
					makeDirs(folder);
					broker.logger.debug(`  ${path.relative(process.cwd(), folder)}/`);
					folderWatchers.push({
						path: folder,
						watcher: watch(folder, (filename) => {
							if (filename.endsWith(".service.js") || filename.endsWith(".service.ts")) {
								broker.logger.debug(`There is changes in '${folder}' folder: `, path.basename(filename));
								const isLoaded = broker.services.some(svc => svc.__filename == filename);
								const fileExists = fs.existsSync(filename);
								if (!isLoaded && fileExists) {
									// This is a new file. We should wait for the file fully copied.
									broker.logger.debug("Loading new file: ", path.basename(filename));
									needToLoad.add(filename);
									loadServices();
								}
							}
						})
					});
				});
			}
		}
	}

	function stopProjectFolderWatchers() {
		broker.logger.debug("");
		broker.logger.debug("Stop watching folders.");
		folderWatchers.forEach(item => item.watcher && item.watcher());
	}

	/**
	 * Expose middleware
	 */
	return {
		name: "HotReload",

		// After broker started
		started(broker) {
			if (broker.options.hotReload == null) {
				return;
			} else if (typeof broker.options.hotReload === "object") {
				if (Array.isArray(broker.options.hotReload.modules)) {
					hotReloadModules = broker.options.hotReload.modules.map(moduleName => `/node_modules/${moduleName}/`);
				}
				if (broker.options.hotReload.extraFiles) {
					/**
					 * **Example:**
					 * ```js
					 * hotReload: {
					 * 	extraFiles: {
					 * 		"./configuration.json": "broker", // reload the broker
					 * 		"./common.js": "allServices", // reload all services
					 * 		"./database": "v1.posts", // reload a service
					 * 		"./database": ["v1.posts", "users"], // reload multiple services
					 * 	}
					 * }
					 * ```
					 */
					extraFiles = broker.options.hotReload.extraFiles;
				}
			} else if (broker.options.hotReload !== true) {
				return;
			}

			watchProjectFiles();

			watchProjectFolders();
		},

		serviceStarted() {
			// Re-watch new services if broker has already started and a new service started.
			if (broker.started) {
				debouncedWatchProjectFiles();
			}
		},

		stopped() {
			stopProjectFolderWatchers();
		}
	};

};
<|endoftext|>


<commit_before>/**
 *
 * @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import Vue from 'vue'

/**
 * Mixin to reparent the panel of the vue-at component to a specific element.
 *
 * By default the panel of the vue-at component is a child of the root element
 * of the component. In some cases this may not be desirable (for example, if
 * a parent element uses "overflow: hidden" and causes the panel to be
 * partially hidden), so this mixin reparents the panel to a specific element
 * when it is shown.
 *
 * Components using this mixin require a reference called "at" to the vue-at
 * component. The desired parent element can be specified using the
 * "atWhoPanelParentSelector" property.
 */
export default {

	data: function() {
		return {
			/**
			 * The selector for the HTML element to reparent the vue-at panel to.
			 */
			atWhoPanelParentSelector: 'body',
			at: null,
			atWhoPanelElement: null,
			originalWrapElement: null,
		}
	},

	computed: {
		/**
		 * Returns the "atwho" property of the vue-at component.
		 *
		 * The "atwho" property is an object when the panel is open and null
		 * when the panel is closed.
		 *
		 * @returns {Object} the "atwho" property of the vue-at component.
		 */
		atwho() {
			if (!this.at) {
				return null
			}

			return this.at.atwho
		},
	},

	watch: {
		/**
		 * Reparents the panel of the vue-at component when shown.
		 *
		 * Besides reparenting the panel its position needs to be adjusted to
		 * the new parent. The panel is initially a child of the "wrap" element
		 * of vue-at and vue-at calculates the position of the panel based on
		 * that element. Fortunately the reference to that element is not used
		 * for anything else, so it can be modified while the panel is open to
		 * point to the new parent.
		 *
		 * @param {Object} atwho current value of atwho
		 * @param {Object} atwhoOld previous value of atwho
		 */
		atwho(atwho, atwhoOld) {
			// Only check whether the object existed or not; its properties are
			// not relevant.
			if ((atwho && atwhoOld) || (!atwho && !atwhoOld)) {
				return
			}

			if (atwho) {
				// Panel will be opened in next tick; defer moving it to the
				// proper parent until that happens
				Vue.nextTick(function() {
					this.atWhoPanelElement = this.at.$refs.wrap.querySelector('.atwho-panel')

					this.originalWrapElement = this.at.$refs.wrap
					this.at.$refs.wrap = window.document.querySelector(this.atWhoPanelParentSelector)

					const atWhoPanelParentSelector = window.document.querySelector(this.atWhoPanelParentSelector)
					atWhoPanelParentSelector.appendChild(this.atWhoPanelElement)

					// The position of the panel will be automatically adjusted
					// due to the reactivity, but that will happen in next tick.
					// To prevent a flicker due to the change of the panel
					// position the style is explicitly adjusted now.
					const { top, left } = this.at._computedWatchers.style.get()
					this.atWhoPanelElement.style.top = top
					this.atWhoPanelElement.style.left = left
				}.bind(this))
			} else {
				this.at.$refs.wrap = this.originalWrapElement
				this.originalWrapElement = null

				// Panel will be closed in next tick; move it back to the
				// expected parent before that happens.
				this.at.$refs.wrap.appendChild(this.atWhoPanelElement)
			}
		},
	},

	mounted() {
		// $refs is not reactive and its contents are set after the initial
		// render.
		this.at = this.$refs.at
	},

}
<commit_msg>Make possible to set extra CSS classes to the vue-at panel<commit_after>/**
 *
 * @copyright Copyright (c) 2020, Daniel Calviño Sánchez <danxuliu@gmail.com>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import Vue from 'vue'

/**
 * Mixin to reparent the panel of the vue-at component to a specific element.
 *
 * By default the panel of the vue-at component is a child of the root element
 * of the component. In some cases this may not be desirable (for example, if
 * a parent element uses "overflow: hidden" and causes the panel to be
 * partially hidden), so this mixin reparents the panel to a specific element
 * when it is shown.
 *
 * Components using this mixin require a reference called "at" to the vue-at
 * component. The desired parent element can be specified using the
 * "atWhoPanelParentSelector" property.
 */
export default {

	data: function() {
		return {
			/**
			 * The selector for the HTML element to reparent the vue-at panel to.
			 */
			atWhoPanelParentSelector: 'body',
			/**
			 * Extra CSS classes to be set in the vue-at panel.
			 */
			atWhoPanelExtraClasses: '',
			at: null,
			atWhoPanelElement: null,
			originalWrapElement: null,
		}
	},

	computed: {
		/**
		 * Returns the "atwho" property of the vue-at component.
		 *
		 * The "atwho" property is an object when the panel is open and null
		 * when the panel is closed.
		 *
		 * @returns {Object} the "atwho" property of the vue-at component.
		 */
		atwho() {
			if (!this.at) {
				return null
			}

			return this.at.atwho
		},

		/**
		 * Returns a list of CSS clases from the space separated string
		 * "atWhoPanelExtraClasses".
		 *
		 * @returns {Array} the list of CSS classes
		 */
		atWhoPanelExtraClassesList() {
			return this.atWhoPanelExtraClasses.split(' ').filter(cssClass => cssClass !== '')
		},
	},

	watch: {
		/**
		 * Reparents the panel of the vue-at component when shown.
		 *
		 * Besides reparenting the panel its position needs to be adjusted to
		 * the new parent. The panel is initially a child of the "wrap" element
		 * of vue-at and vue-at calculates the position of the panel based on
		 * that element. Fortunately the reference to that element is not used
		 * for anything else, so it can be modified while the panel is open to
		 * point to the new parent.
		 *
		 * @param {Object} atwho current value of atwho
		 * @param {Object} atwhoOld previous value of atwho
		 */
		atwho(atwho, atwhoOld) {
			// Only check whether the object existed or not; its properties are
			// not relevant.
			if ((atwho && atwhoOld) || (!atwho && !atwhoOld)) {
				return
			}

			if (atwho) {
				// Panel will be opened in next tick; defer moving it to the
				// proper parent until that happens
				Vue.nextTick(function() {
					this.atWhoPanelElement = this.at.$refs.wrap.querySelector('.atwho-panel')

					if (this.atWhoPanelExtraClassesList.length > 0) {
						this.atWhoPanelElement.classList.add(...this.atWhoPanelExtraClassesList)
					}

					this.originalWrapElement = this.at.$refs.wrap
					this.at.$refs.wrap = window.document.querySelector(this.atWhoPanelParentSelector)

					const atWhoPanelParentSelector = window.document.querySelector(this.atWhoPanelParentSelector)
					atWhoPanelParentSelector.appendChild(this.atWhoPanelElement)

					// The position of the panel will be automatically adjusted
					// due to the reactivity, but that will happen in next tick.
					// To prevent a flicker due to the change of the panel
					// position the style is explicitly adjusted now.
					const { top, left } = this.at._computedWatchers.style.get()
					this.atWhoPanelElement.style.top = top
					this.atWhoPanelElement.style.left = left
				}.bind(this))
			} else {
				this.at.$refs.wrap = this.originalWrapElement
				this.originalWrapElement = null

				// Panel will be closed in next tick; move it back to the
				// expected parent before that happens.
				this.at.$refs.wrap.appendChild(this.atWhoPanelElement)
			}
		},
	},

	mounted() {
		// $refs is not reactive and its contents are set after the initial
		// render.
		this.at = this.$refs.at
	},

}
<|endoftext|>


<commit_before>(function(angular, $) {
    'use strict';

    var module = angular.module('ngx.ui.dateInput', [
        'ngx.date',
        'ngx.dictionary'
    ]);

    /**
     * Date input type
     * @todo range/time refactoring
     */
    module.directive('ngxDateInput', ['$parse', 'ngxDate', 'ngxDictionary', function($parse, ngxDate, ngxDictionary) {
        return {
            require: 'ngModel',
            link: function(scope, element, attrs, ctrl) {
                element = $(element);

                // apply jquery UI datepicker
                element.datepicker({
                    dateFormat: 'd.m.yy',
                    firstDay: 1,
                    showButtonPanel: false,
                    showMinute: false,
                    closeText: ngxDictionary('NGX_UI_DATEINPUT_CLOSE'),
                    prevText: ngxDictionary('NGX_UI_DATEINPUT_PREV'),
                    nextText: ngxDictionary('NGX_UI_DATEINPUT_NEXT'),
                    currentText: ngxDictionary('NGX_UI_DATEINPUT_NOW'),
                    monthNames: ngxDictionary('NGX_UI_DATEINPUT_MONTHS'),
                    dayNamesMin: ngxDictionary('NGX_UI_DATEINPUT_DAYS'),
                    onSelect: function(dateText) {
                        scope.$apply(function() {
                            ctrl.$setViewValue(dateText);
                        });
                    }
                });

                // store element reference into widget scope for future datepicker update
                ctrl.element = element;

                var dateRangeMaxDays = attrs.rangeMaxdays ? attrs.rangeMaxdays : undefined;
                var dateInputMin = ngxDate.parse(attrs.min);
                var dateInputMax = ngxDate.parse(attrs.max);
                var dateInputMaxRange = null;

                // set initial minimum date
                if (dateInputMin) {
                    ctrl.element.datepicker('option', 'minDate', dateInputMin);
                }

                // set initial maximum date
                if (dateInputMax) {
                    ctrl.element.datepicker('option', 'maxDate', dateInputMax);
                }

                // related date range input (from-to)
                if (attrs.rangeInput) {
                    // range config
                    ctrl.range = {
                        type: 'max',
                        ctrl: $parse(attrs.rangeInput)(scope)
                    };
                    // back reference
                    ctrl.range.ctrl.range = {
                        type: 'min',
                        ctrl: ctrl
                    };
                } else {
                    ctrl.range = null;
                }

                // parse value, validate and set into model as timestamp
                ctrl.$parsers.push(function(viewValue) {
                    // datetime validation
                    var date,
                        valid = true;

                    if (viewValue && ctrl.$dirty) {
                        // parse and check date
                        var pd = new RegExp('^([0-9]{1,2})\. ?([0-9]{1,2})\. ?([0-9]{4})').exec(viewValue);
                        valid = (pd && ngxDate.check(pd[3], pd[2], pd[1]));

                        if (valid) {
                            date = new Date(pd[3], pd[2] - 1, pd[1]);

                            // check min input date
                            if (dateInputMin && date < dateInputMin) {
                                valid = false;
                            }

                            // check max input date
                            if (dateInputMax) {
                                if (date > dateInputMax) {
                                    valid = false;
                                }

                                // if max-days range is set, move max input to range end
                                if (dateRangeMaxDays) {
                                    dateInputMaxRange = new Date(date.getTime() + (60*60*24*dateRangeMaxDays*1000));

                                    if (ctrl.range.ctrl.timestampValue) {
                                        if (date.getTime() - (60*60*24*dateRangeMaxDays*1000) > (ctrl.range.ctrl.timestampValue * 1000)) {
                                            valid = false;
                                        }
                                    }
                                }
                            }

                            // apply related time input
                            if (ctrl.timeInput) {
                                if (ctrl.timeInput.$valid) {
                                    var hours = ctrl.timeInput.hours;
                                    var minutes = ctrl.timeInput.minutes;

                                    // when in range (as max) and has no time .. set time to 23:59
                                    if (ctrl.range && ctrl.range.type === 'max' && hours === undefined) {
                                        hours = 23;
                                        minutes = 59;
                                    }

                                    if (hours !== undefined) {
                                        date.setHours(hours);
                                        date.setMinutes(minutes);
                                    }
                                } else {
                                    valid = false;
                                }
                            }
                        }
                    }

                    // model contains ISO date
                    var modelValue = (date && valid ? ngxDate.format('Y-m-d', date) : undefined);

                    ctrl.timestampValue = (date && valid ? date.getTime() / 1000 : undefined);
                    ctrl.$setValidity('date', valid);

                    // date range
                    if (ctrl.range) {
                        // update related date picker min/max
                        if (!ctrl.$error.date) {

                            ctrl.range.ctrl.element.datepicker('option', ctrl.range.type + 'Date', viewValue);

                            if (ctrl.range.type == 'min' && dateInputMaxRange) {
                                ctrl.range.ctrl.element.datepicker('option', 'maxDate', dateInputMaxRange);
                            }
                        }

                        // range validation
                        var min = (ctrl.range.type === 'min' ? ctrl.timestampValue : ctrl.range.ctrl.timestampValue);
                        var max = (ctrl.range.type === 'max' ? ctrl.timestampValue : ctrl.range.ctrl.timestampValue);
                        valid = (min && max ? (min <= max) : true);
                        ctrl.$setValidity('range', valid);
                        ctrl.range.ctrl.$setValidity('range', valid);
                    }

                    return modelValue;
                });
            }
        };
    }]);

})(window.angular, window.jQuery);
<commit_msg>fix regexp on input (escaping dot)<commit_after>(function(angular, $) {
    'use strict';

    var module = angular.module('ngx.ui.dateInput', [
        'ngx.date',
        'ngx.dictionary'
    ]);

    /**
     * Date input type
     * @todo range/time refactoring
     */
    module.directive('ngxDateInput', ['$parse', 'ngxDate', 'ngxDictionary', function($parse, ngxDate, ngxDictionary) {
        return {
            require: 'ngModel',
            link: function(scope, element, attrs, ctrl) {
                element = $(element);

                // apply jquery UI datepicker
                element.datepicker({
                    dateFormat: 'd.m.yy',
                    firstDay: 1,
                    showButtonPanel: false,
                    showMinute: false,
                    closeText: ngxDictionary('NGX_UI_DATEINPUT_CLOSE'),
                    prevText: ngxDictionary('NGX_UI_DATEINPUT_PREV'),
                    nextText: ngxDictionary('NGX_UI_DATEINPUT_NEXT'),
                    currentText: ngxDictionary('NGX_UI_DATEINPUT_NOW'),
                    monthNames: ngxDictionary('NGX_UI_DATEINPUT_MONTHS'),
                    dayNamesMin: ngxDictionary('NGX_UI_DATEINPUT_DAYS'),
                    onSelect: function(dateText) {
                        scope.$apply(function() {
                            ctrl.$setViewValue(dateText);
                        });
                    }
                });

                // store element reference into widget scope for future datepicker update
                ctrl.element = element;

                var dateRangeMaxDays = attrs.rangeMaxdays ? attrs.rangeMaxdays : undefined;
                var dateInputMin = ngxDate.parse(attrs.min);
                var dateInputMax = ngxDate.parse(attrs.max);
                var dateInputMaxRange = null;

                // set initial minimum date
                if (dateInputMin) {
                    ctrl.element.datepicker('option', 'minDate', dateInputMin);
                }

                // set initial maximum date
                if (dateInputMax) {
                    ctrl.element.datepicker('option', 'maxDate', dateInputMax);
                }

                // related date range input (from-to)
                if (attrs.rangeInput) {
                    // range config
                    ctrl.range = {
                        type: 'max',
                        ctrl: $parse(attrs.rangeInput)(scope)
                    };
                    // back reference
                    ctrl.range.ctrl.range = {
                        type: 'min',
                        ctrl: ctrl
                    };
                } else {
                    ctrl.range = null;
                }

                // parse value, validate and set into model as timestamp
                ctrl.$parsers.push(function(viewValue) {
                    // datetime validation
                    var date,
                        valid = true;

                    if (viewValue && ctrl.$dirty) {
                        // parse and check date
                        var pd = new RegExp('^([0-9]{1,2})\\. ?([0-9]{1,2})\\. ?([0-9]{4})').exec(viewValue);
                        valid = (pd && ngxDate.check(pd[3], pd[2], pd[1]));

                        if (valid) {
                            date = new Date(pd[3], pd[2] - 1, pd[1]);

                            // check min input date
                            if (dateInputMin && date < dateInputMin) {
                                valid = false;
                            }

                            // check max input date
                            if (dateInputMax) {
                                if (date > dateInputMax) {
                                    valid = false;
                                }

                                // if max-days range is set, move max input to range end
                                if (dateRangeMaxDays) {
                                    dateInputMaxRange = new Date(date.getTime() + (60*60*24*dateRangeMaxDays*1000));

                                    if (ctrl.range.ctrl.timestampValue) {
                                        if (date.getTime() - (60*60*24*dateRangeMaxDays*1000) > (ctrl.range.ctrl.timestampValue * 1000)) {
                                            valid = false;
                                        }
                                    }
                                }
                            }

                            // apply related time input
                            if (ctrl.timeInput) {
                                if (ctrl.timeInput.$valid) {
                                    var hours = ctrl.timeInput.hours;
                                    var minutes = ctrl.timeInput.minutes;

                                    // when in range (as max) and has no time .. set time to 23:59
                                    if (ctrl.range && ctrl.range.type === 'max' && hours === undefined) {
                                        hours = 23;
                                        minutes = 59;
                                    }

                                    if (hours !== undefined) {
                                        date.setHours(hours);
                                        date.setMinutes(minutes);
                                    }
                                } else {
                                    valid = false;
                                }
                            }
                        }
                    }

                    // model contains ISO date
                    var modelValue = (date && valid ? ngxDate.format('Y-m-d', date) : undefined);

                    ctrl.timestampValue = (date && valid ? date.getTime() / 1000 : undefined);
                    ctrl.$setValidity('date', valid);

                    // date range
                    if (ctrl.range) {
                        // update related date picker min/max
                        if (!ctrl.$error.date) {

                            ctrl.range.ctrl.element.datepicker('option', ctrl.range.type + 'Date', viewValue);

                            if (ctrl.range.type == 'min' && dateInputMaxRange) {
                                ctrl.range.ctrl.element.datepicker('option', 'maxDate', dateInputMaxRange);
                            }
                        }

                        // range validation
                        var min = (ctrl.range.type === 'min' ? ctrl.timestampValue : ctrl.range.ctrl.timestampValue);
                        var max = (ctrl.range.type === 'max' ? ctrl.timestampValue : ctrl.range.ctrl.timestampValue);
                        valid = (min && max ? (min <= max) : true);
                        ctrl.$setValidity('range', valid);
                        ctrl.range.ctrl.$setValidity('range', valid);
                    }

                    return modelValue;
                });
            }
        };
    }]);

})(window.angular, window.jQuery);
<|endoftext|>


<commit_before>// list-api-test.js
//
// Test user collections of people
//
// Copyright 2012, StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var assert = require("assert"),
    vows = require("vows"),
    Step = require("step"),
    _ = require("underscore"),
    http = require("http"),
    urlparse = require("url").parse,
    OAuth = require("oauth").OAuth,
    httputil = require("./lib/http"),
    oauthutil = require("./lib/oauth"),
    actutil = require("./lib/activity"),
    setupApp = oauthutil.setupApp,
    newClient = oauthutil.newClient,
    newPair = oauthutil.newPair,
    register = oauthutil.register;

var makeCred = function(cl, pair) {
    return {
        consumer_key: cl.client_id,
        consumer_secret: cl.client_secret,
        token: pair.token,
        token_secret: pair.token_secret
    };
};

var assertValidList = function(doc, count, itemCount) {
    assert.include(doc, "author");
    assert.include(doc.author, "id");
    assert.include(doc.author, "displayName");
    assert.include(doc.author, "objectType");
    assert.include(doc, "totalItems");
    assert.include(doc, "items");
    assert.include(doc, "displayName");
    assert.include(doc, "id");
    if (_(count).isNumber()) {
        assert.equal(doc.totalItems, count);
    }
    if (_(itemCount).isNumber()) {
        assert.lengthOf(doc.items, itemCount);
    }
};

var assertValidActivity = function(act) {
    assert.isString(act.id);
    assert.include(act, "actor");
    assert.isObject(act.actor);
    assert.include(act.actor, "id");
    assert.isString(act.actor.id);
    assert.include(act, "verb");
    assert.isString(act.verb);
    assert.include(act, "object");
    assert.isObject(act.object);
    assert.include(act.object, "id");
    assert.isString(act.object.id);
    assert.include(act, "published");
    assert.isString(act.published);
    assert.include(act, "updated");
    assert.isString(act.updated);
};

var suite = vows.describe("list api test");

// A batch to test following/unfollowing users

suite.addBatch({
    "When we set up the app": {
        topic: function() {
            setupApp(this.callback);
        },
        teardown: function(app) {
            if (app && app.close) {
                app.close();
            }
        },
        "it works": function(err, app) {
            assert.ifError(err);
        },
        "and we register a client": {
            topic: function() {
                newClient(this.callback);
            },
            "it works": function(err, cl) {
                assert.ifError(err);
                assert.isObject(cl);
            },
            "and we get the list of lists owned by a new user": {
                topic: function(cl) {
                    var cb = this.callback;
                    Step(
                        function() {
                            newPair(cl, "eekamouse", "bongbongdiggydiggydang", this);
                        },
                        function(err, pair) {
                            if (err) throw err;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/eekamouse/lists";

                            httputil.getJSON(url, cred, this);
                        },
                        function(err, doc, response) {
                            cb(err, doc);
                        }
                    );
                },
                "it works": function(err, lists) {
                    assert.ifError(err);
                },
                "it is valid": function(err, lists) {
                    assert.ifError(err);
                    assertValidList(lists, 0);
                }
            },
            "and a user creates a list": {
                topic: function(cl) {
                    var cb = this.callback,
                        pair = null;

                    Step(
                        function() {
                            newPair(cl, "yellowman", "nobodymove", this);
                        },
                        function(err, results) {
                            if (err) throw err;
                            pair = results;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/yellowman/feed",
                                act = {
                                    verb: "post",
                                    object: {
                                        objectType: "collection",
                                        displayName: "Bad Boys",
                                        objectTypes: ["person"]
                                    }
                                };

                            httputil.postJSON(url, cred, act, this);
                        },
                        function(err, doc, response) {
                            cb(err, doc, pair);
                        }
                    );
                },
                "it works": function(err, act, pair) {
                    assert.ifError(err);
                    assert.isObject(act);
                },
                "results look correct": function(err, act, pair) {
                    assert.include(act, "id");
                    assertValidActivity(act);
                },
                "object has correct data": function(err, act) {
                    assert.ifError(err);
                    assert.equal(act.object.objectType, "collection");
                    assert.equal(act.object.displayName, "Bad Boys");
                    assert.include(act.object, "members");
                    assert.isObject(act.object.members);
                    assert.include(act.object.members, "totalItems");
                    assert.equal(act.object.members.totalItems, 0);
                },
                "and we get the list of lists owned by the user": {
                    topic: function(act, pair, cl) {
                        var cb = this.callback,
                            cred = makeCred(cl, pair),
                            url = "http://localhost:4815/api/user/yellowman/lists";

                        httputil.getJSON(url, cred, function(err, doc, response) {
                            cb(err, doc, act.object);
                        });
                    },
                    "it works": function(err, lists, collection) {
                        assert.ifError(err);
                        assert.isObject(lists);
                    },
                    "it looks correct": function(err, lists, collection) {
                        assert.ifError(err);
                        assertValidList(lists, 1);
                        assert.include(lists, "objectTypes");
                        assert.isArray(lists.objectTypes);
                        assert.include(lists.objectTypes, "collection");
                    },
                    "it contains the new list": function(err, lists, collection) {
                        assert.ifError(err);
                        assert.include(lists, "items");
                        assert.isArray(lists.items);
                        assert.lengthOf(lists.items, 1);
                        assert.equal(lists.items[0].id, collection.id);
                    }
                }
            },
            "and a user creates a lot of lists": {
                topic: function(cl) {
                    var cb = this.callback,
                        pair = null;

                    Step(
                        function() {
                            newPair(cl, "dekker", "sabotage", this);
                        },
                        function(err, results) {
                            if (err) throw err;
                            pair = results;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/dekker/feed",
                                act = {
                                    verb: "post",
                                    object: {
                                        objectType: "collection",
                                        objectTypes: ["person"]
                                    }
                                },
                                acti,
                                group = this.group();

                            for (var i = 0; i < 100; i++) {
                                acti = _(act).clone();
                                acti.object.displayName = "Israelites #" + i;
                                httputil.postJSON(url, cred, acti, group());
                            }
                        },
                        function(err, docs, responses) {
                            cb(err, docs, pair);
                        }
                    );
                },
                "it works": function(err, lists) {
                    assert.ifError(err);
                    assert.isArray(lists);
                    assert.lengthOf(lists, 100);
                    for (var i = 0; i < 100; i++) {
                        assert.isObject(lists[i]);
                        assertValidActivity(lists[i]);
                    }
                },
                "and we get the list of lists owned by the user": {
                    topic: function(acts, pair, cl) {
                        var cb = this.callback,
                            cred = makeCred(cl, pair),
                            url = "http://localhost:4815/api/user/dekker/lists";

                        httputil.getJSON(url, cred, function(err, doc, response) {
                            cb(err, doc);
                        });
                    },
                    "it works": function(err, lists, acts) {
                        assert.ifError(err);
                        assert.isObject(lists);
                    },
                    "it looks correct": function(err, lists, acts) {
                        assert.ifError(err);
                        assertValidList(lists, 100, 20);
                        assert.include(lists, "objectTypes");
                        assert.isArray(lists.objectTypes);
                        assert.include(lists.objectTypes, "collection");
                    }
                }
            }
        }
    }
});

suite["export"](module);

<commit_msg>test deleting a list<commit_after>// list-api-test.js
//
// Test user collections of people
//
// Copyright 2012, StatusNet Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var assert = require("assert"),
    vows = require("vows"),
    Step = require("step"),
    _ = require("underscore"),
    http = require("http"),
    urlparse = require("url").parse,
    OAuth = require("oauth").OAuth,
    httputil = require("./lib/http"),
    oauthutil = require("./lib/oauth"),
    actutil = require("./lib/activity"),
    setupApp = oauthutil.setupApp,
    newClient = oauthutil.newClient,
    newPair = oauthutil.newPair,
    register = oauthutil.register;

var makeCred = function(cl, pair) {
    return {
        consumer_key: cl.client_id,
        consumer_secret: cl.client_secret,
        token: pair.token,
        token_secret: pair.token_secret
    };
};

var assertValidList = function(doc, count, itemCount) {
    assert.include(doc, "author");
    assert.include(doc.author, "id");
    assert.include(doc.author, "displayName");
    assert.include(doc.author, "objectType");
    assert.include(doc, "totalItems");
    assert.include(doc, "items");
    assert.include(doc, "displayName");
    assert.include(doc, "id");
    if (_(count).isNumber()) {
        assert.equal(doc.totalItems, count);
    }
    if (_(itemCount).isNumber()) {
        assert.lengthOf(doc.items, itemCount);
    }
};

var assertValidActivity = function(act) {
    assert.isString(act.id);
    assert.include(act, "actor");
    assert.isObject(act.actor);
    assert.include(act.actor, "id");
    assert.isString(act.actor.id);
    assert.include(act, "verb");
    assert.isString(act.verb);
    assert.include(act, "object");
    assert.isObject(act.object);
    assert.include(act.object, "id");
    assert.isString(act.object.id);
    assert.include(act, "published");
    assert.isString(act.published);
    assert.include(act, "updated");
    assert.isString(act.updated);
};

var suite = vows.describe("list api test");

// A batch to test following/unfollowing users

suite.addBatch({
    "When we set up the app": {
        topic: function() {
            setupApp(this.callback);
        },
        teardown: function(app) {
            if (app && app.close) {
                app.close();
            }
        },
        "it works": function(err, app) {
            assert.ifError(err);
        },
        "and we register a client": {
            topic: function() {
                newClient(this.callback);
            },
            "it works": function(err, cl) {
                assert.ifError(err);
                assert.isObject(cl);
            },
            "and we get the list of lists owned by a new user": {
                topic: function(cl) {
                    var cb = this.callback;
                    Step(
                        function() {
                            newPair(cl, "eekamouse", "bongbongdiggydiggydang", this);
                        },
                        function(err, pair) {
                            if (err) throw err;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/eekamouse/lists";

                            httputil.getJSON(url, cred, this);
                        },
                        function(err, doc, response) {
                            cb(err, doc);
                        }
                    );
                },
                "it works": function(err, lists) {
                    assert.ifError(err);
                },
                "it is valid": function(err, lists) {
                    assert.ifError(err);
                    assertValidList(lists, 0);
                }
            },
            "and a user creates a list": {
                topic: function(cl) {
                    var cb = this.callback,
                        pair = null;

                    Step(
                        function() {
                            newPair(cl, "yellowman", "nobodymove", this);
                        },
                        function(err, results) {
                            if (err) throw err;
                            pair = results;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/yellowman/feed",
                                act = {
                                    verb: "post",
                                    object: {
                                        objectType: "collection",
                                        displayName: "Bad Boys",
                                        objectTypes: ["person"]
                                    }
                                };

                            httputil.postJSON(url, cred, act, this);
                        },
                        function(err, doc, response) {
                            cb(err, doc, pair);
                        }
                    );
                },
                "it works": function(err, act, pair) {
                    assert.ifError(err);
                    assert.isObject(act);
                },
                "results look correct": function(err, act, pair) {
                    assert.include(act, "id");
                    assertValidActivity(act);
                },
                "object has correct data": function(err, act) {
                    assert.ifError(err);
                    assert.equal(act.object.objectType, "collection");
                    assert.equal(act.object.displayName, "Bad Boys");
                    assert.include(act.object, "members");
                    assert.isObject(act.object.members);
                    assert.include(act.object.members, "totalItems");
                    assert.equal(act.object.members.totalItems, 0);
                },
                "and we get the list of lists owned by the user": {
                    topic: function(act, pair, cl) {
                        var cb = this.callback,
                            cred = makeCred(cl, pair),
                            url = "http://localhost:4815/api/user/yellowman/lists";

                        httputil.getJSON(url, cred, function(err, doc, response) {
                            cb(err, doc, act.object);
                        });
                    },
                    "it works": function(err, lists, collection) {
                        assert.ifError(err);
                        assert.isObject(lists);
                    },
                    "it looks correct": function(err, lists, collection) {
                        assert.ifError(err);
                        assertValidList(lists, 1);
                        assert.include(lists, "objectTypes");
                        assert.isArray(lists.objectTypes);
                        assert.include(lists.objectTypes, "collection");
                    },
                    "it contains the new list": function(err, lists, collection) {
                        assert.ifError(err);
                        assert.include(lists, "items");
                        assert.isArray(lists.items);
                        assert.lengthOf(lists.items, 1);
                        assert.equal(lists.items[0].id, collection.id);
                    }
                }
            },
            "and a user creates a lot of lists": {
                topic: function(cl) {
                    var cb = this.callback,
                        pair = null;

                    Step(
                        function() {
                            newPair(cl, "dekker", "sabotage", this);
                        },
                        function(err, results) {
                            if (err) throw err;
                            pair = results;
                            var cred = makeCred(cl, pair),
                                url = "http://localhost:4815/api/user/dekker/feed",
                                act = {
                                    verb: "post",
                                    object: {
                                        objectType: "collection",
                                        objectTypes: ["person"]
                                    }
                                },
                                acti,
                                group = this.group();

                            for (var i = 0; i < 100; i++) {
                                acti = _(act).clone();
                                acti.object.displayName = "Israelites #" + i;
                                httputil.postJSON(url, cred, acti, group());
                            }
                        },
                        function(err, docs, responses) {
                            cb(err, docs, pair);
                        }
                    );
                },
                "it works": function(err, lists) {
                    assert.ifError(err);
                    assert.isArray(lists);
                    assert.lengthOf(lists, 100);
                    for (var i = 0; i < 100; i++) {
                        assert.isObject(lists[i]);
                        assertValidActivity(lists[i]);
                    }
                },
                "and we get the list of lists owned by the user": {
                    topic: function(acts, pair, cl) {
                        var cb = this.callback,
                            cred = makeCred(cl, pair),
                            url = "http://localhost:4815/api/user/dekker/lists";

                        httputil.getJSON(url, cred, function(err, doc, response) {
                            cb(err, doc);
                        });
                    },
                    "it works": function(err, lists, acts) {
                        assert.ifError(err);
                        assert.isObject(lists);
                    },
                    "it looks correct": function(err, lists, acts) {
                        assert.ifError(err);
                        assertValidList(lists, 100, 20);
                        assert.include(lists, "objectTypes");
                        assert.isArray(lists.objectTypes);
                        assert.include(lists.objectTypes, "collection");
                    }
                }
            },
            "and a user deletes a list": {
                topic: function(cl) {
                    var cb = this.callback,
                        pair = null,
                        cred = null,
                        url = "http://localhost:4815/api/user/maxromeo/feed",
                        list = null;

                    Step(
                        function() {
                            newPair(cl, "maxromeo", "warina", this);
                        },
                        function(err, results) {
                            if (err) throw err;
                            pair = results;
                            cred = makeCred(cl, pair);
                            var act = {
                                verb: "post",
                                object: {
                                    objectType: "collection",
                                    displayName: "Babylonians",
                                    objectTypes: ["person"]
                                }
                            };
                            httputil.postJSON(url, cred, act, this);
                        },
                        function(err, doc, response) {
                            if (err) throw err;
                            list = doc.object;
                            var act = {
                                verb: "delete",
                                object: list
                            };
                            httputil.postJSON(url, cred, act, this);
                        },
                        function(err, doc, response) {
                            cb(err, doc, pair);
                        }
                    );
                },
                "it works": function(err, act) {
                    assert.ifError(err);
                    assertValidActivity(act);
                },
                "and we get the list of lists owned by the user": {
                    topic: function(act, pair, cl) {
                        var cb = this.callback,
                            cred = makeCred(cl, pair),
                            url = "http://localhost:4815/api/user/maxromeo/lists";

                        httputil.getJSON(url, cred, function(err, doc, response) {
                            cb(err, doc);
                        });
                    },
                    "it works": function(err, lists, acts) {
                        assert.ifError(err);
                        assert.isObject(lists);
                    },
                    "it looks correct": function(err, lists, acts) {
                        assert.ifError(err);
                        assertValidList(lists, 0);
                        assert.include(lists, "objectTypes");
                        assert.isArray(lists.objectTypes);
                        assert.include(lists.objectTypes, "collection");
                    }
                }
            }
        }
    }
});

suite["export"](module);

<|endoftext|>


<commit_before>var temp_file = require('../../../files/temp_file');
var StringDecoder = require('string_decoder').StringDecoder;

var BUFFER_SIZE = 64 * 1024;

module.exports = function(master, stream) {
  var listen_handle,
      key_maps,
      current_file,
      waiting_files = [];
  var ending = false;

  var ender = function() {
    ending = true;
    if (listen_handle) {
      master.database.stopListening(listen_handle);
      listen_handle = undefined;
    }
  }

  stream.on('end', ender);
  stream.on('close', ender);

  // Stream existing data on all key maps into a backlog file
  temp_file.open(function(err, backlog_file) {
    //console.log('backlog_file.open returned');

    if (err) {
      master.error(stream, err);
      return;
    }

    var send = function(data) {
      //console.log('sending: ');
      //console.log(data);
      backlog_file.write(JSON.stringify(data) + "\n", function(err, pos, length) {
        if (err) {
          master.error(stream, err);
          return;
        }
        if (!backlog_file._written_bytes) {
          backlog_file._written_bytes = 0;
        }
        backlog_file._written_bytes += length;
        notify(backlog_file, pos, length); // notify sending stream so data gets to the client
      });
    };

    key_maps = master.database.key_map_names.map(function(key_map_name) {
      return {name : key_map_name, key_map: master.database[key_map_name]};
    });

    var pipe_collection_read_into_file = function(key_map_name, callback) {
      return function(err, record) {
        if (err) {
          master.error(stream, err);
          return;
        }
        console.log(record);
        if (record != null) {
          send({m: key_map_name, k: record.k, v: record.v});
        } else {
          callback();
        }
      };
    }

    master.database.meta.collection.read(pipe_collection_read_into_file('meta', function() {

      backlog_file._finished_writing_bytes  = backlog_file._written_bytes;

      console.log('done with meta');
      // done with meta.
      // Now we can pipe all other key_maps
      backlog_file._sync_finished_writing_file_count = 0;
      key_maps.forEach(function(key_map) {
        key_map.key_map.collection.read(pipe_collection_read_into_file(key_map.name, function() {
          console.log('done with ' + key_map.name);
          backlog_file._sync_finished_writing_file_count ++;
          if (backlog_file._sync_finished_writing_file_count == key_maps.length) {
            // We are done with backlog
            // Signal it
            delete backlog_file._sync_finished_writing_file_count;
            console.log('done with backlog');
            backlog_file._sync_finished_writing = true;
          }
        }), true);
      });

    }));

    waiting_files.push(backlog_file);

  });

  // Stream new records into a running file
  temp_file.open(function(err, running_file) {
    console.log('running_file.open returned');

    var send = function(data) {
      running_file.write(JSON.stringify(data) + "\n", function(err) {
        if (err) {
          master.error(stream, err);
          return;
        }
        notify();
      });
    };

    listen_handle = master.database.listenOnAllRegisteredKeyMaps(function(key_map_name, key, value) {
      send({m: key_map_name, k: key, v: value});
    });

    waiting_files.push(running_file);
  });

  // Stream these files downstream to the client

  var string_decoder = new StringDecoder('utf8');

  var notify = function(sending_file, pos, length) {
    console.log('notify');

    var keep_running = false;

    if (waiting_files.length < 1) {
      return;
    }
    if (!sending_file) {
      sending_file = waiting_files[0];
    }

    if (!sending_file._sent_bytes) {
      sending_file._sent_bytes = 0;
    }

    if (!pos && !length) {
      keep_running = true;
    }

    if (!pos) {
      pos = sending_file._sent_bytes;
    }

    if (!length) {
      length = BUFFER_SIZE;
    }

    sending_file.try_read(pos, length, function(err, buffer, bytesRead) {
      if (err) {
        master.error(stream, err);
        return;
      }

      console.log('read ' + bytesRead + ' bytes');

      if (bytesRead > 0) {
        sending_file._sent_bytes += bytesRead;
        stream.write(string_decoder.write(buffer), 'utf8');
        if (keep_running) {
          process.nextTick(function() {
            notify();
          })
        }
      } else {
        if (sending_file._sent_bytes == sending_file._finished_writing_bytes) {
          waiting_files.splice(0, 1);

          process.nextTick(function() {
            notify();
          });

        }
      }
    });
  };

};<commit_msg>replication starting<commit_after>var temp_file = require('../../../files/temp_file');
var StringDecoder = require('string_decoder').StringDecoder;

var BUFFER_SIZE = 64 * 1024;

module.exports = function(master, stream) {
  var listen_handle,
      key_maps,
      current_file,
      waiting_files = [];
  var ending = false;

  var ender = function() {
    ending = true;
    if (listen_handle) {
      master.database.stopListening(listen_handle);
      listen_handle = undefined;
    }
    waiting_files.forEach(function(waiting_file) {
      waiting_file.end(function(err) {
        if (err) {
          master.error(err);
          return;
        }
        waiting_file.destroy(function(err) {
          if (err) {
            master.error(err);
          }
        });
      });
    });
  }

  stream.on('end', ender);
  stream.on('close', ender);

  // Stream existing data on all key maps into a backlog file
  temp_file.open(function(err, backlog_file) {
    //console.log('backlog_file.open returned');

    if (err) {
      master.error(stream, err);
      return;
    }

    var send = function(data) {
      //console.log('sending: ');
      //console.log(data);
      backlog_file.write(JSON.stringify(data) + "\n", function(err, pos, length) {
        if (err) {
          master.error(stream, err);
          return;
        }
        if (!backlog_file._written_bytes) {
          backlog_file._written_bytes = 0;
        }
        backlog_file._written_bytes += length;
        notify(backlog_file, pos, length); // notify sending stream so data gets to the client
      });
    };

    key_maps = master.database.key_map_names.map(function(key_map_name) {
      return {name : key_map_name, key_map: master.database[key_map_name]};
    });

    var pipe_collection_read_into_file = function(key_map_name, callback) {
      return function(err, record) {
        if (err) {
          master.error(stream, err);
          return;
        }
        console.log(record);
        if (record != null) {
          send({m: key_map_name, k: record.k, v: record.v});
        } else {
          callback();
        }
      };
    }

    master.database.meta.collection.read(pipe_collection_read_into_file('meta', function() {

      backlog_file._finished_writing_bytes  = backlog_file._written_bytes;

      console.log('done with meta');
      // done with meta.
      // Now we can pipe all other key_maps
      backlog_file._sync_finished_writing_file_count = 0;
      key_maps.forEach(function(key_map) {
        key_map.key_map.collection.read(pipe_collection_read_into_file(key_map.name, function() {
          console.log('done with ' + key_map.name);
          backlog_file._sync_finished_writing_file_count ++;
          if (backlog_file._sync_finished_writing_file_count == key_maps.length) {
            // We are done with backlog
            // Signal it
            delete backlog_file._sync_finished_writing_file_count;
            console.log('done with backlog');
            backlog_file._sync_finished_writing = true;
          }
        }), true);
      });

    }));

    waiting_files.push(backlog_file);

  });

  // Stream new records into a running file
  temp_file.open(function(err, running_file) {
    console.log('running_file.open returned');

    var send = function(data) {
      running_file.write(JSON.stringify(data) + "\n", function(err) {
        if (err) {
          master.error(stream, err);
          return;
        }
        notify();
      });
    };

    listen_handle = master.database.listenOnAllRegisteredKeyMaps(function(key_map_name, key, value) {
      send({m: key_map_name, k: key, v: value});
    });

    waiting_files.push(running_file);
  });

  // Stream these files downstream to the client

  var string_decoder = new StringDecoder('utf8');

  var notify = function(sending_file, pos, length) {
    console.log('notify');

    var keep_running = false;

    if (waiting_files.length < 1) {
      return;
    }
    if (!sending_file) {
      sending_file = waiting_files[0];
    }

    if (!sending_file._sent_bytes) {
      sending_file._sent_bytes = 0;
    }

    if (!pos && !length) {
      keep_running = true;
    }

    if (!pos) {
      pos = sending_file._sent_bytes;
    }

    if (!length) {
      length = BUFFER_SIZE;
    }

    sending_file.try_read(pos, length, function(err, buffer, bytesRead) {
      if (err) {
        master.error(stream, err);
        return;
      }

      console.log('read ' + bytesRead + ' bytes');

      if (bytesRead > 0) {
        sending_file._sent_bytes += bytesRead;
        stream.write(string_decoder.write(buffer), 'utf8');
        if (keep_running) {
          process.nextTick(function() {
            notify();
          })
        }
      } else {
        if (sending_file._sent_bytes == sending_file._finished_writing_bytes) {
          waiting_files.splice(0, 1);

          process.nextTick(function() {
            notify();
          });

        }
      }
    });
  };

};<|endoftext|>


<commit_before>//接收者
Minder.Receiver = kity.createClass('Receiver',{
    clear : function(){
        this.container.innerHTML = '';
        this.selection && this.selection.setHide();
        this.index = 0;
        return this;
    },
    setTextEditStatus : function(status){
        this.textEditStatus = status || false;
        return this;
    },
    isTextEditStatus:function(){
        return this.textEditStatus;
    },
    constructor : function(km){
        this.setKityMinder(km);
        this.textEditStatus = false;
        var _div = document.createElement('div');
        _div.setAttribute('contenteditable',true);
        _div.className = 'km_receiver';
        this.container = document.body.insertBefore(_div,document.body.firstChild);
        utils.addCssRule('km_receiver_css',' .km_receiver{position:absolute;padding:0;margin:0;word-wrap:break-word;clip:rect(1em 1em 1em 1em);}');//
        this.km.on('beforekeyup', utils.proxy(this.keyboardEvents,this));
        this.timer = null;
        this.index = 0;
    },
    setRange : function(range,index){

        this.index = index || this.index;

        var text = this.container.firstChild;
        this.range = range;
        range.setStart(text || this.container, this.index).collapse(true);
        setTimeout(function(){
            range.select()
        });
        return this;
    },
    setTextShape:function(textShape){
        if(!textShape){
            textShape = new kity.Text();
        }
        this.textShape = textShape;
        this.container.innerHTML = textShape.getContent();
        return this;
    },
    setTextShapeSize:function(size){
        this.textShape.setSize(size);
        return this;
    },
    getTextShapeHeight:function(){
        return this.textShape.getRenderBox().height;
    },
    appendTextShapeToPaper:function(paper){
        paper.addShape(this.textShape);
        return this;
    },
    setKityMinder:function(km){
        this.km = km;
        return this;
    },
    setMinderNode:function(node){
        this.minderNode = node;
        return this;
    },
    keyboardEvents : function(e){

        clearTimeout(this.timer);
        var me = this;
        var orgEvt = e.originEvent;
        var keyCode = orgEvt.keyCode;
        switch(e.type){

            case 'beforekeyup':
                if(this.isTextEditStatus()){
                    switch(keyCode){
                        case keymap.Enter:
                        case keymap.Tab:
                            if(this.keydownNode === this.minderNode){
                                this.setTextEditStatus(false);
                                this.clear();
                            }
                            e.preventDefault();
                            return;
                        case keymap.Shift:
                        case keymap.Control:
                        case keymap.Alt:
                        case keymap.Cmd:
                            return;

                    }
                    var text = (this.container.textContent || this.container.innerText).replace(/\u200b/g,'');

                    this.textShape.setContent(text);
                    this.setContainerStyle();
                    this.minderNode.setText(text);

                    this.km.updateLayout(this.minderNode);
                    this.setBaseOffset();
                    this.updateTextData();
                    this.updateIndex();
                    this.updateSelection();

                    this.timer = setTimeout(function(){
                        me.selection.setShow()
                },500);
                    return true;
                }

        }
    },
    updateIndex:function(){
        this.index = this.range.getStart().startOffset;
    },
    updateTextData : function(){
        this.textShape.textData =  this.getTextOffsetData();
    },
    setSelection : function(selection){
        this.selection = selection;
        return this;
    },
    updateSelection : function(){
        this.selection.setShowHold();
        this.selection.bringTop();
        //更新模拟选区的范围
        this.selection.setStartOffset(this.index).collapse(true);
        if(this.index == this.textData.length){
            if(this.index == 0){
                this.selection.setPosition(this.getBaseOffset())
            }else{
                this.selection.setPosition({
                    x : this.textData[this.index-1].x + this.textData[this.index-1].width,
                    y : this.textData[this.index-1].y
                })
            }

        }else{
            this.selection.setPosition(this.textData[this.index])
        }
        return this;
    },
    getBaseOffset:function(){
        return this.textShape.getRenderBox('top');
    },
    setBaseOffset :function(){
        this.offset = this.textShape.getRenderBox('top');
        return this;
    },
    setContainerStyle : function(){
        var textShapeBox = this.textShape.getRenderBox();

        this.container.style.cssText =  ";left:" + this.offset.x
            + 'px;top:' + (this.offset.y+textShapeBox.height)
            + 'px;width:' + textShapeBox.width
            + 'px;height:' + textShapeBox.height + 'px;';
        return this;
    },
    getTextOffsetData:function(){
        var text = this.textShape.getContent();
        this.textData = [];

        for(var i= 0,l = text.length;i<l;i++){
            var box = this.textShape.getExtentOfChar(i);
            this.textData.push({
                x:box.x + this.offset.x,
                y:this.offset.y,
                width:box.width,
                height:box.height
            })
        }
        return this;
    },
    setCurrentIndex:function(offset){
        var me = this;
        this.getTextOffsetData();
        var hadChanged = false;
        utils.each(this.textData,function(i,v){
            //点击开始之前
            if(i == 0 && offset.x <= v.x){
                me.index = 0;
                return false;
            }

            if(i == me.textData.length -1 && offset.x >= v.x){
                me.index = me.textData.length;
                return false;
            }
            if(offset.x >= v.x && offset.x <= v.x + v.width){
                if(offset.x  - v.x > v.width/2){
                    me.index = i + 1;

                }else {
                    me.index = i

                }
                hadChanged = true;
                return false;
            }
        });
        return this;

    },
    setSelectionHeight:function(){
        this.selection.setHeight(this.getTextShapeHeight());
        return this;
    },

    updateSelectionByMousePosition:function(offset,dir){

        var me = this;
        utils.each(this.textData,function(i,v){
            //点击开始之前
            if(i == 0 && offset.x <= v.x){
                me.selection.setStartOffset(0);
                return false;
            }

            if(i == me.textData.length -1 && offset.x >= v.x){
                me.selection.setEndOffset(me.textData.length);
                return false;
            }
            if(offset.x >= v.x && offset.x <= v.x + v.width){
                if(me.index == i){
                    if(i == 0){
                        me.selection.setStartOffset(i)
                    }
                    me.selection.setEndOffset(i + (dir == 1 ? 1 : 0))
                }else if(i > me.index){
                    me.selection.setStartOffset(me.index);
                    me.selection.setEndOffset(i + (dir == 1 ? 1 : 0))
                }else{
                    me.selection.setStartOffset(i + (dir == 1 ? 1 : 0));
                    me.selection.setEndOffset(me.index)
                }

                return false;
            }
        });
        return this;
    },
    updateSelectionShow:function(){
        var startOffset = this.textData[this.selection.startOffset],
            endOffset = this.textData[this.selection.endOffset],
            width = 0 ;
        if(this.selection.collapsed){

            this.selection.updateShow(startOffset||this.textData[this.textData.length-1],0);
            return this;
        }
        if(!endOffset){
            var lastOffset = this.textData[this.textData.length -1];
            width = lastOffset.x - startOffset.x + lastOffset.width;
        }else{
            width = endOffset.x - startOffset.x;
        }

        this.selection.updateShow(startOffset,width);
        return this;
    },
    updateRange:function(range){
        var node = this.container.firstChild;
        range.setStart(node,this.selection.startOffset);
        range.setEnd(node,this.selection.endOffset);
        range.select();
        return this;
    },
    setIndex:function(index){
        this.index = index;
        return this
    },
    setContainerTxt:function(txt){
        this.container.textContent = txt;
        return this;
    }
});<commit_msg>by zhanyi<commit_after>//接收者
Minder.Receiver = kity.createClass('Receiver',{
    clear : function(){
        this.container.innerHTML = '';
        this.selection && this.selection.setHide();
        this.index = 0;
        return this;
    },
    setTextEditStatus : function(status){
        this.textEditStatus = status || false;
        return this;
    },
    isTextEditStatus:function(){
        return this.textEditStatus;
    },
    constructor : function(km){
        this.setKityMinder(km);
        this.textEditStatus = false;
        var _div = document.createElement('div');
        _div.setAttribute('contenteditable',true);
        _div.className = 'km_receiver';
        this.container = document.body.insertBefore(_div,document.body.firstChild);
        utils.addCssRule('km_receiver_css',' .km_receiver{position:absolute;padding:0;margin:0;word-wrap:break-word;clip:rect(1em 1em 1em 1em);}');//
        this.km.on('beforekeyup', utils.proxy(this.keyboardEvents,this));
        this.timer = null;
        this.index = 0;
    },
    setRange : function(range,index){

        this.index = index || this.index;

        var text = this.container.firstChild;
        this.range = range;
        range.setStart(text || this.container, this.index).collapse(true);
        setTimeout(function(){
            range.select()
        });
        return this;
    },
    setTextShape:function(textShape){
        if(!textShape){
            textShape = new kity.Text();
        }
        this.textShape = textShape;
        this.container.innerHTML = textShape.getContent();
        return this;
    },
    setTextShapeSize:function(size){
        this.textShape.setSize(size);
        return this;
    },
    getTextShapeHeight:function(){
        return this.textShape.getRenderBox().height;
    },
    appendTextShapeToPaper:function(paper){
        paper.addShape(this.textShape);
        return this;
    },
    setKityMinder:function(km){
        this.km = km;
        return this;
    },
    setMinderNode:function(node){
        this.minderNode = node;
        return this;
    },
    keyboardEvents : function(e){

        clearTimeout(this.timer);
        var me = this;
        var orgEvt = e.originEvent;
        var keyCode = orgEvt.keyCode;
        switch(e.type){

            case 'beforekeyup':
                if(this.isTextEditStatus()){
                    switch(keyCode){
                        case keymap.Enter:
                        case keymap.Tab:
                            if(this.keydownNode === this.minderNode){
                                this.setTextEditStatus(false);
                                this.clear();
                            }
                            e.preventDefault();
                            return;
                        case keymap.Shift:
                        case keymap.Control:
                        case keymap.Alt:
                        case keymap.Cmd:
                            return;

                    }
                    var text = (this.container.textContent || this.container.innerText).replace(/\u200b/g,'');

                    if(this.textShape.getOpacity() == 0){
                        this.textShape.setOpacity(1);
                    }
                    this.textShape.setContent(text);
                    this.setContainerStyle();
                    this.minderNode.setText(text);
                    if(text.length == 0){
                        this.textShape.setContent('a');
                        this.textShape.setOpacity(0);
                    }
                    this.km.updateLayout(this.minderNode);
                    this.setBaseOffset();
                    this.updateTextData();
                    this.updateIndex();
                    this.updateSelection();

                    this.timer = setTimeout(function(){
                        me.selection.setShow()
                },500);
                    return true;
                }

        }
    },
    updateIndex:function(){
        this.index = this.range.getStart().startOffset;
    },
    updateTextData : function(){
        this.textShape.textData =  this.getTextOffsetData();
    },
    setSelection : function(selection){
        this.selection = selection;
        return this;
    },
    updateSelection : function(){
        this.selection.setShowHold();
        this.selection.bringTop();
        //更新模拟选区的范围
        this.selection.setStartOffset(this.index).collapse(true);
        if(this.index == this.textData.length){
            if(this.index == 0){
                this.selection.setPosition(this.getBaseOffset())
            }else{
                this.selection.setPosition({
                    x : this.textData[this.index-1].x + this.textData[this.index-1].width,
                    y : this.textData[this.index-1].y
                })
            }

        }else{
            this.selection.setPosition(this.textData[this.index])
        }
        return this;
    },
    getBaseOffset:function(){
        return this.textShape.getRenderBox('top');
    },
    setBaseOffset :function(){
        this.offset = this.textShape.getRenderBox('top');
        return this;
    },
    setContainerStyle : function(){
        var textShapeBox = this.textShape.getRenderBox();

        this.container.style.cssText =  ";left:" + this.offset.x
            + 'px;top:' + (this.offset.y+textShapeBox.height)
            + 'px;width:' + textShapeBox.width
            + 'px;height:' + textShapeBox.height + 'px;';
        return this;
    },
    getTextOffsetData:function(){
        var text = this.textShape.getContent();
        this.textData = [];

        for(var i= 0,l = text.length;i<l;i++){
            var box = this.textShape.getExtentOfChar(i);
            this.textData.push({
                x:box.x + this.offset.x,
                y:this.offset.y,
                width:box.width,
                height:box.height
            })
        }
        return this;
    },
    setCurrentIndex:function(offset){
        var me = this;
        this.getTextOffsetData();
        var hadChanged = false;
        utils.each(this.textData,function(i,v){
            //点击开始之前
            if(i == 0 && offset.x <= v.x){
                me.index = 0;
                return false;
            }

            if(i == me.textData.length -1 && offset.x >= v.x){
                me.index = me.textData.length;
                return false;
            }
            if(offset.x >= v.x && offset.x <= v.x + v.width){
                if(offset.x  - v.x > v.width/2){
                    me.index = i + 1;

                }else {
                    me.index = i

                }
                hadChanged = true;
                return false;
            }
        });
        return this;

    },
    setSelectionHeight:function(){
        this.selection.setHeight(this.getTextShapeHeight());
        return this;
    },

    updateSelectionByMousePosition:function(offset,dir){

        var me = this;
        utils.each(this.textData,function(i,v){
            //点击开始之前
            if(i == 0 && offset.x <= v.x){
                me.selection.setStartOffset(0);
                return false;
            }

            if(i == me.textData.length -1 && offset.x >= v.x){
                me.selection.setEndOffset(me.textData.length);
                return false;
            }
            if(offset.x >= v.x && offset.x <= v.x + v.width){
                if(me.index == i){
                    if(i == 0){
                        me.selection.setStartOffset(i)
                    }
                    me.selection.setEndOffset(i + (dir == 1 ? 1 : 0))
                }else if(i > me.index){
                    me.selection.setStartOffset(me.index);
                    me.selection.setEndOffset(i + (dir == 1 ? 1 : 0))
                }else{
                    me.selection.setStartOffset(i + (dir == 1 ? 1 : 0));
                    me.selection.setEndOffset(me.index)
                }

                return false;
            }
        });
        return this;
    },
    updateSelectionShow:function(){
        var startOffset = this.textData[this.selection.startOffset],
            endOffset = this.textData[this.selection.endOffset],
            width = 0 ;
        if(this.selection.collapsed){

            this.selection.updateShow(startOffset||this.textData[this.textData.length-1],0);
            return this;
        }
        if(!endOffset){
            var lastOffset = this.textData[this.textData.length -1];
            width = lastOffset.x - startOffset.x + lastOffset.width;
        }else{
            width = endOffset.x - startOffset.x;
        }

        this.selection.updateShow(startOffset,width);
        return this;
    },
    updateRange:function(range){
        var node = this.container.firstChild;
        range.setStart(node,this.selection.startOffset);
        range.setEnd(node,this.selection.endOffset);
        range.select();
        return this;
    },
    setIndex:function(index){
        this.index = index;
        return this
    },
    setContainerTxt:function(txt){
        this.container.textContent = txt;
        return this;
    }
});<|endoftext|>