import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/tap:i18n';
import { HTTP } from 'meteor/http';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks';
import { Users, Rooms, Messages, Subscriptions, Settings, LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, LivechatVisitors } from '../../../models';
import { Logger } from '../../../logger';
import { sendMessage, deleteMessage, updateMessage } from '../../../lib';
import { addUserRoles, removeUserFromRoles } from '../../../authorization';
import _ from 'underscore';
import s from 'underscore.string';
import moment from 'moment';
import dns from 'dns';
import UAParser from 'ua-parser-js';
import * as Mailer from '../../../mailer';
import { LivechatInquiry } from '../../lib/LivechatInquiry';
import { QueueMethods } from './QueueMethods';
import { Analytics } from './Analytics';

export const Livechat = {
	Analytics,
	historyMonitorType: 'url',

	logger: new Logger('Livechat', {
		sections: {
			webhook: 'Webhook',
		},
	}),

	getNextAgent(department) {
		if (settings.get('Livechat_Routing_Method') === 'External') {
			for (let i = 0; i < 10; i++) {
				try {
					const queryString = department ? `?departmentId=${ department }` : '';
					const result = HTTP.call('GET', `${ settings.get('Livechat_External_Queue_URL') }${ queryString }`, {
						headers: {
							'User-Agent': 'RocketChat Server',
							Accept: 'application/json',
							'X-RocketChat-Secret-Token': settings.get('Livechat_External_Queue_Token'),
						},
					});

					if (result && result.data && result.data.username) {
						const agent = Users.findOneOnlineAgentByUsername(result.data.username);

						if (agent) {
							return {
								agentId: agent._id,
								username: agent.username,
							};
						}
					}
				} catch (e) {
					console.error('Error requesting agent from external queue.', e);
					break;
				}
			}
			throw new Meteor.Error('no-agent-online', 'Sorry, no online agents');
		} else if (department) {
			return LivechatDepartmentAgents.getNextAgentForDepartment(department);
		}
		return Users.getNextAgent();
	},
	getAgents(department) {
		if (department) {
			return LivechatDepartmentAgents.findByDepartmentId(department);
		}
		return Users.findAgents();
	},
	getOnlineAgents(department) {
		if (department) {
			return LivechatDepartmentAgents.getOnlineForDepartment(department);
		}
		return Users.findOnlineAgents();
	},
	getRequiredDepartment(onlineRequired = true) {
		const departments = LivechatDepartment.findEnabledWithAgents();

		return departments.fetch().find((dept) => {
			if (!dept.showOnRegistration) {
				return false;
			}
			if (!onlineRequired) {
				return true;
			}
			const onlineAgents = LivechatDepartmentAgents.getOnlineForDepartment(dept._id);
			return onlineAgents && onlineAgents.count() > 0;
		});
	},
	getRoom(guest, message, roomInfo, agent) {
		let room = Rooms.findOneById(message.rid);
		let newRoom = false;

		if (room && !room.open) {
			message.rid = Random.id();
			room = null;
		}

		if (room == null) {
			// if no department selected verify if there is at least one active and pick the first
			if (!agent && !guest.department) {
				const department = this.getRequiredDepartment();

				if (department) {
					guest.department = department._id;
				}
			}

			// delegate room creation to QueueMethods
			const routingMethod = settings.get('Livechat_Routing_Method');
			room = QueueMethods[routingMethod](guest, message, roomInfo, agent);

			newRoom = true;
		}

		if (!room || room.v.token !== guest.token) {
			throw new Meteor.Error('cannot-access-room');
		}

		if (newRoom) {
			Messages.setRoomIdByToken(guest.token, room._id);
		}

		return { room, newRoom };
	},

	sendMessage({ guest, message, roomInfo, agent }) {
		const { room, newRoom } = this.getRoom(guest, message, roomInfo, agent);
		if (guest.name) {
			message.alias = guest.name;
		}

		// return messages;
		return _.extend(sendMessage(guest, message, room), { newRoom, showConnecting: this.showConnecting() });
	},

	updateMessage({ guest, message }) {
		check(message, Match.ObjectIncluding({ _id: String }));

		const originalMessage = Messages.findOneById(message._id);
		if (!originalMessage || !originalMessage._id) {
			return;
		}

		const editAllowed = settings.get('Message_AllowEditing');
		const editOwn = originalMessage.u && originalMessage.u._id === guest._id;

		if (!editAllowed || !editOwn) {
			throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', { method: 'livechatUpdateMessage' });
		}

		updateMessage(message, guest);

		return true;
	},

	deleteMessage({ guest, message }) {
		check(message, Match.ObjectIncluding({ _id: String }));

		const msg = Messages.findOneById(message._id);
		if (!msg || !msg._id) {
			return;
		}

		const deleteAllowed = settings.get('Message_AllowDeleting');
		const editOwn = msg.u && msg.u._id === guest._id;

		if (!deleteAllowed || !editOwn) {
			throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { method: 'livechatDeleteMessage' });
		}

		deleteMessage(message, guest);

		return true;
	},

	registerGuest({ token, name, email, department, phone, username } = {}) {
		check(token, String);

		let userId;
		const updateUser = {
			$set: {
				token,
			},
		};

		const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });

		if (user) {
			userId = user._id;
		} else {
			if (!username) {
				username = LivechatVisitors.getNextVisitorUsername();
			}

			let existingUser = null;

			if (s.trim(email) !== '' && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(email))) {
				userId = existingUser._id;
			} else {
				const userData = {
					username,
				};

				const storeHttpHeaderData = settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations');

				if (this.connection && storeHttpHeaderData) {
					userData.userAgent = this.connection.httpHeaders['user-agent'];
					userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress;
					userData.host = this.connection.httpHeaders.host;
				}

				userId = LivechatVisitors.insert(userData);
			}
		}

		if (phone) {
			updateUser.$set.phone = [
				{ phoneNumber: phone.number },
			];
		}

		if (email && email.trim() !== '') {
			updateUser.$set.visitorEmails = [
				{ address: email },
			];
		}

		if (name) {
			updateUser.$set.name = name;
		}

		if (!department) {
			Object.assign(updateUser, { $unset: { department: 1 } });
		} else {
			const dep = LivechatDepartment.findOneByIdOrName(department);
			updateUser.$set.department = dep && dep._id;
		}

		LivechatVisitors.updateById(userId, updateUser);

		return userId;
	},

	setDepartmentForGuest({ token, department } = {}) {
		check(token, String);

		const updateUser = {
			$set: {
				department,
			},
		};

		const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } });
		if (user) {
			return LivechatVisitors.updateById(user._id, updateUser);
		}
		return false;
	},

	saveGuest({ _id, name, email, phone }) {
		const updateData = {};

		if (name) {
			updateData.name = name;
		}
		if (email) {
			updateData.email = email;
		}
		if (phone) {
			updateData.phone = phone;
		}
		const ret = LivechatVisitors.saveGuestById(_id, updateData);

		Meteor.defer(() => {
			callbacks.run('livechat.saveGuest', updateData);
		});

		return ret;
	},

	closeRoom({ user, visitor, room, comment }) {
		if (!room || room.t !== 'l' || !room.open) {
			return false;
		}

		const now = new Date();

		const closeData = {
			closedAt: now,
			chatDuration: (now.getTime() - room.ts) / 1000,
		};

		if (user) {
			closeData.closer = 'user';
			closeData.closedBy = {
				_id: user._id,
				username: user.username,
			};
		} else if (visitor) {
			closeData.closer = 'visitor';
			closeData.closedBy = {
				_id: visitor._id,
				username: visitor.username,
			};
		}

		Rooms.closeByRoomId(room._id, closeData);
		LivechatInquiry.closeByRoomId(room._id, closeData);

		const message = {
			t: 'livechat-close',
			msg: comment,
			groupable: false,
		};

		// Retreive the closed room
		room = Rooms.findOneByIdOrName(room._id);

		sendMessage(user, message, room);

		if (room.servedBy) {
			Subscriptions.hideByRoomIdAndUserId(room._id, room.servedBy._id);
		}
		Messages.createCommandWithRoomIdAndUser('promptTranscript', room._id, closeData.closedBy);

		Meteor.defer(() => {
			callbacks.run('livechat.closeRoom', room);
		});

		return true;
	},

	setCustomFields({ token, key, value, overwrite } = {}) {
		check(token, String);
		check(key, String);
		check(value, String);
		check(overwrite, Boolean);

		const customField = LivechatCustomField.findOneById(key);
		if (!customField) {
			throw new Meteor.Error('invalid-custom-field');
		}

		if (customField.scope === 'room') {
			return Rooms.updateLivechatDataByToken(token, key, value, overwrite);
		} else {
			return LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite);
		}
	},

	getInitSettings() {
		const rcSettings = {};

		Settings.findNotHiddenPublic([
			'Livechat_title',
			'Livechat_title_color',
			'Livechat_enabled',
			'Livechat_registration_form',
			'Livechat_allow_switching_departments',
			'Livechat_offline_title',
			'Livechat_offline_title_color',
			'Livechat_offline_message',
			'Livechat_offline_success_message',
			'Livechat_offline_form_unavailable',
			'Livechat_display_offline_form',
			'Livechat_videocall_enabled',
			'Jitsi_Enabled',
			'Language',
			'Livechat_enable_transcript',
			'Livechat_transcript_message',
			'Livechat_fileupload_enabled',
			'FileUpload_Enabled',
			'Livechat_conversation_finished_message',
			'Livechat_name_field_registration_form',
			'Livechat_email_field_registration_form',
			'Livechat_registration_form_message',
			'Livechat_force_accept_data_processing_consent',
			'Livechat_data_processing_consent_text',
		]).forEach((setting) => {
			rcSettings[setting._id] = setting.value;
		});

		settings.get('Livechat_history_monitor_type', (key, value) => {
			rcSettings[key] = value;
		});

		rcSettings.Livechat_Show_Connecting = this.showConnecting();

		return rcSettings;
	},

	saveRoomInfo(roomData, guestData) {
		if ((roomData.topic != null || roomData.tags != null) && !Rooms.setTopicAndTagsById(roomData._id, roomData.topic, roomData.tags)) {
			return false;
		}

		Meteor.defer(() => {
			callbacks.run('livechat.saveRoom', roomData);
		});

		if (!_.isEmpty(guestData.name)) {
			return Rooms.setNameById(roomData._id, guestData.name, guestData.name) && Subscriptions.updateDisplayNameByRoomId(roomData._id, guestData.name);
		}
	},

	closeOpenChats(userId, comment) {
		const user = Users.findOneById(userId);
		Rooms.findOpenByAgent(userId).forEach((room) => {
			this.closeRoom({ user, room, comment });
		});
	},

	forwardOpenChats(userId) {
		Rooms.findOpenByAgent(userId).forEach((room) => {
			const guest = LivechatVisitors.findOneById(room.v._id);
			this.transfer(room, guest, { departmentId: guest.department });
		});
	},

	savePageHistory(token, roomId, pageInfo) {
		if (pageInfo.change === Livechat.historyMonitorType) {

			const user = Users.findOneById('rocket.cat');

			const pageTitle = pageInfo.title;
			const pageUrl = pageInfo.location.href;
			const extraData = {
				navigation: {
					page: pageInfo,
					token,
				},
			};

			if (!roomId) {
				// keep history of unregistered visitors for 1 month
				const keepHistoryMiliseconds = 2592000000;
				extraData.expireAt = new Date().getTime() + keepHistoryMiliseconds;
			}

			if (!settings.get('Livechat_Visitor_navigation_as_a_message')) {
				extraData._hidden = true;
			}

			return Messages.createNavigationHistoryWithRoomIdMessageAndUser(roomId, `${ pageTitle } - ${ pageUrl }`, user, extraData);
		}

		return;
	},

	transfer(room, guest, transferData) {
		let agent;

		if (transferData.userId) {
			const user = Users.findOneOnlineAgentById(transferData.userId);
			if (!user) {
				return false;
			}

			const { _id: agentId, username } = user;
			agent = Object.assign({}, { agentId, username });
		} else if (settings.get('Livechat_Routing_Method') !== 'Guest_Pool') {
			agent = Livechat.getNextAgent(transferData.departmentId);
		} else {
			return Livechat.returnRoomAsInquiry(room._id, transferData.departmentId);
		}

		const { servedBy } = room;

		if (agent && agent.agentId !== servedBy._id) {
			Rooms.changeAgentByRoomId(room._id, agent);

			if (transferData.departmentId) {
				Rooms.changeDepartmentIdByRoomId(room._id, transferData.departmentId);
			}

			const subscriptionData = {
				rid: room._id,
				name: guest.name || guest.username,
				alert: true,
				open: true,
				unread: 1,
				userMentions: 1,
				groupMentions: 0,
				u: {
					_id: agent.agentId,
					username: agent.username,
				},
				t: 'l',
				desktopNotifications: 'all',
				mobilePushNotifications: 'all',
				emailNotifications: 'all',
			};
			Subscriptions.removeByRoomIdAndUserId(room._id, servedBy._id);

			Subscriptions.insert(subscriptionData);
			Rooms.incUsersCountById(room._id);

			Messages.createUserLeaveWithRoomIdAndUser(room._id, { _id: servedBy._id, username: servedBy.username });
			Messages.createUserJoinWithRoomIdAndUser(room._id, { _id: agent.agentId, username: agent.username });

			const guestData = {
				token: guest.token,
				department: transferData.departmentId,
			};

			this.setDepartmentForGuest(guestData);
			const data = Users.getAgentInfo(agent.agentId);

			Livechat.stream.emit(room._id, {
				type: 'agentData',
				data,
			});

			return true;
		}

		return false;
	},

	returnRoomAsInquiry(rid, departmentId) {
		const room = Rooms.findOneById(rid);
		if (!room) {
			throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'livechat:returnRoomAsInquiry' });
		}

		if (!room.servedBy) {
			return false;
		}

		const user = Users.findOne(room.servedBy._id);
		if (!user || !user._id) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:returnRoomAsInquiry' });
		}

		const agentIds = [];
		// get the agents of the department
		if (departmentId) {
			const agents = Livechat.getAgents(departmentId);
			if (!agents || agents.count() === 0) {
				return false;
			}

			agents.forEach((agent) => {
				agentIds.push(agent.agentId);
			});

			Rooms.changeDepartmentIdByRoomId(room._id, departmentId);
		}

		// delete agent and room subscription
		Subscriptions.removeByRoomId(rid);

		// remove agent from room
		Rooms.removeAgentByRoomId(rid);

		// find inquiry corresponding to room
		const inquiry = LivechatInquiry.findOne({ rid });
		if (!inquiry) {
			return false;
		}

		let openInq;
		// mark inquiry as open
		if (agentIds.length === 0) {
			openInq = LivechatInquiry.openInquiry(inquiry._id);
		} else {
			openInq = LivechatInquiry.openInquiryWithAgents(inquiry._id, agentIds);
		}

		if (openInq) {
			Messages.createUserLeaveWithRoomIdAndUser(rid, { _id: room.servedBy._id, username: room.servedBy.username });

			Livechat.stream.emit(rid, {
				type: 'agentData',
				data: null,
			});
		}

		return openInq;
	},

	sendRequest(postData, callback, trying = 1) {
		try {
			const options = {
				headers: {
					'X-RocketChat-Livechat-Token': settings.get('Livechat_secret_token'),
				},
				data: postData,
			};
			return HTTP.post(settings.get('Livechat_webhookUrl'), options);
		} catch (e) {
			Livechat.logger.webhook.error(`Response error on ${ trying } try ->`, e);
			// try 10 times after 10 seconds each
			if (trying < 10) {
				Livechat.logger.webhook.warn('Will try again in 10 seconds ...');
				trying++;
				setTimeout(Meteor.bindEnvironment(() => {
					Livechat.sendRequest(postData, callback, trying);
				}), 10000);
			}
		}
	},

	getLivechatRoomGuestInfo(room) {
		const visitor = LivechatVisitors.findOneById(room.v._id);
		const agent = Users.findOneById(room.servedBy && room.servedBy._id);

		const ua = new UAParser();
		ua.setUA(visitor.userAgent);

		const postData = {
			_id: room._id,
			label: room.fname || room.label, // using same field for compatibility
			topic: room.topic,
			createdAt: room.ts,
			lastMessageAt: room.lm,
			tags: room.tags,
			customFields: room.livechatData,
			visitor: {
				_id: visitor._id,
				token: visitor.token,
				name: visitor.name,
				username: visitor.username,
				email: null,
				phone: null,
				department: visitor.department,
				ip: visitor.ip,
				os: ua.getOS().name && (`${ ua.getOS().name } ${ ua.getOS().version }`),
				browser: ua.getBrowser().name && (`${ ua.getBrowser().name } ${ ua.getBrowser().version }`),
				customFields: visitor.livechatData,
			},
		};

		if (agent) {
			postData.agent = {
				_id: agent._id,
				username: agent.username,
				name: agent.name,
				email: null,
			};

			if (agent.emails && agent.emails.length > 0) {
				postData.agent.email = agent.emails[0].address;
			}
		}

		if (room.crmData) {
			postData.crmData = room.crmData;
		}

		if (visitor.visitorEmails && visitor.visitorEmails.length > 0) {
			postData.visitor.email = visitor.visitorEmails;
		}
		if (visitor.phone && visitor.phone.length > 0) {
			postData.visitor.phone = visitor.phone;
		}

		return postData;
	},

	addAgent(username) {
		check(username, String);

		const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addAgent' });
		}

		if (addUserRoles(user._id, 'livechat-agent')) {
			Users.setOperator(user._id, true);
			Users.setLivechatStatus(user._id, 'available');
			return user;
		}

		return false;
	},

	addManager(username) {
		check(username, String);

		const user = Users.findOneByUsername(username, { fields: { _id: 1, username: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:addManager' });
		}

		if (addUserRoles(user._id, 'livechat-manager')) {
			return user;
		}

		return false;
	},

	removeAgent(username) {
		check(username, String);

		const user = Users.findOneByUsername(username, { fields: { _id: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeAgent' });
		}

		if (removeUserFromRoles(user._id, 'livechat-agent')) {
			Users.setOperator(user._id, false);
			Users.setLivechatStatus(user._id, 'not-available');
			return true;
		}

		return false;
	},

	removeManager(username) {
		check(username, String);

		const user = Users.findOneByUsername(username, { fields: { _id: 1 } });

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'livechat:removeManager' });
		}

		return removeUserFromRoles(user._id, 'livechat-manager');
	},

	removeGuest(_id) {
		check(_id, String);

		const guest = LivechatVisitors.findById(_id);
		if (!guest) {
			throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest' });
		}

		this.cleanGuestHistory(_id);
		return LivechatVisitors.removeById(_id);
	},

	cleanGuestHistory(_id) {
		const guest = LivechatVisitors.findById(_id);
		if (!guest) {
			throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:cleanGuestHistory' });
		}

		const { token } = guest;

		Rooms.findByVisitorToken(token).forEach((room) => {
			Messages.removeFilesByRoomId(room._id);
			Messages.removeByRoomId(room._id);
		});

		Subscriptions.removeByVisitorToken(token);
		Rooms.removeByVisitorToken(token);
	},

	saveDepartment(_id, departmentData, departmentAgents) {
		check(_id, Match.Maybe(String));

		check(departmentData, {
			enabled: Boolean,
			name: String,
			description: Match.Optional(String),
			showOnRegistration: Boolean,
			email: String,
			showOnOfflineForm: Boolean,
		});

		check(departmentAgents, [
			Match.ObjectIncluding({
				agentId: String,
				username: String,
			}),
		]);

		if (_id) {
			const department = LivechatDepartment.findOneById(_id);
			if (!department) {
				throw new Meteor.Error('error-department-not-found', 'Department not found', { method: 'livechat:saveDepartment' });
			}
		}

		return LivechatDepartment.createOrUpdateDepartment(_id, departmentData, departmentAgents);
	},

	removeDepartment(_id) {
		check(_id, String);

		const department = LivechatDepartment.findOneById(_id, { fields: { _id: 1 } });

		if (!department) {
			throw new Meteor.Error('department-not-found', 'Department not found', { method: 'livechat:removeDepartment' });
		}

		return LivechatDepartment.removeById(_id);
	},

	showConnecting() {
		if (settings.get('Livechat_Routing_Method') === 'Guest_Pool') {
			return settings.get('Livechat_open_inquiery_show_connecting');
		} else {
			return false;
		}
	},

	sendEmail(from, to, replyTo, subject, html) {
		Mailer.send({
			to,
			from,
			replyTo,
			subject,
			html,
		});
	},

	sendTranscript({ token, rid, email }) {
		check(rid, String);
		check(email, String);

		const room = Rooms.findOneById(rid);

		const visitor = LivechatVisitors.getVisitorByToken(token);
		const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en';

		// allow to only user to send transcripts from their own chats
		if (!room || room.t !== 'l' || !room.v || room.v.token !== token) {
			throw new Meteor.Error('error-invalid-room', 'Invalid room');
		}

		const messages = Messages.findVisibleByRoomIdNotContainingTypes(rid, ['livechat_navigation_history'], { sort: { ts: 1 } });

		let html = '<div> <hr>';
		messages.forEach((message) => {
			if (message.t && ['command', 'livechat-close', 'livechat_video_call'].indexOf(message.t) !== -1) {
				return;
			}

			let author;
			if (message.u._id === visitor._id) {
				author = TAPi18n.__('You', { lng: userLanguage });
			} else {
				author = message.u.username;
			}

			const datetime = moment(message.ts).locale(userLanguage).format('LLL');
			const singleMessage = `
				<p><strong>${ author }</strong>  <em>${ datetime }</em></p>
				<p>${ message.msg }</p>
			`;
			html = html + singleMessage;
		});

		html = `${ html }</div>`;

		let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);

		if (fromEmail) {
			fromEmail = fromEmail[0];
		} else {
			fromEmail = settings.get('From_Email');
		}

		const subject = TAPi18n.__('Transcript_of_your_livechat_conversation', { lng: userLanguage });

		this.sendEmail(fromEmail, email, fromEmail, subject, html);

		Meteor.defer(() => {
			callbacks.run('livechat.sendTranscript', messages, email);
		});

		return true;
	},

	notifyGuestStatusChanged(token, status) {
		LivechatInquiry.updateVisitorStatus(token, status);
		Rooms.updateVisitorStatus(token, status);
	},

	sendOfflineMessage(data = {}) {
		if (!settings.get('Livechat_display_offline_form')) {
			return false;
		}

		const message = (`${ data.message }`).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>' + '$2');

		const html = `
			<h1>New livechat message</h1>
			<p><strong>Visitor name:</strong> ${ data.name }</p>
			<p><strong>Visitor email:</strong> ${ data.email }</p>
			<p><strong>Message:</strong><br>${ message }</p>`;

		let fromEmail = settings.get('From_Email').match(/\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}\b/i);

		if (fromEmail) {
			fromEmail = fromEmail[0];
		} else {
			fromEmail = settings.get('From_Email');
		}

		if (settings.get('Livechat_validate_offline_email')) {
			const emailDomain = data.email.substr(data.email.lastIndexOf('@') + 1);

			try {
				Meteor.wrapAsync(dns.resolveMx)(emailDomain);
			} catch (e) {
				throw new Meteor.Error('error-invalid-email-address', 'Invalid email address', { method: 'livechat:sendOfflineMessage' });
			}
		}

		let emailTo = settings.get('Livechat_offline_email');
		if (data.department) {
			const dep = LivechatDepartment.findOneByIdOrName(data.department);
			emailTo = dep.email || emailTo;
		}

		const from = `${ data.name } - ${ data.email } <${ fromEmail }>`;
		const replyTo = `${ data.name } <${ data.email }>`;
		const subject = `Livechat offline message from ${ data.name }: ${ (`${ data.message }`).substring(0, 20) }`;

		this.sendEmail(from, emailTo, replyTo, subject, html);

		Meteor.defer(() => {
			callbacks.run('livechat.offlineMessage', data);
		});

		return true;
	},

	notifyAgentStatusChanged(userId, status) {
		Rooms.findOpenByAgent(userId).forEach((room) => {
			Livechat.stream.emit(room._id, {
				type: 'agentStatus',
				status,
			});
		});
	},
};

Livechat.stream = new Meteor.Streamer('livechat-room');

Livechat.stream.allowRead((roomId, extraData) => {
	const room = Rooms.findOneById(roomId);

	if (!room) {
		console.warn(`Invalid eventName: "${ roomId }"`);
		return false;
	}

	if (room.t === 'l' && extraData && extraData.visitorToken && room.v.token === extraData.visitorToken) {
		return true;
	}
	return false;
});

settings.get('Livechat_history_monitor_type', (key, value) => {
	Livechat.historyMonitorType = value;
});


您確定要結束聊天並更換部門嗎？

請花一點時間來協助完成本次聊天的小問卷

根據您的滿意度回答題目，1表示完全不滿意，5表示完全滿意。

感謝！我們會盡快回覆您。

我們現在不在線。請留言。

離線中，造成您的不便請見諒。


import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { settings } from '../../../settings';

Template.cmsPage.onCreated(function() {
	this.page = new ReactiveVar('');
	return Tracker.autorun(() => {
		const cmsPage = Session.get('cmsPage');
		if (cmsPage != null) {
			return this.page.set(settings.get(cmsPage));
		}
	});
});

Template.cmsPage.helpers({
	page() {
		return Template.instance().page.get();
	},
});

Template.cmsPage.events({
	'click .cms-page-close'() {
		return FlowRouter.go('/');
	},
});

Template.cmsPage.onRendered(function() {
	return $('#initial-page-loading').remove();
});


import { Migrations } from '../../../app/migrations';
import { Rooms, Subscriptions, Messages, Users } from '../../../app/models';

Migrations.add({
	version: 5,
	up() {
		console.log('Dropping test rooms with less than 2 messages');

		Rooms.find({
			msgs: {
				$lt: 2,
			},
		}).forEach((room) => {
			console.log('Dropped: ', room.name);
			Rooms.removeById(room._id);
			Messages.removeByRoomId(room._id);
			return Subscriptions.removeByRoomId(room._id);
		});

		console.log('Dropping test rooms with less than 2 user');
		Rooms.find({
			usernames: {
				$size: 1,
			},
		}).forEach((room) => {
			console.log('Dropped: ', room.name);
			Rooms.removeById(room._id);
			Messages.removeByRoomId(room._id);
			return Subscriptions.removeByRoomId(room._id);
		});

		console.log('Adding username to all users');
		Users.find({
			username: {
				$exists: 0,
			},
			emails: {
				$exists: 1,
			},
		}).forEach((user) => {
			let newUserName = user.emails[0].address.split('@')[0];
			if (Users.findOneByUsername(newUserName)) {
				newUserName = newUserName + Math.floor((Math.random() * 10) + 1);
				if (Users.findOneByUsername(newUserName)) {
					newUserName = newUserName + Math.floor((Math.random() * 10) + 1);
					if (Users.findOneByUsername(newUserName)) {
						newUserName = newUserName + Math.floor((Math.random() * 10) + 1);
					}
				}
			}
			console.log(`Adding: username ${ newUserName } to all user ${ user._id }`);
			return Users.setUsername(user._id, newUserName);
		});

		console.log('Fixing _id of direct messages rooms');
		Rooms.findByType('d').forEach(function(room) {
			let newId = '';
			const id0 = Users.findOneByUsername(room.usernames[0])._id;
			const id1 = Users.findOneByUsername(room.usernames[1])._id;
			const ids = [id0, id1];

			newId = ids.sort().join('');
			if (newId !== room._id) {
				console.log(`Fixing: _id ${ room._id } to ${ newId }`);
				Subscriptions.update({
					rid: room._id,
				}, {
					$set: {
						rid: newId,
					},
				}, {
					multi: 1,
				});
				Messages.update({
					rid: room._id,
				}, {
					$set: {
						rid: newId,
					},
				}, {
					multi: 1,
				});
				Rooms.removeById(room._id);
				room._id = newId;
				Rooms.insert(room);
			}

			Subscriptions.update({
				rid: room._id,
				'u._id': id0,
			}, {
				$set: {
					name: room.usernames[1],
				},
			});

			return Subscriptions.update({
				rid: room._id,
				'u._id': id1,
			}, {
				$set: {
					name: room.usernames[0],
				},
			});
		});

		console.log('Adding u.username to all documents');
		Users.find({}, {
			username: 1,
		}).forEach((user) => {
			console.log(`Adding: u.username ${ user.username } to all document`);
			Rooms.update({
				'u._id': user._id,
			}, {
				$set: {
					'u.username': user.username,
				},
			}, {
				multi: 1,
			});

			Subscriptions.update({
				'u._id': user._id,
			}, {
				$set: {
					'u.username': user.username,
				},
			}, {
				multi: 1,
			});

			Messages.update({
				'u._id': user._id,
			}, {
				$set: {
					'u.username': user.username,
				},
			}, {
				multi: 1,
			});

			Messages.update({
				uid: user._id,
			}, {
				$set: {
					u: user,
				},
			}, {
				multi: 1,
			});

			Messages.update({
				by: user._id,
			}, {
				$set: {
					u: user,
				},
			}, {
				multi: 1,
			});

			Messages.update({
				uid: {
					$exists: 1,
				},
			}, {
				$unset: {
					uid: 1,
					by: 1,
				},
			}, {
				multi: 1,
			});
		});

		return console.log('End');
	},
});


if (this.connection && storeHttpHeaderData) {

userData.ip = this.connection.httpHeaders['x-real-ip'] || this.connection.httpHeaders['x-forwarded-for'] || this.connection.clientAddress;

keep replace replace replace replace keep keep keep

const connection = this.connection || connectionData;

if (connection && connection.httpHeaders) {

userData.ip = connection.httpHeaders['x-real-ip'] || connection.httpHeaders['x-forwarded-for'] || connection.clientAddress;

import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Rooms, LivechatVisitors, LivechatCustomField } from '../../../../models';
import { hasPermission } from '../../../../authorization';
import { API } from '../../../../api';
import { findGuest } from '../lib/livechat';
import { Livechat } from '../../lib/Livechat';

API.v1.addRoute('livechat/visitor', {
	post() {
		try {
			check(this.bodyParams, {
				visitor: Match.ObjectIncluding({
					token: String,
					name: Match.Maybe(String),
					email: Match.Maybe(String),
					department: Match.Maybe(String),
					phone: Match.Maybe(String),
					username: Match.Maybe(String),
					customFields: Match.Maybe([
						Match.ObjectIncluding({
							key: String,
							value: String,
							overwrite: Boolean,
						}),
					]),
				}),
			});

			const { token, customFields } = this.bodyParams.visitor;
			const guest = this.bodyParams.visitor;

			if (this.bodyParams.visitor.phone) {
				guest.phone = { number: this.bodyParams.visitor.phone };
			}

			const visitorId = Livechat.registerGuest(guest);

			let visitor = LivechatVisitors.getVisitorByToken(token);
			// If it's updating an existing visitor, it must also update the roomInfo
			const cursor = Rooms.findOpenByVisitorToken(token);
			cursor.forEach((room) => {
				Livechat.saveRoomInfo(room, visitor);
			});

			if (customFields && customFields instanceof Array) {
				customFields.forEach((field) => {
					const customField = LivechatCustomField.findOneById(field.key);
					if (!customField) {
						return;
					}
					const { key, value, overwrite } = field;
					if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) {
						return API.v1.failure();
					}
				});
			}

			visitor = LivechatVisitors.findOneById(visitorId);
			return API.v1.success({ visitor });
		} catch (e) {
			return API.v1.failure(e);
		}
	},
});

API.v1.addRoute('livechat/visitor/:token', {
	get() {
		try {
			check(this.urlParams, {
				token: String,
			});

			const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
			return API.v1.success({ visitor });
		} catch (e) {
			return API.v1.failure(e.error);
		}
	},
	delete() {
		try {
			check(this.urlParams, {
				token: String,
			});

			const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token);
			if (!visitor) {
				throw new Meteor.Error('invalid-token');
			}

			const { _id } = visitor;
			const result = Livechat.removeGuest(_id);
			if (result) {
				return API.v1.success({
					visitor: {
						_id,
						ts: new Date().toISOString(),
					},
				});
			}

			return API.v1.failure();
		} catch (e) {
			return API.v1.failure(e.error);
		}
	},
});

API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, {
	get() {
		if (!hasPermission(this.userId, 'view-livechat-manager')) {
			return API.v1.unauthorized();
		}

		const rooms = Rooms.findOpenByVisitorToken(this.urlParams.token, {
			fields: {
				name: 1,
				t: 1,
				cl: 1,
				u: 1,
				usernames: 1,
				servedBy: 1,
			},
		}).fetch();
		return API.v1.success({ rooms });
	},
});

API.v1.addRoute('livechat/visitor.status', {
	post() {
		try {
			check(this.bodyParams, {
				token: String,
				status: String,
			});

			const { token, status } = this.bodyParams;

			const guest = findGuest(token);
			if (!guest) {
				throw new Meteor.Error('invalid-token');
			}

			Livechat.notifyGuestStatusChanged(token, status);

			return API.v1.success({ token, status });
		} catch (e) {
			return API.v1.failure(e);
		}
	},
});


import { settings } from '../../settings';
import s from 'underscore.string';

export const placeholders = {
	replace: (str, data) => {
		if (!str) {
			return '';
		}

		str = str.replace(/\[Site_Name\]/g, settings.get('Site_Name') || '');
		str = str.replace(/\[Site_URL\]/g, settings.get('Site_Url') || '');

		if (data) {
			str = str.replace(/\[name\]/g, data.name || '');
			str = str.replace(/\[fname\]/g, s.strLeft(data.name, ' ') || '');
			str = str.replace(/\[lname\]/g, s.strRightBack(data.name, ' ') || '');
			str = str.replace(/\[email\]/g, data.email || '');
			str = str.replace(/\[password\]/g, data.password || '');
			str = str.replace(/\[reason\]/g, data.reason || '');
			str = str.replace(/\[User\]/g, data.user || '');
			str = str.replace(/\[Room\]/g, data.room || '');

			if (data.unsubscribe) {
				str = str.replace(/\[unsubscribe\]/g, data.unsubscribe);
			}
		}

		str = str.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>' + '$2');

		return str;
	},
};


Oled sa kindel, et soovid vestlust lõpetada?

Oled sa kindel, et soovid lõpetada vestlust ja vahetada osakonda?

Oled sa kindel, et soovid osakonda vahetada?

Kui sõbralik oli sinuga vestelnud agent?

Kui vastuvõtlik oli sinuga vestelnud agent?

Palun võtke hetk, et vastata kiirele küsitlusele toimunud vestluse kohta


import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { hasPermission } from '../../../authorization';
import { cleanRoomHistory } from '../functions';

Meteor.methods({
	cleanRoomHistory({ roomId, latest, oldest, inclusive = true, limit, excludePinned = false, ignoreDiscussion = true, filesOnly = false, fromUsers = [] }) {
		check(roomId, String);
		check(latest, Date);
		check(oldest, Date);
		check(inclusive, Boolean);
		check(limit, Match.Maybe(Number));
		check(excludePinned, Match.Maybe(Boolean));
		check(filesOnly, Match.Maybe(Boolean));
		check(fromUsers, Match.Maybe([String]));

		const userId = Meteor.userId();

		if (!userId) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cleanRoomHistory' });
		}

		if (!hasPermission(userId, 'clean-channel-history', roomId)) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' });
		}

		return cleanRoomHistory({ rid: roomId, latest, oldest, inclusive, limit, excludePinned, ignoreDiscussion, filesOnly, fromUsers });
	},
});


+ltr()
transform: translateX(0%)
+rtl()
transform: translateX(100%)
&:last-child .v-slider__tick-label
+ltr()

keep keep keep keep replace keep keep keep

import './VSlider.sass'

// Components
import VInput from '../VInput'
import { VScaleTransition } from '../transitions'

// Mixins
import mixins, { ExtractVue } from '../../util/mixins'
import Loadable from '../../mixins/loadable'

// Directives
import ClickOutside from '../../directives/click-outside'

// Helpers
import { addOnceEventListener, deepEqual, keyCodes, createRange, convertToUnit, passiveSupported } from '../../util/helpers'
import { consoleWarn } from '../../util/console'

// Types
import Vue, { VNode, VNodeChildrenArrayContents, PropType } from 'vue'
import { ScopedSlotChildren } from 'vue/types/vnode'

interface options extends Vue {
  $refs: {
    track: HTMLElement
  }
}

export default mixins<options &
/* eslint-disable indent */
  ExtractVue<[
    typeof VInput,
    typeof Loadable
  ]>
/* eslint-enable indent */
>(
  VInput,
  Loadable
/* @vue/component */
).extend({
  name: 'v-slider',

  directives: {
    ClickOutside,
  },

  mixins: [Loadable],

  props: {
    disabled: Boolean,
    inverseLabel: Boolean,
    max: {
      type: [Number, String],
      default: 100,
    },
    min: {
      type: [Number, String],
      default: 0,
    },
    step: {
      type: [Number, String],
      default: 1,
    },
    thumbColor: String,
    thumbLabel: {
      type: [Boolean, String] as PropType<boolean | 'always' | undefined>,
      default: undefined,
      validator: v => typeof v === 'boolean' || v === 'always',
    },
    thumbSize: {
      type: [Number, String],
      default: 32,
    },
    tickLabels: {
      type: Array as PropType<string[]>,
      default: () => ([]),
    },
    ticks: {
      type: [Boolean, String] as PropType<boolean | 'always'>,
      default: false,
      validator: v => typeof v === 'boolean' || v === 'always',
    },
    tickSize: {
      type: [Number, String],
      default: 2,
    },
    trackColor: String,
    trackFillColor: String,
    value: [Number, String],
    vertical: Boolean,
  },

  data: () => ({
    app: null as any,
    oldValue: null as any,
    keyPressed: 0,
    isFocused: false,
    isActive: false,
    noClick: false, // Prevent click event if dragging took place, hack for #7915
  }),

  computed: {
    classes (): object {
      return {
        ...VInput.options.computed.classes.call(this),
        'v-input__slider': true,
        'v-input__slider--vertical': this.vertical,
        'v-input__slider--inverse-label': this.inverseLabel,
      }
    },
    internalValue: {
      get (): number {
        return this.lazyValue
      },
      set (val: number) {
        val = isNaN(val) ? this.minValue : val
        // Round value to ensure the
        // entire slider range can
        // be selected with step
        const value = this.roundValue(Math.min(Math.max(val, this.minValue), this.maxValue))

        if (value === this.lazyValue) return

        this.lazyValue = value

        this.$emit('input', value)
      },
    },
    trackTransition (): string {
      return this.keyPressed >= 2 ? 'none' : ''
    },
    minValue (): number {
      return parseFloat(this.min)
    },
    maxValue (): number {
      return parseFloat(this.max)
    },
    stepNumeric (): number {
      return this.step > 0 ? parseFloat(this.step) : 0
    },
    inputWidth (): number {
      const value = (this.roundValue(this.internalValue) - this.minValue) / (this.maxValue - this.minValue) * 100

      return value
    },
    trackFillStyles (): Partial<CSSStyleDeclaration> {
      const startDir = this.vertical ? 'bottom' : 'left'
      const endDir = this.vertical ? 'top' : 'right'
      const valueDir = this.vertical ? 'height' : 'width'

      const start = this.$vuetify.rtl ? 'auto' : '0'
      const end = this.$vuetify.rtl ? '0' : 'auto'
      const value = this.disabled ? `calc(${this.inputWidth}% - 10px)` : `${this.inputWidth}%`

      return {
        transition: this.trackTransition,
        [startDir]: start,
        [endDir]: end,
        [valueDir]: value,
      }
    },
    trackStyles (): Partial<CSSStyleDeclaration> {
      const startDir = this.vertical ? this.$vuetify.rtl ? 'bottom' : 'top' : this.$vuetify.rtl ? 'left' : 'right'
      const endDir = this.vertical ? 'height' : 'width'

      const start = '0px'
      const end = this.disabled ? `calc(${100 - this.inputWidth}% - 10px)` : `calc(${100 - this.inputWidth}%)`

      return {
        transition: this.trackTransition,
        [startDir]: start,
        [endDir]: end,
      }
    },
    showTicks (): boolean {
      return this.tickLabels.length > 0 ||
        !!(!this.disabled && this.stepNumeric && this.ticks)
    },
    numTicks (): number {
      return Math.ceil((this.maxValue - this.minValue) / this.stepNumeric)
    },
    showThumbLabel (): boolean {
      return !this.disabled && !!(
        this.thumbLabel ||
        this.$scopedSlots['thumb-label']
      )
    },
    computedTrackColor (): string | undefined {
      if (this.disabled) return undefined
      if (this.trackColor) return this.trackColor
      if (this.isDark) return this.validationState
      return this.validationState || 'primary lighten-3'
    },
    computedTrackFillColor (): string | undefined {
      if (this.disabled) return undefined
      if (this.trackFillColor) return this.trackFillColor
      return this.validationState || this.computedColor
    },
    computedThumbColor (): string | undefined {
      if (this.thumbColor) return this.thumbColor
      return this.validationState || this.computedColor
    },
  },

  watch: {
    min (val) {
      const parsed = parseFloat(val)
      parsed > this.internalValue && this.$emit('input', parsed)
    },
    max (val) {
      const parsed = parseFloat(val)
      parsed < this.internalValue && this.$emit('input', parsed)
    },
    value: {
      handler (v: number) {
        this.internalValue = v
      },
    },
  },

  // If done in as immediate in
  // value watcher, causes issues
  // with vue-test-utils
  beforeMount () {
    this.internalValue = this.value
  },

  mounted () {
    // Without a v-app, iOS does not work with body selectors
    this.app = document.querySelector('[data-app]') ||
      consoleWarn('Missing v-app or a non-body wrapping element with the [data-app] attribute', this)
  },

  methods: {
    genDefaultSlot (): VNodeChildrenArrayContents {
      const children: VNodeChildrenArrayContents = [this.genLabel()]
      const slider = this.genSlider()
      this.inverseLabel
        ? children.unshift(slider)
        : children.push(slider)

      children.push(this.genProgress())

      return children
    },
    genSlider (): VNode {
      return this.$createElement('div', {
        class: {
          'v-slider': true,
          'v-slider--horizontal': !this.vertical,
          'v-slider--vertical': this.vertical,
          'v-slider--focused': this.isFocused,
          'v-slider--active': this.isActive,
          'v-slider--disabled': this.disabled,
          'v-slider--readonly': this.readonly,
          ...this.themeClasses,
        },
        directives: [{
          name: 'click-outside',
          value: this.onBlur,
        }],
        on: {
          click: this.onSliderClick,
        },
      }, this.genChildren())
    },
    genChildren (): VNodeChildrenArrayContents {
      return [
        this.genInput(),
        this.genTrackContainer(),
        this.genSteps(),
        this.genThumbContainer(
          this.internalValue,
          this.inputWidth,
          this.isActive,
          this.isFocused,
          this.onThumbMouseDown,
          this.onFocus,
          this.onBlur,
        ),
      ]
    },
    genInput (): VNode {
      return this.$createElement('input', {
        attrs: {
          value: this.internalValue,
          id: this.computedId,
          disabled: this.disabled,
          readonly: true,
          tabindex: -1,
          ...this.$attrs,
        },
        // on: this.genListeners(), // TODO: do we need to attach the listeners to input?
      })
    },
    genTrackContainer (): VNode {
      const children = [
        this.$createElement('div', this.setBackgroundColor(this.computedTrackColor, {
          staticClass: 'v-slider__track-background',
          style: this.trackStyles,
        })),
        this.$createElement('div', this.setBackgroundColor(this.computedTrackFillColor, {
          staticClass: 'v-slider__track-fill',
          style: this.trackFillStyles,
        })),
      ]

      return this.$createElement('div', {
        staticClass: 'v-slider__track-container',
        ref: 'track',
      }, children)
    },
    genSteps (): VNode | null {
      if (!this.step || !this.showTicks) return null

      const tickSize = parseFloat(this.tickSize)
      const range = createRange(this.numTicks + 1)
      const direction = this.vertical ? 'bottom' : 'left'
      const offsetDirection = this.vertical ? 'right' : 'top'

      if (this.vertical) range.reverse()

      const ticks = range.map(i => {
        const index = this.$vuetify.rtl ? this.maxValue - i : i
        const children = []

        if (this.tickLabels[index]) {
          children.push(this.$createElement('div', {
            staticClass: 'v-slider__tick-label',
          }, this.tickLabels[index]))
        }

        const width = i * (100 / this.numTicks)
        const filled = this.$vuetify.rtl ? (100 - this.inputWidth) < width : width < this.inputWidth

        return this.$createElement('span', {
          key: i,
          staticClass: 'v-slider__tick',
          class: {
            'v-slider__tick--filled': filled,
          },
          style: {
            width: `${tickSize}px`,
            height: `${tickSize}px`,
            [direction]: `calc(${width}% - ${tickSize / 2}px)`,
            [offsetDirection]: `calc(50% - ${tickSize / 2}px)`,
          },
        }, children)
      })

      return this.$createElement('div', {
        staticClass: 'v-slider__ticks-container',
        class: {
          'v-slider__ticks-container--always-show': this.ticks === 'always' || this.tickLabels.length > 0,
        },
      }, ticks)
    },
    genThumbContainer (
      value: number,
      valueWidth: number,
      isActive: boolean,
      isFocused: boolean,
      onDrag: Function,
      onFocus: Function,
      onBlur: Function,
      ref = 'thumb'
    ): VNode {
      const children = [this.genThumb()]

      const thumbLabelContent = this.genThumbLabelContent(value)
      this.showThumbLabel && children.push(this.genThumbLabel(thumbLabelContent))

      return this.$createElement('div', this.setTextColor(this.computedThumbColor, {
        ref,
        key: ref,
        staticClass: 'v-slider__thumb-container',
        class: {
          'v-slider__thumb-container--active': isActive,
          'v-slider__thumb-container--focused': isFocused,
          'v-slider__thumb-container--show-label': this.showThumbLabel,
        },
        style: this.getThumbContainerStyles(valueWidth),
        attrs: {
          role: 'slider',
          tabindex: this.disabled || this.readonly ? -1 : this.$attrs.tabindex ? this.$attrs.tabindex : 0,
          'aria-label': this.label,
          'aria-valuemin': this.min,
          'aria-valuemax': this.max,
          'aria-valuenow': this.internalValue,
          'aria-readonly': String(this.readonly),
          'aria-orientation': this.vertical ? 'vertical' : 'horizontal',
          ...this.$attrs,
        },
        on: {
          focus: onFocus,
          blur: onBlur,
          keydown: this.onKeyDown,
          keyup: this.onKeyUp,
          touchstart: onDrag,
          mousedown: onDrag,
        },
      }), children)
    },
    genThumbLabelContent (value: number | string): ScopedSlotChildren {
      return this.$scopedSlots['thumb-label']
        ? this.$scopedSlots['thumb-label']!({ value })
        : [this.$createElement('span', [String(value)])]
    },
    genThumbLabel (content: ScopedSlotChildren): VNode {
      const size = convertToUnit(this.thumbSize)

      const transform = this.vertical
        ? `translateY(20%) translateY(${(Number(this.thumbSize) / 3) - 1}px) translateX(55%) rotate(135deg)`
        : `translateY(-20%) translateY(-12px) translateX(-50%) rotate(45deg)`

      return this.$createElement(VScaleTransition, {
        props: { origin: 'bottom center' },
      }, [
        this.$createElement('div', {
          staticClass: 'v-slider__thumb-label-container',
          directives: [{
            name: 'show',
            value: this.isFocused || this.isActive || this.thumbLabel === 'always',
          }],
        }, [
          this.$createElement('div', this.setBackgroundColor(this.computedThumbColor, {
            staticClass: 'v-slider__thumb-label',
            style: {
              height: size,
              width: size,
              transform,
            },
          }), [this.$createElement('div', content)]),
        ]),
      ])
    },
    genThumb (): VNode {
      return this.$createElement('div', this.setBackgroundColor(this.computedThumbColor, {
        staticClass: 'v-slider__thumb',
      }))
    },
    getThumbContainerStyles (width: number): object {
      const direction = this.vertical ? 'top' : 'left'
      let value = this.$vuetify.rtl ? 100 - width : width
      value = this.vertical ? 100 - value : value

      return {
        transition: this.trackTransition,
        [direction]: `${value}%`,
      }
    },
    onThumbMouseDown (e: MouseEvent) {
      e.preventDefault()

      this.oldValue = this.internalValue
      this.keyPressed = 2
      this.isActive = true

      const mouseUpOptions = passiveSupported ? { passive: true, capture: true } : true
      const mouseMoveOptions = passiveSupported ? { passive: true } : false
      if ('touches' in e) {
        this.app.addEventListener('touchmove', this.onMouseMove, mouseMoveOptions)
        addOnceEventListener(this.app, 'touchend', this.onSliderMouseUp, mouseUpOptions)
      } else {
        this.app.addEventListener('mousemove', this.onMouseMove, mouseMoveOptions)
        addOnceEventListener(this.app, 'mouseup', this.onSliderMouseUp, mouseUpOptions)
      }

      this.$emit('start', this.internalValue)
    },
    onSliderMouseUp (e: Event) {
      e.stopPropagation()
      this.keyPressed = 0
      const mouseMoveOptions = passiveSupported ? { passive: true } : false
      this.app.removeEventListener('touchmove', this.onMouseMove, mouseMoveOptions)
      this.app.removeEventListener('mousemove', this.onMouseMove, mouseMoveOptions)

      this.$emit('end', this.internalValue)
      if (!deepEqual(this.oldValue, this.internalValue)) {
        this.$emit('change', this.internalValue)
        this.noClick = true
      }

      this.isActive = false
    },
    onMouseMove (e: MouseEvent) {
      const { value } = this.parseMouseMove(e)
      this.internalValue = value
    },
    onKeyDown (e: KeyboardEvent) {
      if (this.disabled || this.readonly) return

      const value = this.parseKeyDown(e, this.internalValue)

      if (value == null) return

      this.internalValue = value
      this.$emit('change', value)
    },
    onKeyUp () {
      this.keyPressed = 0
    },
    onSliderClick (e: MouseEvent) {
      if (this.noClick) {
        this.noClick = false
        return
      }
      const thumb = this.$refs.thumb as HTMLElement
      thumb.focus()

      this.onMouseMove(e)
      this.$emit('change', this.internalValue)
    },
    onBlur (e: Event) {
      this.isFocused = false

      this.$emit('blur', e)
    },
    onFocus (e: Event) {
      this.isFocused = true

      this.$emit('focus', e)
    },
    parseMouseMove (e: MouseEvent) {
      const start = this.vertical ? 'top' : 'left'
      const length = this.vertical ? 'height' : 'width'
      const click = this.vertical ? 'clientY' : 'clientX'

      const {
        [start]: trackStart,
        [length]: trackLength,
      } = this.$refs.track.getBoundingClientRect() as any
      const clickOffset = 'touches' in e ? (e as any).touches[0][click] : e[click] // Can we get rid of any here?

      // It is possible for left to be NaN, force to number
      let clickPos = Math.min(Math.max((clickOffset - trackStart) / trackLength, 0), 1) || 0

      if (this.vertical) clickPos = 1 - clickPos
      if (this.$vuetify.rtl) clickPos = 1 - clickPos

      const isInsideTrack = clickOffset >= trackStart && clickOffset <= trackStart + trackLength
      const value = parseFloat(this.min) + clickPos * (this.maxValue - this.minValue)

      return { value, isInsideTrack }
    },
    parseKeyDown (e: KeyboardEvent, value: number) {
      if (this.disabled) return

      const { pageup, pagedown, end, home, left, right, down, up } = keyCodes

      if (![pageup, pagedown, end, home, left, right, down, up].includes(e.keyCode)) return

      e.preventDefault()
      const step = this.stepNumeric || 1
      const steps = (this.maxValue - this.minValue) / step
      if ([left, right, down, up].includes(e.keyCode)) {
        this.keyPressed += 1

        const increase = this.$vuetify.rtl ? [left, up] : [right, up]
        const direction = increase.includes(e.keyCode) ? 1 : -1
        const multiplier = e.shiftKey ? 3 : (e.ctrlKey ? 2 : 1)

        value = value + (direction * step * multiplier)
      } else if (e.keyCode === home) {
        value = this.minValue
      } else if (e.keyCode === end) {
        value = this.maxValue
      } else {
        const direction = e.keyCode === pagedown ? 1 : -1
        value = value - (direction * step * (steps > 100 ? steps / 10 : 10))
      }

      return value
    },
    roundValue (value: number): number {
      if (!this.stepNumeric) return value
      // Format input value using the same number
      // of decimals places as in the step prop
      const trimmedStep = this.step.toString().trim()
      const decimals = trimmedStep.indexOf('.') > -1
        ? (trimmedStep.length - trimmedStep.indexOf('.') - 1)
        : 0
      const offset = this.minValue % this.stepNumeric

      const newValue = Math.round((value - offset) / this.stepNumeric) * this.stepNumeric + offset

      return parseFloat(Math.min(newValue, this.maxValue).toFixed(decimals))
    },
  },
})


<template>
  <v-breadcrumbs
    v-if="structure !== false"
    :items="breadcrumbs"
    class="px-0 py-5 text-capitalize"
  />
</template>

<script>
  // Utilities
  import {
    get,
    sync,
  } from 'vuex-pathify'

  export default {
    name: 'DocumentationBreadcrumbs',

    data: () => ({ script: null }),

    computed: {
      breadcrumbs: get('documentation/breadcrumbs'),
      structure: sync('documentation/structure'),
    },

    watch: {
      '$route.path': {
        immediate: true,
        handler () {
          if (
            !this.breadcrumbs.length ||
            this.breadcrumbs[0].disabled
          ) return

          this.addMicroData()
        },
      },
    },

    methods: {
      addMicroData () {
        this.$ssrContext
          ? this.addMicroDataServer()
          : this.addMicroDataDesktop()
      },
      addMicroDataDesktop () {
        if (typeof document === 'undefined') return

        const script = document.createElement('script')

        script.type = 'application/ld+json'
        script.innerHTML = JSON.stringify(this.genBreadcrumbList(), 2, null)
        document.body.appendChild(script)

        this.script && document.body.removeChild(this.script)
        this.script = script
      },
      addMicroDataServer () {
        const breadcrumbs = JSON.stringify(this.genBreadcrumbList(), 2, null)

        this.$ssrContext.scripts += `\n<script type="application\\/ld+json">${breadcrumbs}<\\/script>\n`
      },
      genBreadcrumbList () {
        return {
          '@context': 'https://schema.org',
          '@type': 'BreadcrumbList',
          itemListElement: this.breadcrumbs.map(this.genListItem),
        }
      },
      genListItem (breadcrumb, index) {
        const origin = this.$ssrContext
          ? `https://${this.$ssrContext.hostname}`
          : typeof window !== 'undefined' ? window.location.origin : 'https://vuetifyjs.com'

        return {
          '@type': 'ListItem',
          item: `${origin}${breadcrumb.to}`,
          name: breadcrumb.text,
          position: index + 1,
        }
      },
    },
  }
</script>


// Components
import VApp from '../VApp'

// Utilities
import {
  mount,
  Wrapper,
} from '@vue/test-utils'

describe('VApp.ts', () => {
  type Instance = InstanceType<typeof VApp>
  let mountFunction: (options?: object) => Wrapper<Instance>

  beforeEach(() => {
    mountFunction = (options = {}) => {
      return mount(VApp, {
        ...options,
      })
    }
  })

  it('should match a snapshot', () => {
    const wrapper = mountFunction({
      mocks: {
        $vuetify: {
          rtl: false,
          theme: {
            dark: false,
          },
        },
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should have data-app attribute', () => {
    const wrapper = mountFunction({
      mocks: {
        $vuetify: {
          rtl: false,
          theme: {
            dark: false,
          },
        },
      },
    })

    const app = wrapper.find('.v-application')
    expect(app.attributes()['data-app']).toBe('true')
  })

  it('should allow a custom id', () => {
    const wrapper = mountFunction({
      propsData: {
        id: 'inspire',
      },
      mocks: {
        $vuetify: {
          rtl: false,
          theme: {
            dark: false,
          },
        },
      },
    })
    const app = wrapper.find('.v-application')
    expect(app.attributes().id).toBe('inspire')

    expect(wrapper.html()).toMatchSnapshot()
  })
})


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VBreadcrumbs.ts should have breadcrumbs classes 1`] = `

`;

exports[`VBreadcrumbs.ts should render items without slot 1`] = `

      a

    /

      b

    /

      c

    /

      d

`;

exports[`VBreadcrumbs.ts should use a custom divider slot 1`] = `

      a

    /divider/

      b

    /divider/

      c

    /divider/

      d

`;

exports[`VBreadcrumbs.ts should use slot to render items if present 1`] = `

      A

    /

      B

    /

      C

    /

      D

`;


transform: translateX(-100%)
+rtl()
transform: translateX(0%)
.v-slider__tick-label
top: 8px
+ltr()

keep keep keep replace keep keep keep keep keep


import VDatePickerDateTable from '../VDatePickerDateTable'
import { Lang } from '../../../services/lang'
import { preset } from '../../../presets/default'
import {
  mount,
  MountOptions,
  Wrapper,
} from '@vue/test-utils'

describe('VDatePickerDateTable.ts', () => {
  type Instance = InstanceType<typeof VDatePickerDateTable>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>
  beforeEach(() => {
    mountFunction = (options?: MountOptions<Instance>) => {
      return mount(VDatePickerDateTable, {
        ...options,
        mocks: {
          $vuetify: {
            rtl: false,
            lang: new Lang(preset),
          },
        },
      })
    }
  })

  it('should render component and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render readonly component and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        readonly: true,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render disabled component and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        disabled: true,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with showWeek and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2018-02',
        current: '2005-07',
        value: null,
        firstDayOfWeek: 2,
        showWeek: true,
      },
    })
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component and match snapshot for multiple selection', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        multiple: true,
        selectedDates: ['2005-11-03', '2005-11-05', '2005-11-08'],
        value: '2005-11-03',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with events (array) and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        events: ['2005-05-03'],
        eventColor: 'red',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with events (function) and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        events: date => date === '2005-05-03',
        eventColor: 'red',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with events colored by object and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        events: ['2005-05-03', '2005-05-04'],
        eventColor: { '2005-05-03': 'red', '2005-05-04': 'blue lighten-1' },
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render component with events colored by function and match snapshot', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        events: ['2005-05-03', '2005-05-04'],
        eventColor: date => ({ '2005-05-03': 'red' }[date]),
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should match snapshot with first day of week', function () {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        firstDayOfWeek: 2,
      },
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it.skip('should watch tableDate value and run transition', async () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
      },
    })

    wrapper.setProps({
      tableDate: '2005-06',
    })
    await wrapper.vm.$nextTick()
    expect(wrapper.findAll('table').at(0).element.className).toBe('tab-transition-enter tab-transition-enter-active')
  })

  it.skip('should watch tableDate value and run reverse transition', async () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
      },
    })

    wrapper.setProps({
      tableDate: '2005-04',
    })
    await wrapper.vm.$nextTick()
    expect(wrapper.findAll('table').at(0).element.className).toBe('tab-reverse-transition-enter tab-reverse-transition-enter-active')
  })

  it('should emit event when date button is clicked', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
      },
    })

    const input = jest.fn()
    wrapper.vm.$on('input', input)

    wrapper.findAll('tbody button').at(0).trigger('click')
    expect(input).toHaveBeenCalledWith('2005-05-01')
  })

  it('should not emit event when disabled month button is clicked', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        current: '2005-07',
        value: '2005-11-03',
        allowedDates: () => false,
      },
    })

    const input = jest.fn()
    wrapper.vm.$on('input', input)

    wrapper.findAll('tbody button').at(0).trigger('click')
    expect(input).not.toHaveBeenCalled()
  })

  it('should emit tableDate event when scrolled and scrollable', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
        scrollable: true,
      },
    })

    const tableDate = jest.fn()
    wrapper.vm.$on('update:table-date', tableDate)

    wrapper.trigger('wheel')
    expect(tableDate).toHaveBeenCalledWith('2005-06')
  })

  it('should not emit tableDate event when scrolled and not scrollable', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
      },
    })

    const tableDate = jest.fn()
    wrapper.vm.$on('update:table-date', tableDate)

    wrapper.trigger('wheel')
    expect(tableDate).not.toHaveBeenCalled()
  })

  // TODO
  it.skip('should emit tableDate event when swiped', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
      },
    })

    const tableDate = jest.fn()
    wrapper.vm.$on('update:table-date', tableDate)

    wrapper.trigger('touchstart')
    wrapper.trigger('touchend')
    expect(tableDate).toHaveBeenCalledWith('2005-06')
  })

  it('should change tableDate when touch is called', () => {
    const wrapper = mountFunction({
      propsData: {
        tableDate: '2005-05',
      },
    })

    const tableDate = jest.fn()
    wrapper.vm.$on('update:table-date', tableDate)

    wrapper.vm.touch(1, wrapper.vm.calculateTableDate)
    expect(tableDate).toHaveBeenCalledWith('2005-06')
    wrapper.vm.touch(-1, wrapper.vm.calculateTableDate)
    expect(tableDate).toHaveBeenCalledWith('2005-04')
  })
})


# Intersection observer

次のウィンドウ内でスクロールし、色付きのドットを見てください。[v-card](/components/cards) が error から success に変わることがわかります。

### オプション指定

マウント時に指定されたユーザー コールバックのみを呼び出し、要素がビューポートに入ったときユーザー コールバックを 1 回だけ呼び出します。**quiet** 修飾子を使用する場合、呼び出されるのは 1 回だけです。

指定されたコールバックを、バインド時に自動的に呼び出しません。

対象となる要素がIntersect（交差）したときに呼び出す関数。

[Intersection Observer API](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API) は IE11 では既定では使用できませんが、 [polyfill](https://github.com/w3c/IntersectionObserver) を使用して実装できます。


const tickSize = parseFloat(this.tickSize)

const range = createRange(this.numTicks + 1)

const direction = this.vertical ? 'bottom' : 'left'

const offsetDirection = this.vertical ? 'right' : 'top'

keep keep keep replace replace keep keep keep

const direction = this.vertical ? 'bottom' : (this.$vuetify.rtl ? 'right' : 'left')

const offsetDirection = this.vertical ? (this.$vuetify.rtl ? 'left' : 'right') : 'top'

// Imports
@import './_variables.scss'

// Theme
+theme(v-slider) using ($material)
  .v-slider__track-background,
  .v-slider__track-fill,
  .v-slider__thumb
    background: map-deep-get($material, 'slider', 'inactive')

// Block
.v-slider
  cursor: default
  display: flex
  align-items: center
  position: relative
  flex: 1
  user-select: none

  input
    cursor: default
    padding: 0
    width: 100%
    display: none

// Elements
.v-slider__track-container
  position: absolute
  border-radius: $slider-track-border-radius

.v-slider__track
  &-background, &-fill
    position: absolute
    transition: $slider-transition

.v-slider__thumb-container
  outline: none
  position: absolute
  transition: $slider-transition
  top: 50%

  &:hover
    .v-slider__thumb:before
      transform: scale(1)

.v-slider__thumb
  position: absolute
  width: $slider-thumb-size
  height: $slider-thumb-size
  left: -#{$slider-thumb-size / 2}
  top: 50%
  border-radius: $slider-thumb-border-radius
  transition: $slider-transition
  transform: translateY(-50%)
  user-select: none

  &:before
    transition: $primary-transition
    content: ''
    color: inherit
    width: #{$slider-thumb-size + $slider-thumb-focused-size-increase}
    height: #{$slider-thumb-size + $slider-thumb-focused-size-increase}
    border-radius: $slider-thumb-border-radius
    background: currentColor
    opacity: $slider-thumb-before-opacity
    position: absolute
    left: -#{$slider-thumb-focused-size-increase / 2}
    top: -#{$slider-thumb-focused-size-increase / 2}
    transform: scale(0.1)
    pointer-events: none

.v-slider__ticks-container
  position: absolute

.v-slider__tick
  position: absolute
  opacity: 0
  background-color: rgba(0, 0, 0, 0.5) // TODO: move to theme mixin?
  transition: $slider-transition
  border-radius: $slider-tick-border-radius

  &--filled
    background-color: rgba(255, 255, 255, 0.5) // TODO: move to theme mixin?

  &:first-child .v-slider__tick-label
    +ltr()
      transform: none

    +rtl()
      transform: translateX(100%)

  &:last-child .v-slider__tick-label
    +ltr()
      transform: translateX(-100%)

    +rtl()
      transform: none

.v-slider__tick-label
  position: absolute
  user-select: none
  white-space: nowrap

.v-slider__thumb-label-container
  position: absolute
  left: 0
  top: 0
  transition: $slider-thumb-label-transition

.v-slider__thumb-label
  display: flex
  align-items: center
  justify-content: center
  font-size: $slider-thumb-label-font-size
  color: #fff // TODO: move to theme mixin?
  width: $slider-thumb-label-width
  height: $slider-thumb-label-height
  border-radius: 50% 50% 0
  position: absolute
  left: 0
  bottom: 100%
  user-select: none
  transition: $slider-thumb-label-transition

// Modifiers
.v-slider--horizontal
  min-height: $slider-horizontal-min-height
  margin-left: $slider-horizontal-left
  margin-right: $slider-horizontal-right

  .v-slider__track
    &-container
      width: 100%
      height: $slider-track-width
      left: 0
      top: 50%
      transform: translateY(-50%)

    &-background, &-fill
      height: 100%

  .v-slider__ticks-container
    left: 0
    height: $slider-track-width
    width: 100%

  .v-slider__tick
    &:first-child .v-slider__tick-label
      +ltr()
        transform: translateX(0%)

      +rtl()
        transform: translateX(100%)

    &:last-child .v-slider__tick-label
      +ltr()
        transform: translateX(-100%)

      +rtl()
        transform: translateX(0%)

    .v-slider__tick-label
      top: 8px

      +ltr()
        transform: translateX(-50%)

      +rtl()
        transform: translateX(50%)

  .v-slider__thumb-label
    // Needed for IE11 which does not
    // process translateY(calc(-20% - 12px))
    // or translate(-50%, calc(-20% - 12px))
    transform: translateY(-20%) translateY(-12px) translateX(-50%) rotate(45deg)

    > *
      transform: rotate(-45deg)

.v-slider--vertical
  min-height: $slider-vertical-min-height
  margin-top: $slider-vertical-margin-top
  margin-bottom: $slider-vertical-margin-bottom

  .v-slider__track
    &-container
      height: 100%
      width: $slider-track-width
      left: 50%
      top: 0
      transform: translateX(-50%)

    &-background, &-fill
      width: 100%

  .v-slider__thumb-container
    left: 50%

  .v-slider__ticks-container
    top: 0
    width: $slider-track-width
    height: 100%
    left: 50%
    transform: translateX(-50%)

  .v-slider__tick
    .v-slider__tick-label,
    &:first-child .v-slider__tick-label,
    &:last-child .v-slider__tick-label
      +ltr()
        transform: translateY(-50%)
        left: 12px

      +rtl()
        transform: translateY(-50%)
        right: 12px

  .v-slider__thumb-label
    // Needed for IE11 which does not
    // process translateY(calc(-20% - 12px))
    // or translate(-50%, calc(-20% - 12px))
    // transform: translateY(20%) translateY(10px) translateX(55%) rotate(135deg)

    > *
      transform: rotate(-135deg)

.v-slider__thumb-container--focused
  .v-slider__thumb:before
    transform: scale(1)

.v-slider--active
  .v-slider__tick
    opacity: 1

.v-slider__thumb-container--active
  .v-slider__thumb:before
    transform: scale(1.5) !important

.v-slider--disabled
  pointer-events: none

  .v-slider__thumb
    width: #{$slider-thumb-size - 4}
    height: #{$slider-thumb-size - 4}
    left: -#{($slider-thumb-size - 4) / 2}

    &:before
      display: none

.v-slider__ticks-container--always-show
  .v-slider__tick
    opacity: 1

// Input
.v-input__slider
  &.v-input--is-readonly > .v-input__control
    pointer-events: none

  .v-input__slot .v-label
    +ltr()
      margin-left: 0
      margin-right: $slider-label-margin-end

    +rtl()
      margin-right: 0
      margin-left: $slider-label-margin-end

.v-input__slider--inverse-label
  .v-input__slot .v-label
    +ltr()
      margin-right: 0
      margin-left: $slider-label-margin-start

    +rtl()
      margin-left: 0
      margin-right: $slider-label-margin-start

.v-input__slider--vertical
  align-items: center

  +ltr()
    flex-direction: column-reverse

  +rtl()
    flex-direction: column

  .v-input
    &__slot, &__prepend-outer, &__append-outer
      margin: 0

  .v-messages
    display: none

.v-input--has-state
  .v-slider__track-background
    opacity: $slider-state-track-background-opacity


// Types
import Framework from '../../../framework'
import { Breakpoint } from 'vuetify/types/services/breakpoint'
import { Icons } from 'vuetify/types/services/icons'
import { Lang } from 'vuetify/types/services/lang'
import { Theme } from 'vuetify/types/services/theme'

describe('$vuetify.presets', () => {
  it('should merge user and default preset', () => {
    const vuetify = new Framework({
      rtl: true,
      breakpoint: {
        thresholds: { xs: 200 },
      },
      icons: {
        iconfont: 'md',
        values: { complete: 'bar' },
      },
      lang: {
        locales: {
          en: {
            badge: 'Foobar',
            dataIterator: { noResultsText: 'Fizzbuzz' },
          },
        },
      },
      theme: {
        dark: true,
        themes: {
          light: {
            primary: 'blue',
            // https://github.com/vuetifyjs/vuetify/issues/10100
            secondary: { darken4: 'red' },
          },
        },
      },
    })

    expect(JSON.stringify(vuetify.preset)).toMatchSnapshot()

    const icons: Icons = vuetify.framework as any
    const lang: Lang = vuetify.framework.lang as any
    const itheme: Theme = vuetify.framework.theme as any
    const breakpoints: Breakpoint = vuetify.framework.breakpoint as any

    expect(JSON.stringify(icons.values)).toMatchSnapshot()
    expect(JSON.stringify(lang.locales)).toMatchSnapshot()
    expect(JSON.stringify(itheme.themes)).toMatchSnapshot()
    expect(JSON.stringify(breakpoints.thresholds)).toMatchSnapshot()
  })
})


// Styles
import '../VAutocomplete/VAutocomplete.sass'

// Extensions
import VSelect from '../VSelect/VSelect'
import VAutocomplete from '../VAutocomplete/VAutocomplete'

// Utils
import { keyCodes } from '../../util/helpers'

// Types
import { PropType } from 'vue'

/* @vue/component */
export default VAutocomplete.extend({
  name: 'v-combobox',

  props: {
    delimiters: {
      type: Array as PropType<string[]>,
      default: () => ([]),
    },
    returnObject: {
      type: Boolean,
      default: true,
    },
  },

  data: () => ({
    editingIndex: -1,
  }),

  computed: {
    computedCounterValue (): number {
      return this.multiple
        ? this.selectedItems.length
        : (this.internalSearch || '').toString().length
    },
    hasSlot (): boolean {
      return VSelect.options.computed.hasSlot.call(this) || this.multiple
    },
    isAnyValueAllowed (): boolean {
      return true
    },
    menuCanShow (): boolean {
      if (!this.isFocused) return false

      return this.hasDisplayedItems ||
        (!!this.$slots['no-data'] && !this.hideNoData)
    },
  },

  methods: {
    onInternalSearchChanged (val: any) {
      if (
        val &&
        this.multiple &&
        this.delimiters.length
      ) {
        const delimiter = this.delimiters.find(d => val.endsWith(d))
        if (delimiter != null) {
          this.internalSearch = val.slice(0, val.length - delimiter.length)
          this.updateTags()
        }
      }

      this.updateMenuDimensions()
    },
    genInput () {
      const input = VAutocomplete.options.methods.genInput.call(this)

      delete input.data!.attrs!.name
      input.data!.on!.paste = this.onPaste

      return input
    },
    genChipSelection (item: object, index: number) {
      const chip = VSelect.options.methods.genChipSelection.call(this, item, index)

      // Allow user to update an existing value
      if (this.multiple) {
        chip.componentOptions!.listeners! = {
          ...chip.componentOptions!.listeners!,
          dblclick: () => {
            this.editingIndex = index
            this.internalSearch = this.getText(item)
            this.selectedIndex = -1
          },
        }
      }

      return chip
    },
    onChipInput (item: object) {
      VSelect.options.methods.onChipInput.call(this, item)

      this.editingIndex = -1
    },
    // Requires a manual definition
    // to overwrite removal in v-autocomplete
    onEnterDown (e: Event) {
      e.preventDefault()
      // If has menu index, let v-select-list handle
      if (this.getMenuIndex() > -1) return

      this.$nextTick(this.updateSelf)
    },
    onFilteredItemsChanged (val: never[], oldVal: never[]) {
      if (!this.autoSelectFirst) return

      VAutocomplete.options.methods.onFilteredItemsChanged.call(this, val, oldVal)
    },
    onKeyDown (e: KeyboardEvent) {
      const keyCode = e.keyCode

      VSelect.options.methods.onKeyDown.call(this, e)

      // If user is at selection index of 0
      // create a new tag
      if (this.multiple &&
        keyCode === keyCodes.left &&
        this.$refs.input.selectionStart === 0
      ) {
        this.updateSelf()
      } else if (keyCode === keyCodes.enter) {
        this.onEnterDown(e)
      }

      // The ordering is important here
      // allows new value to be updated
      // and then moves the index to the
      // proper location
      this.changeSelectedIndex(keyCode)
    },
    onTabDown (e: KeyboardEvent) {
      // When adding tags, if searching and
      // there is not a filtered options,
      // add the value to the tags list
      if (this.multiple &&
        this.internalSearch &&
        this.getMenuIndex() === -1
      ) {
        e.preventDefault()
        e.stopPropagation()

        return this.updateTags()
      }

      VAutocomplete.options.methods.onTabDown.call(this, e)
    },
    selectItem (item: object) {
      // Currently only supports items:<string[]>
      if (this.editingIndex > -1) {
        this.updateEditing()
      } else {
        VAutocomplete.options.methods.selectItem.call(this, item)
      }
    },
    setSelectedItems () {
      if (this.internalValue == null ||
        this.internalValue === ''
      ) {
        this.selectedItems = []
      } else {
        this.selectedItems = this.multiple ? this.internalValue : [this.internalValue]
      }
    },
    setValue (value?: any) {
      VSelect.options.methods.setValue.call(this, value != null ? value : this.internalSearch)
    },
    updateEditing () {
      const value = this.internalValue.slice()
      value[this.editingIndex] = this.internalSearch

      this.setValue(value)

      this.editingIndex = -1
    },
    updateCombobox () {
      const isUsingSlot = Boolean(this.$scopedSlots.selection) || this.hasChips

      // If search is not dirty and is
      // using slot, do nothing
      if (isUsingSlot && !this.searchIsDirty) return

      // The internal search is not matching
      // the internal value, update the input
      if (this.internalSearch !== this.getText(this.internalValue)) this.setValue()

      // Reset search if using slot
      // to avoid a double input
      if (isUsingSlot) this.internalSearch = undefined
    },
    updateSelf () {
      this.multiple ? this.updateTags() : this.updateCombobox()
    },
    updateTags () {
      const menuIndex = this.getMenuIndex()

      // If the user is not searching
      // and no menu item is selected
      // do nothing
      if (menuIndex < 0 &&
        !this.searchIsDirty
      ) return

      if (this.editingIndex > -1) {
        return this.updateEditing()
      }

      const index = this.selectedItems.indexOf(this.internalSearch)
      // If it already exists, do nothing
      // this might need to change to bring
      // the duplicated item to the last entered
      if (index > -1) {
        const internalValue = this.internalValue.slice()
        internalValue.splice(index, 1)

        this.setValue(internalValue)
      }

      // If menu index is greater than 1
      // the selection is handled elsewhere
      // TODO: find out where
      if (menuIndex > -1) return (this.internalSearch = null)

      this.selectItem(this.internalSearch)
      this.internalSearch = null
    },
    onPaste (event: ClipboardEvent) {
      if (!this.multiple || this.searchIsDirty) return

      const pastedItemText = event.clipboardData!.getData('text/vnd.vuetify.autocomplete.item+plain')
      if (pastedItemText && this.findExistingIndex(pastedItemText as any) === -1) {
        event.preventDefault()
        VSelect.options.methods.selectItem.call(this, pastedItemText as any)
      }
    },
  },
})


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Table Row should render non-string values 1`] = `

    string

    12.34

    1,2

    false

    [object Object]

`;

exports[`Table Row should render without slots 1`] = `

    0.68

    0.65

`;


const index = this.$vuetify.rtl ? this.maxValue - i : i


<template>
  <v-row justify="center" align="center">
    <v-container>
      <v-progress-linear
        :active="active"
        :background-opacity="opacity"
        :bottom="bottom"
        :buffer-value="buffer"
        :height="height"
        :indeterminate="indeterminate"
        :query="query"
        :rounded="rounded"
        :stream="stream"
        :striped="striped"
        :top="top"
        :value="value"
        color="light-blue"
      ></v-progress-linear>
    </v-container>

    <v-row>
      <v-col cols="12" md="6" lg="3">
        <v-text-field
          v-model="buffer"
          class="mx-4"
          label="Buffer"
          max="100"
          min="1"
          step="1"
          style="width: 125px"
          type="number"
          @keydown="false"
        ></v-text-field>
      </v-col>
      <v-col cols="12" md="6" lg="3">
        <v-text-field
          v-model="height"
          class="mx-4"
          label="Height - px"
          max="100"
          min="1"
          step="1"
          style="width: 125px"
          type="number"
          @keydown="false"
        ></v-text-field>
      </v-col>
      <v-col cols="12" md="6" lg="3">
        <v-text-field
          v-model="opacity"
          class="mx-4"
          label="Opacity"
          max="1"
          min="0"
          step=".01"
          style="width: 125px"
          type="number"
          @keydown="false"
        ></v-text-field>
      </v-col>
      <v-col cols="12" md="6" lg="3">
        <v-text-field
          v-model="value"
          class="mx-4"
          label="Value - %"
          max="100"
          min="1"
          step="1"
          style="width: 125px"
          type="number"
          @keydown="false"
        ></v-text-field>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="active" label="Toggle active" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="bottom" label="Toggle bottom" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="indeterminate" label="Toggle indeterminate" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="query" label="Toggle query" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="rounded" label="Toggle rounded" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="stream" label="Toggle stream" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="striped" label="Toggle striped" class="mx-4"></v-switch>
      </v-col>
      <v-col cols="6" md="3">
        <v-switch v-model="top" label="Toggle top" class="mx-4"></v-switch>
      </v-col>
    </v-row>
  </v-row>
</template>

<script>
  export default {
    data: () => ({
      absolute: false,
      active: true,
      opacity: 0.3,
      bottom: false,
      buffer: 100,
      fixed: false,
      height: 4,
      indeterminate: false,
      query: false,
      rounded: false,
      stream: false,
      striped: false,
      top: false,
      value: 25,
    }),
  }
</script>


<template>
  <div class="text-center">
    <v-switch v-model="closeOnContentClick" label="Close on content click"></v-switch>
    <v-menu top :close-on-content-click="closeOnContentClick">
      <template v-slot:activator="{ on }">
        <v-btn
          color="primary"
          dark
          v-on="on"
        >
          Dropdown
        </v-btn>
      </template>

      <v-list>
        <v-list-item
          v-for="(item, index) in items"
          :key="index"
          @click=""
        >
          <v-list-item-title>{{ item.title }}</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>
  </div>
</template>

<script>
  export default {
    data: () => ({
      items: [
        { title: 'Click Me' },
        { title: 'Click Me' },
        { title: 'Click Me' },
        { title: 'Click Me 2' },
      ],
      closeOnContentClick: true,
    }),
  }
</script>


# Internationalisierung (i18n)

Vuetify unterstützt die Internationalisierung (i18n) der Komponenten. Beim Hochfahren Ihrer Anwendung können Sie die verfügbaren Sprachen und die aktuell aktive Sprache mit der **current**-Option angeben. Der **lang**-Service unterstützt auch eine einfache Integration mit [vue-i18n](#vue-i-18-n).

Um die verfügbaren Sprachen oder die aktuelle Sprache zu setzen, geben Sie die Option `lang` bei der Installation von Vuetify an. Die angegebene `locales`-Eigenschaft wird mit den bereits existierenden Sprachen zusammengeführt. Sie können das Gebietsschema während der Laufzeit durch das `$vuetify`-Objekt in der Vue Instanz ändern.

Zur Zeit bietet Vuetify Übersetzungen in den folgenden Sprachen:

Vuetify bietet nur eine grundlegende Übersetzungsfunktion `t`. Für fortgeschrittenere Internationalisierungen wird empfohlen Vuetifys Integration um [vue-i18n](#vue-i-18-n) zu erweitern

Wenn Sie eigene Vuetify-Komponenten erstellen, die in die Internationalisierungs-Engine eingehängt werden müssen, können Sie die `t`-Funktion verwenden, die Teil der `$vuetify.lang` API ist.

Ein wichtiger Hinweis bei der Verwendung externer Lokalisierungs-Plugins ist, dass vuetify nicht automatisch auf Englisch zurückfällt, wenn keine Lokalisierung für die aktuelle Lokalisierung existiert. Stellen Sie also sicher, dass Sie das Plugin sowohl mit den englischen als auch mit Ihren eigenen Lokalisierungen versorgen.


const filled = this.$vuetify.rtl ? (100 - this.inputWidth) < width : width < this.inputWidth

keep keep keep keep keep replace keep keep keep

const width = index * (100 / this.numTicks)


export default {
  badge: 'badge',
  close: 'Close',
  dataIterator: {
    noResultsText: 'Geen ooreenstemmende resultate is gevind nie',
    loadingText: 'Loading item...',
  },
  dataTable: {
    itemsPerPageText: 'Rye per bladsy:',
    ariaLabel: {
      sortDescending: 'Sorted descending.',
      sortAscending: 'Sorted ascending..',
      sortNone: 'Not sorted.',
      activateNone: 'Activate to remove sorting.',
      activateDescending: 'Activate to sort descending.',
      activateAscending: 'Activate to sort ascending.',
    },
    sortBy: 'Sort by',
  },
  dataFooter: {
    itemsPerPageText: 'Aantal per bladsy:',
    itemsPerPageAll: 'Alles',
    nextPage: 'Volgende bladsy',
    prevPage: 'Vorige bladsy',
    firstPage: 'Eerste bladsy',
    lastPage: 'Laaste bladsy',
    pageText: '{0}-{1} van {2}',
  },
  datePicker: {
    itemsSelected: '{0} gekies',
  },
  noDataText: 'Geen data is beskikbaar nie',
  carousel: {
    prev: 'Vorige visuele',
    next: 'Volgende visuele',
    ariaLabel: {
      delimiter: 'Carousel slide {0} of {1}',
    },
  },
  calendar: {
    moreEvents: '{0} meer',
  },
  fileInput: {
    counter: '{0} files',
    counterSize: '{0} files ({1} in total)',
  },
  timePicker: {
    am: 'AM',
    pm: 'PM',
  },
}


// src/index.js

import './src/sass/main.scss'
// OR
require('./src/sass/main.scss')


daysUntilLock: 365
lockComment: false

pulls:
  daysUntilLock: 30
  exemptLabels: ['S: on hold', 'S: work in progress']


import VDialog from './VDialog'

export { VDialog }
export default VDialog


@import '../../styles/styles.sass';

$app-bar-transition: .4s opacity map-get($transition, 'fast-out-slow-in') !default;
$app-bar-scrolled-title-padding-bottom: 9px !default;


<template>
  <v-row justify="center">
    <v-menu transition="slide-x-transition">
      <template v-slot:activator="{ on }">
        <v-btn color="primary" class="ma-2" v-on="on">
          Slide X Transition
        </v-btn>
      </template>
      <v-list>
        <v-list-item
          v-for="n in 5"
          :key="n"
          link
        >
          <v-list-item-title v-text="'Item ' + n"></v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>

    <v-menu transition="scroll-y-transition">
      <template v-slot:activator="{ on }">
        <v-btn color="secondary" class="ma-2" v-on="on">
          Scroll Y Transition
        </v-btn>
      </template>
      <v-list>
        <v-list-item
          v-for="n in 5"
          :key="n"
          link
        >
          <v-list-item-title v-text="'Item ' + n"></v-list-item-title>
        </v-list-item>
      </v-list>
    </v-menu>
  </v-row>
</template>


@prisma/migrate

This package is intended for Prisma's internal use

DEV=true node -r esbuild-register helpers/build.ts


import testMatrix from './_matrix'
// @ts-ignore
import type { Prisma as PrismaNamespace, PrismaClient } from './node_modules/@prisma/client'

declare let prisma: PrismaClient
declare let Prisma: typeof PrismaNamespace

testMatrix.setupTestSuite(
  () => {
    test('allows to use DbNull together with query extensions', async () => {
      const xprisma = prisma.$extends({
        query: {
          $allModels: {
            $allOperations({ args, query }) {
              return query(args)
            },
          },
        },
      })

      await expect(xprisma.user.create({ data: { json: Prisma.DbNull } })).resolves.not.toThrow()
    })
  },
  {
    optOut: {
      from: ['sqlserver', 'sqlite', 'mongodb'],
      reason: `
        sqlserver, sqlite - JSON column is not supported
        mongodb - DbNull is not supported
      `,
    },
  },
)