quikchat
Version:
quikchat is a simple vanilla (no dependancies) JavaScript chat control for browsers
1,339 lines (1,260 loc) • 80.4 kB
JavaScript
'use strict';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _classCallCheck(a, n) {
if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
writable: !1
}), e;
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
// Auto-generated version file - DO NOT EDIT MANUALLY
// This file is automatically updated by tools/updateVersion.js
var quikchatVersion = {
version: "1.1.17",
license: "BSD-2",
url: "https://github/deftio/quikchat",
fun: true
};
/**
* Simplified virtual scrolling implementation for QuikChat
* @private
*/
var SimpleVirtualScroller = /*#__PURE__*/function () {
function SimpleVirtualScroller(container) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, SimpleVirtualScroller);
this.container = container;
this.items = [];
this.itemHeight = options.itemHeight || 80; // Default/estimate height
this.buffer = options.buffer || 5;
this.visibleRange = {
start: 0,
end: 0
};
this.renderedElements = new Map();
this.itemHeights = new Map(); // Cache actual heights
this.itemPositions = new Map(); // Cache positions
this.totalHeight = 0;
this.onRenderItem = options.onRenderItem || function () {};
this.sanitizer = options.sanitizer || null; // Content sanitizer
this._initStructure();
this._attachScrollListener();
}
return _createClass(SimpleVirtualScroller, [{
key: "_initStructure",
value: function _initStructure() {
var _this = this;
var existingClasses = this.container.className;
this.container.innerHTML = '';
this.container.className = existingClasses;
this.container.style.position = 'relative';
this.container.style.overflow = 'auto';
// Ensure container has height
if (this.container.offsetHeight === 0) {
// Try to get height from computed style or parent
var computedHeight = window.getComputedStyle(this.container).height;
if (computedHeight === '0px' || computedHeight === 'auto') {
// If still no height, use a reasonable default
this.container.style.height = '400px';
console.warn('QuikChat Virtual Scrolling: Container has no height, setting to 400px');
}
}
this.spacer = document.createElement('div');
this.spacer.style.cssText = 'position: absolute; top: 0; left: 0; width: 1px; pointer-events: none; z-index: -1;';
this.content = document.createElement('div');
this.content.style.cssText = 'position: relative; width: 100%;';
this.container.appendChild(this.spacer);
this.container.appendChild(this.content);
// Initial render after structure is set up
setTimeout(function () {
_this._updateVisibleRange();
_this._renderVisibleItems();
}, 0);
}
}, {
key: "_attachScrollListener",
value: function _attachScrollListener() {
var _this2 = this;
var ticking = false;
this.container.addEventListener('scroll', function () {
if (!ticking) {
requestAnimationFrame(function () {
_this2._updateVisibleRange();
_this2._renderVisibleItems();
ticking = false;
});
ticking = true;
}
});
}
}, {
key: "_getItemHeight",
value: function _getItemHeight(index) {
// Return cached height or estimate
return this.itemHeights.get(index) || this.itemHeight;
}
}, {
key: "_getItemPosition",
value: function _getItemPosition(index) {
// Return cached position or calculate
if (this.itemPositions.has(index)) {
return this.itemPositions.get(index);
}
// Calculate position based on previous items
var position = 0;
for (var i = 0; i < index; i++) {
position += this._getItemHeight(i);
}
this.itemPositions.set(index, position);
return position;
}
}, {
key: "_recalculatePositions",
value: function _recalculatePositions(fromIndex) {
// Recalculate positions from a specific index onwards
var position = fromIndex > 0 ? this._getItemPosition(fromIndex) : 0;
for (var i = fromIndex; i < this.items.length; i++) {
this.itemPositions.set(i, position);
position += this._getItemHeight(i);
// Update position of rendered elements
var element = this.renderedElements.get(i);
if (element) {
element.style.top = "".concat(this.itemPositions.get(i), "px");
}
}
// Update total height
this.totalHeight = position;
this.spacer.style.height = "".concat(this.totalHeight, "px");
}
}, {
key: "_updateVisibleRange",
value: function _updateVisibleRange() {
var scrollTop = this.container.scrollTop;
var viewportHeight = this.container.clientHeight;
// If container has no height, don't render anything (avoid infinite loop)
if (viewportHeight === 0) {
this.visibleRange = {
start: 0,
end: 0
};
return;
}
// Find first visible item based on positions
var startIndex = 0;
var endIndex = this.items.length;
// Use cached positions when available
if (this.itemPositions.size > 0) {
// Find start index
for (var i = 0; i < this.items.length; i++) {
var pos = this._getItemPosition(i);
if (pos + this._getItemHeight(i) > scrollTop) {
startIndex = Math.max(0, i - this.buffer);
break;
}
}
// Find end index
for (var _i = startIndex; _i < this.items.length; _i++) {
var _pos = this._getItemPosition(_i);
if (_pos > scrollTop + viewportHeight) {
endIndex = Math.min(this.items.length, _i + this.buffer);
break;
}
}
} else {
// Fallback to estimate if no positions cached yet
startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
endIndex = Math.min(this.items.length, Math.ceil((scrollTop + viewportHeight) / this.itemHeight) + this.buffer);
}
this.visibleRange = {
start: startIndex,
end: endIndex
};
}
}, {
key: "_renderVisibleItems",
value: function _renderVisibleItems() {
var _this3 = this;
var _this$visibleRange = this.visibleRange,
start = _this$visibleRange.start,
end = _this$visibleRange.end;
// Remove elements outside range
this.renderedElements.forEach(function (element, index) {
if (index < start || index >= end) {
element.remove();
_this3.renderedElements["delete"](index);
// Don't clear height cache - we might need it again
}
});
// Add new visible elements
var _loop = function _loop(i) {
var item = _this3.items[i];
if (!item || _this3.renderedElements.has(i)) return 1; // continue
var element = _this3._createItemElement(item, i);
element.style.position = 'absolute';
element.style.top = "".concat(_this3._getItemPosition(i), "px");
element.style.left = '0';
element.style.right = '0';
_this3.renderedElements.set(i, element);
_this3.content.appendChild(element);
// Measure actual height after rendering
requestAnimationFrame(function () {
if (_this3.renderedElements.has(i)) {
var actualHeight = element.offsetHeight;
if (actualHeight && actualHeight !== _this3._getItemHeight(i)) {
_this3.itemHeights.set(i, actualHeight);
_this3._recalculatePositions(i);
}
}
});
};
for (var i = start; i < end; i++) {
if (_loop(i)) continue;
}
this.onRenderItem(this.content);
}
}, {
key: "_createItemElement",
value: function _createItemElement(item, index) {
var messageDiv = document.createElement('div');
messageDiv.className = "quikchat-message quikchat-message-".concat(item.align || 'left', " quikchat-msgid-").concat(String(item.msgid).padStart(10, '0'));
messageDiv.setAttribute('data-index', index);
messageDiv.setAttribute('data-msgid', item.msgid);
messageDiv.setAttribute('role', 'article');
messageDiv.setAttribute('aria-label', "Message from ".concat(item.userString || 'user'));
if (item.tags && item.tags.length > 0) {
item.tags.forEach(function (tag) {
return messageDiv.classList.add("quikchat-tag-".concat(tag));
});
}
var userDiv = document.createElement('div');
userDiv.className = 'quikchat-message-user';
userDiv.innerHTML = this.sanitizer ? this.sanitizer(item.userString || '') : item.userString || '';
userDiv.setAttribute('aria-label', 'User');
var contentDiv = document.createElement('div');
contentDiv.className = 'quikchat-message-content';
contentDiv.innerHTML = this.sanitizer ? this.sanitizer(item.content || '') : item.content || '';
contentDiv.setAttribute('aria-label', 'Message content');
messageDiv.appendChild(userDiv);
messageDiv.appendChild(contentDiv);
if (!item.visible) {
messageDiv.style.display = 'none';
}
return messageDiv;
}
}, {
key: "addItem",
value: function addItem(item) {
this.items.push(item);
var index = this.items.length - 1;
// Calculate position for new item
var position = index > 0 ? this._getItemPosition(index - 1) + this._getItemHeight(index - 1) : 0;
this.itemPositions.set(index, position);
// Update total height (using estimate for new item)
this.totalHeight = position + this.itemHeight;
this.spacer.style.height = "".concat(this.totalHeight, "px");
if (index >= this.visibleRange.start && index < this.visibleRange.end) {
this._renderVisibleItems();
}
if (item.scrollIntoView) {
this.container.scrollTop = this.container.scrollHeight;
}
return index;
}
}, {
key: "addItems",
value: function addItems(items) {
var _this$items;
// Batch add items for better performance
var startLength = this.items.length;
(_this$items = this.items).push.apply(_this$items, _toConsumableArray(items));
// Calculate positions for new items
var position = startLength > 0 ? this._getItemPosition(startLength - 1) + this._getItemHeight(startLength - 1) : 0;
for (var i = startLength; i < this.items.length; i++) {
this.itemPositions.set(i, position);
position += this.itemHeight; // Use estimate for new items
}
this.totalHeight = position;
this.spacer.style.height = "".concat(this.totalHeight, "px");
// Update visible range and render
this._updateVisibleRange();
this._renderVisibleItems();
// Handle scrollIntoView for last item if needed
if (items.length > 0 && items[items.length - 1].scrollIntoView) {
this.container.scrollTop = this.container.scrollHeight;
}
}
}, {
key: "clear",
value: function clear() {
this.items = [];
this.renderedElements.clear();
this.itemHeights.clear();
this.itemPositions.clear();
this.totalHeight = 0;
this.content.innerHTML = '';
this.spacer.style.height = '0px';
this.visibleRange = {
start: 0,
end: 0
};
// Force update after clear
this._updateVisibleRange();
this._renderVisibleItems();
}
}, {
key: "updateItem",
value: function updateItem(index, updates) {
if (index >= 0 && index < this.items.length) {
this.items[index] = _objectSpread2(_objectSpread2({}, this.items[index]), updates);
if (this.renderedElements.has(index)) {
this._renderVisibleItems();
}
}
}
}, {
key: "destroy",
value: function destroy() {
this.container.innerHTML = '';
this.items = [];
this.renderedElements.clear();
}
}]);
}();
/**
* QuikChat - A zero-dependency JavaScript chat widget for modern web applications
* @class quikchat
*/
var quikchat = /*#__PURE__*/function () {
/**
* Creates a new QuikChat instance
* @constructor
* @param {string|HTMLElement} parentElement - CSS selector or DOM element to attach the chat widget to
* @param {Function} [onSend] - Callback function triggered when user sends a message
* @param {Object} [options] - Configuration options
* @param {string} [options.theme='quikchat-theme-light'] - CSS theme class name
* @param {boolean} [options.trackHistory=true] - Whether to track message history
* @param {Object} [options.titleArea] - Title area configuration
* @param {string} [options.titleArea.title='Chat'] - Title text/HTML
* @param {boolean} [options.titleArea.show=false] - Whether to show title area initially
* @param {'left'|'center'|'right'} [options.titleArea.align='center'] - Title alignment
* @param {Object} [options.messagesArea] - Messages area configuration
* @param {boolean} [options.messagesArea.alternating=true] - Alternate message colors
* @param {Object} [options.inputArea] - Input area configuration
* @param {boolean} [options.inputArea.show=true] - Whether to show input area initially
* @param {boolean} [options.sendOnEnter=true] - Send message on Enter key
* @param {boolean} [options.sendOnShiftEnter=false] - Send message on Shift+Enter
* @param {string} [options.instanceClass=''] - Additional CSS class for the widget instance
* @example
* // Basic usage
* const chat = new quikchat('#chat-container', (instance, message) => {
* console.log('User sent:', message);
* });
*
* @example
* // With options
* const chat = new quikchat('#chat', handleMessage, {
* theme: 'quikchat-theme-dark',
* titleArea: { title: 'Support Chat', show: true },
* sendOnEnter: false,
* sendOnShiftEnter: true
* });
*/
function quikchat(parentElement) {
var onSend = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {};
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_classCallCheck(this, quikchat);
var defaultOpts = {
theme: 'quikchat-theme-light',
trackHistory: true,
titleArea: {
title: "Chat",
show: false,
align: "center"
},
messagesArea: {
alternating: true
},
inputArea: {
show: true
},
sendOnEnter: true,
sendOnShiftEnter: false,
instanceClass: '',
virtualScrolling: true,
// Default to true for better performance
virtualScrollingThreshold: 500,
// Lower threshold since it performs so well
// i18n support
lang: 'en',
dir: 'ltr',
// 'ltr' or 'rtl'
translations: {
'en': {
sendButton: 'Send',
inputPlaceholder: 'Type a message...',
titleDefault: 'Chat'
}
},
// Security: content sanitizer callback
sanitizer: null // null = no sanitization (backward compatible)
};
var meta = _objectSpread2(_objectSpread2({}, defaultOpts), options); // merge options with defaults
// Merge user translations with defaults
if (options.translations) {
meta.translations = _objectSpread2(_objectSpread2({}, defaultOpts.translations), options.translations);
}
if (typeof parentElement === 'string') {
parentElement = document.querySelector(parentElement);
}
//console.log(parentElement, meta);
this._parentElement = parentElement;
this._theme = meta.theme;
this._onSend = onSend ? onSend : function () {}; // call back function for onSend
// i18n settings
this.lang = meta.lang;
this.dir = meta.dir;
this.translations = meta.translations;
this.currentTranslations = this.translations[this.lang] || this.translations['en'];
this._createWidget();
if (meta.instanceClass) {
this._chatWidget.classList.add(meta.instanceClass);
}
// title area
if (meta.titleArea) {
this.titleAreaSetContents(meta.titleArea.title, meta.titleArea.align);
if (meta.titleArea.show === true) {
this.titleAreaShow();
} else {
this.titleAreaHide();
}
}
// messages area
if (meta.messagesArea) {
this.messagesAreaAlternateColors(meta.messagesArea.alternating);
}
// input area
if (meta.inputArea) {
if (meta.inputArea.show === true) this.inputAreaShow();else this.inputAreaHide();
}
// plumbing
this._attachEventListeners();
this.trackHistory = meta.trackHistory || true;
this._historyLimit = 10000000;
this._history = [];
this._activeTags = new Set();
// send on enter / shift enter
this.sendOnEnter = meta.sendOnEnter;
this.sendOnShiftEnter = meta.sendOnShiftEnter;
// Virtual scrolling setup
this.virtualScrollingEnabled = meta.virtualScrolling;
this.virtualScrollingThreshold = meta.virtualScrollingThreshold;
this.virtualScroller = null;
// Sanitizer setup
this._sanitizer = meta.sanitizer || null;
// Don't initialize virtual scrolling immediately - wait for threshold
// Virtual scrolling will be initialized when message count exceeds threshold
}
/**
* Initialize virtual scrolling
* @private
*/
return _createClass(quikchat, [{
key: "_initVirtualScrolling",
value: function _initVirtualScrolling() {
var _this4 = this;
if (this.virtualScrollingEnabled && this._messagesArea && !this.virtualScroller) {
// Check if we've hit the threshold (or about to with the next message)
if (this._history.length >= this.virtualScrollingThreshold - 1) {
this.virtualScroller = new SimpleVirtualScroller(this._messagesArea, {
itemHeight: 80,
buffer: 5,
sanitizer: this._sanitizer,
// Pass sanitizer to virtual scroller
onRenderItem: function onRenderItem(content) {
// Apply alternating colors if enabled
if (_this4._messagesArea.classList.contains('quikchat-messages-area-alt')) {
_this4._updateMessageStyles();
}
}
});
// Migrate existing messages to virtual scroller
this._migrateToVirtualScrolling();
}
}
}
/**
* Migrate existing messages to virtual scrolling
* @private
*/
}, {
key: "_migrateToVirtualScrolling",
value: function _migrateToVirtualScrolling() {
var _this5 = this;
if (!this.virtualScroller) return;
// Don't clear DOM here - virtual scroller will do it in _initStructure
// Add all messages to virtual scroller
var items = this._history.map(function (msg) {
return {
msgid: msg.msgid,
content: msg.content,
userString: msg.userString,
align: msg.align,
role: msg.role,
visible: msg.visible,
tags: msg.tags || [],
scrollIntoView: false
};
});
this.virtualScroller.addItems(items);
// Force a render in case container needs time to get dimensions
setTimeout(function () {
if (_this5.virtualScroller) {
_this5.virtualScroller._updateVisibleRange();
_this5.virtualScroller._renderVisibleItems();
}
}, 10);
}
}, {
key: "_createWidget",
value: function _createWidget() {
var sendButtonText = this.currentTranslations.sendButton || 'Send';
var inputPlaceholder = this.currentTranslations.inputPlaceholder || 'Type a message...';
var widgetHTML = "\n <div class=\"quikchat-base ".concat(this._theme, "\" dir=\"").concat(this.dir, "\" lang=\"").concat(this.lang, "\" role=\"region\" aria-label=\"Chat widget\">\n <div class=\"quikchat-title-area\" role=\"heading\" aria-level=\"2\">\n <span style=\"font-size: 1.5em; font-weight: 600;\">Title Area</span>\n </div>\n <div class=\"quikchat-messages-area\" role=\"log\" aria-live=\"polite\" aria-label=\"Chat messages\"></div>\n <div class=\"quikchat-input-area\" role=\"form\" aria-label=\"Message input\">\n <textarea class=\"quikchat-input-textbox\" \n placeholder=\"").concat(inputPlaceholder, "\"\n aria-label=\"Type your message\"\n autocomplete=\"off\"\n autocapitalize=\"sentences\"></textarea>\n <button class=\"quikchat-input-send-btn\" \n aria-label=\"Send message\"\n type=\"button\">").concat(sendButtonText, "</button>\n </div>\n </div>\n ");
this._parentElement.innerHTML = widgetHTML;
this._chatWidget = this._parentElement.querySelector('.quikchat-base');
this._titleArea = this._chatWidget.querySelector('.quikchat-title-area');
this._messagesArea = this._chatWidget.querySelector('.quikchat-messages-area');
this._inputArea = this._chatWidget.querySelector('.quikchat-input-area');
this._textEntry = this._inputArea.querySelector('.quikchat-input-textbox');
this._sendButton = this._inputArea.querySelector('.quikchat-input-send-btn');
this.msgid = 0;
// Add mobile viewport handling
this._setupMobileSupport();
}
/**
* Setup mobile support - prevent zoom on input focus and handle virtual keyboard
* @private
*/
}, {
key: "_setupMobileSupport",
value: function _setupMobileSupport() {
var _this6 = this;
// Prevent zoom on input focus for mobile
var meta = document.querySelector('meta[name="viewport"]');
if (!meta) {
meta = document.createElement('meta');
meta.name = 'viewport';
document.head.appendChild(meta);
}
// Store original content
this._originalViewport = meta.content;
// Prevent zoom on focus
this._textEntry.addEventListener('focus', function () {
if (window.innerWidth <= 768) {
// Mobile device width
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
}
});
this._textEntry.addEventListener('blur', function () {
if (_this6._originalViewport) {
meta.content = _this6._originalViewport;
} else {
meta.content = 'width=device-width, initial-scale=1.0';
}
});
// Handle virtual keyboard resize
if ('visualViewport' in window) {
window.visualViewport.addEventListener('resize', function () {
_this6._handleVirtualKeyboard();
});
}
}
/**
* Handle virtual keyboard appearance/disappearance
* @private
*/
}, {
key: "_handleVirtualKeyboard",
value: function _handleVirtualKeyboard() {
// Adjust layout when virtual keyboard appears
var keyboardHeight = window.innerHeight - window.visualViewport.height;
if (keyboardHeight > 0) {
// Keyboard is visible - adjust chat widget height
this._chatWidget.style.paddingBottom = "".concat(keyboardHeight, "px");
} else {
// Keyboard hidden - restore original padding
this._chatWidget.style.paddingBottom = '';
}
}
/**
* Attach event listeners to the widget
*/
}, {
key: "_attachEventListeners",
value: function _attachEventListeners() {
var _this7 = this;
this._sendButton.addEventListener('click', function (event) {
event.preventDefault();
_this7._onSend(_this7, _this7._textEntry.value.trim());
});
window.addEventListener('resize', function () {
return _this7._handleContainerResize();
});
this._chatWidget.addEventListener('resize', function () {
return _this7._handleContainerResize();
});
this._textEntry.addEventListener('keydown', function (event) {
// Check if Shift + Enter is pressed then we just do carraige
if (event.shiftKey && event.keyCode === 13) {
// Prevent default behavior (adding new line)
if (_this7.sendOnShiftEnter) {
event.preventDefault();
_this7._onSend(_this7, _this7._textEntry.value.trim());
}
} else if (event.keyCode === 13) {
// Enter but not Shift + Enter
if (_this7.sendOnEnter) {
event.preventDefault();
_this7._onSend(_this7, _this7._textEntry.value.trim());
}
}
});
this._messagesArea.addEventListener('scroll', function () {
var _this7$_messagesArea = _this7._messagesArea,
scrollTop = _this7$_messagesArea.scrollTop,
scrollHeight = _this7$_messagesArea.scrollHeight,
clientHeight = _this7$_messagesArea.clientHeight;
_this7.userScrolledUp = scrollTop + clientHeight < scrollHeight;
});
}
// set the onSend function callback.
}, {
key: "setCallbackOnSend",
value: function setCallbackOnSend(callback) {
this._onSend = callback;
}
// set a callback for everytime a message is added (listener)
}, {
key: "setCallbackonMessageAdded",
value: function setCallbackonMessageAdded(callback) {
this._onMessageAdded = callback;
}
/**
* Sets the callback function for when content is appended to a message
* @param {Function} callback - Function to call when content is appended
* @param {quikchat} callback.instance - The QuikChat instance
* @param {number} callback.msgId - The ID of the message being appended to
* @param {string} callback.content - The content being appended
* @since 1.1.15
* @example
* chat.setCallbackonMessageAppend((instance, msgId, content) => {
* console.log(`Appended "${content}" to message ${msgId}`);
* });
*/
}, {
key: "setCallbackonMessageAppend",
value: function setCallbackonMessageAppend(callback) {
this._onMessageAppend = callback;
}
/**
* Sets the callback function for when a message's content is replaced
* @param {Function} callback - Function to call when content is replaced
* @param {quikchat} callback.instance - The QuikChat instance
* @param {number} callback.msgId - The ID of the message being replaced
* @param {string} callback.content - The new content
* @since 1.1.15
* @example
* chat.setCallbackonMessageReplace((instance, msgId, content) => {
* console.log(`Message ${msgId} replaced with: ${content}`);
* });
*/
}, {
key: "setCallbackonMessageReplace",
value: function setCallbackonMessageReplace(callback) {
this._onMessageReplace = callback;
}
/**
* Sets the callback function for when a message is deleted
* @param {Function} callback - Function to call when a message is deleted
* @param {quikchat} callback.instance - The QuikChat instance
* @param {number} callback.msgId - The ID of the deleted message
* @since 1.1.15
* @example
* chat.setCallbackonMessageDelete((instance, msgId) => {
* console.log(`Message ${msgId} was deleted`);
* });
*/
}, {
key: "setCallbackonMessageDelete",
value: function setCallbackonMessageDelete(callback) {
this._onMessageDelete = callback;
}
// Public methods
/**
* Toggles the visibility of the title area
* @returns {void}
*/
}, {
key: "titleAreaToggle",
value: function titleAreaToggle() {
this._titleArea.style.display === 'none' ? this.titleAreaShow() : this.titleAreaHide();
}
/**
* Shows the title area
* @returns {void}
*/
}, {
key: "titleAreaShow",
value: function titleAreaShow() {
this._titleArea.style.display = '';
this._adjustMessagesAreaHeight();
}
/**
* Hides the title area
* @returns {void}
*/
}, {
key: "titleAreaHide",
value: function titleAreaHide() {
this._titleArea.style.display = 'none';
this._adjustMessagesAreaHeight();
}
/**
* Sets the contents of the title area
* @param {string} title - HTML content to display in the title area
* @param {'left'|'center'|'right'} [align='center'] - Text alignment
* @returns {void}
* @example
* chat.titleAreaSetContents('<h2>Support Chat</h2>', 'center');
*/
}, {
key: "titleAreaSetContents",
value: function titleAreaSetContents(title) {
var align = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'center';
this._titleArea.innerHTML = this._sanitizeContent(title);
this._titleArea.style.textAlign = align;
}
/**
* Gets the current contents of the title area
* @returns {string} The HTML content of the title area
*/
}, {
key: "titleAreaGetContents",
value: function titleAreaGetContents() {
return this._titleArea.innerHTML;
}
}, {
key: "inputAreaToggle",
value: function inputAreaToggle() {
this._inputArea.classList.toggle('hidden');
this._inputArea.style.display === 'none' ? this.inputAreaShow() : this.inputAreaHide();
}
}, {
key: "inputAreaShow",
value: function inputAreaShow() {
this._inputArea.style.display = '';
this._adjustMessagesAreaHeight();
}
}, {
key: "inputAreaHide",
value: function inputAreaHide() {
this._inputArea.style.display = 'none';
this._adjustMessagesAreaHeight();
}
}, {
key: "_adjustMessagesAreaHeight",
value: function _adjustMessagesAreaHeight() {
var hiddenElements = _toConsumableArray(this._chatWidget.children).filter(function (child) {
return child.classList.contains('hidden');
});
var totalHiddenHeight = hiddenElements.reduce(function (sum, child) {
return sum + child.offsetHeight;
}, 0);
var containerHeight = this._chatWidget.offsetHeight;
this._messagesArea.style.height = "calc(100% - ".concat(containerHeight - totalHiddenHeight, "px)");
}
}, {
key: "_handleContainerResize",
value: function _handleContainerResize() {
this._adjustMessagesAreaHeight();
this._adjustSendButtonWidth();
return true;
}
}, {
key: "_adjustSendButtonWidth",
value: function _adjustSendButtonWidth() {
var sendButtonText = this._sendButton.textContent.trim();
var fontSize = parseFloat(getComputedStyle(this._sendButton).fontSize);
var minWidth = fontSize * sendButtonText.length + 16;
this._sendButton.style.minWidth = "".concat(minWidth, "px");
return true;
}
//messagesArea functions
}, {
key: "messagesAreaAlternateColors",
value: function messagesAreaAlternateColors() {
var alt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
if (alt) {
this._messagesArea.classList.add('quikchat-messages-area-alt');
} else {
this._messagesArea.classList.remove('quikchat-messages-area-alt');
}
return alt === true;
}
}, {
key: "messagesAreaAlternateColorsToggle",
value: function messagesAreaAlternateColorsToggle() {
this._messagesArea.classList.toggle('quikchat-messages-area-alt');
}
}, {
key: "messagesAreaAlternateColorsGet",
value: function messagesAreaAlternateColorsGet() {
return this._messagesArea.classList.contains('quikchat-messages-area-alt');
}
/**
* Adds a new message to the chat with full configuration options
* @param {Object} input - Message configuration object
* @param {string} [input.content=''] - Message content (HTML allowed)
* @param {string} [input.userString='user'] - Display name for the message sender
* @param {'left'|'right'|'center'} [input.align='right'] - Message alignment
* @param {string} [input.role='user'] - Role identifier (user, assistant, system)
* @param {number} [input.userID=-1] - User ID for the message
* @param {string|false} [input.timestamp=false] - ISO timestamp or false for auto
* @param {string|false} [input.updatedtime=false] - Last updated timestamp
* @param {boolean|'smart'} [input.scrollIntoView=true] - Scroll behavior (true/false/'smart')
* @param {boolean} [input.visible=true] - Whether message is initially visible
* @param {string[]} [input.tags=[]] - Tags for message categorization
* @returns {number} Message ID for the newly added message
* @example
* const msgId = chat.messageAddFull({
* content: 'Hello!',
* userString: 'Bot',
* align: 'left',
* scrollIntoView: 'smart',
* tags: ['greeting']
* });
*/
}, {
key: "messageAddFull",
value: function messageAddFull() {
var _this8 = this;
var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
content: "",
userString: "user",
align: "right",
role: "user",
userID: -1,
timestamp: false,
updatedtime: false,
scrollIntoView: true,
visible: true,
tags: []
};
var msgid = this.msgid;
this.msgid++;
var messageDiv = null; // Initialize messageDiv to null
// Check if we should initialize virtual scrolling
if (this.virtualScrollingEnabled && !this.virtualScroller && this._history.length >= this.virtualScrollingThreshold - 1) {
this._initVirtualScrolling();
}
// If virtual scrolling is enabled, use the virtual scroller
if (this.virtualScroller) {
var messageData = {
msgid: msgid,
content: input.content,
userString: input.userString,
align: input.align,
role: input.role,
visible: input.visible !== undefined ? input.visible : true,
tags: input.tags || [],
scrollIntoView: input.scrollIntoView
};
// Add tags to active tags set
if (Array.isArray(input.tags)) {
input.tags.forEach(function (tag) {
if (typeof tag === 'string' && /^[a-zA-Z0-9-]+$/.test(tag)) {
_this8._activeTags.add(tag);
}
});
}
// Add to virtual scroller
this.virtualScroller.addItem(messageData);
// Clear text entry
this._textEntry.value = '';
this._adjustMessagesAreaHeight();
} else {
// Original DOM-based implementation
messageDiv = document.createElement('div');
var msgidClass = 'quikchat-msgid-' + String(msgid).padStart(10, '0');
'quikchat-userid-' + String(input.userString).padStart(10, '0'); // hash this..
messageDiv.classList.add('quikchat-message', msgidClass, 'quikchat-structure');
messageDiv.setAttribute('role', 'article');
messageDiv.setAttribute('aria-label', "Message from ".concat(input.userString || 'user'));
if (Array.isArray(input.tags)) {
input.tags.forEach(function (tag) {
if (typeof tag === 'string' && /^[a-zA-Z0-9-]+$/.test(tag)) {
messageDiv.classList.add("quikchat-tag-".concat(tag));
_this8._activeTags.add(tag);
}
});
}
var userDiv = document.createElement('div');
userDiv.innerHTML = this._sanitizeContent(input.userString);
userDiv.classList.add('quikchat-user-label');
userDiv.style.textAlign = input.align;
var contentDiv = document.createElement('div');
contentDiv.classList.add('quikchat-message-content');
// Determine text alignment for right-aligned messages
if (input.align === "right") {
var isMultiLine = input.content.includes("\n");
var isLong = input.content.length > 50; // Adjust length threshold
if (isMultiLine || isLong) {
contentDiv.classList.add("quikchat-right-multiline");
} else {
contentDiv.classList.add("quikchat-right-singleline");
}
}
contentDiv.innerHTML = this._sanitizeContent(input.content);
messageDiv.appendChild(userDiv);
messageDiv.appendChild(contentDiv);
this._messagesArea.appendChild(messageDiv);
if (input.visible === false) {
messageDiv.style.display = 'none';
}
// Handle scroll behavior based on scrollIntoView parameter
// 'smart' = only scroll if near bottom, true = always scroll, false = never scroll
if (input.scrollIntoView === true) {
this.messageScrollToBottom();
} else if (input.scrollIntoView === 'smart' && !this.userScrolledUp) {
this.messageScrollToBottom();
}
// If scrollIntoView is false, don't scroll at all
this._textEntry.value = '';
this._adjustMessagesAreaHeight();
this._handleShortLongMessageCSS(messageDiv, input.align); // Handle CSS for short/long messages
this._updateMessageStyles();
}
// Add timestamp now, unless it is passed in
var timestamp = input.timestamp ? input.timestamp : new Date().toISOString();
var updatedtime = input.updatedtime ? input.updatedtime : timestamp;
var visible = input.visible !== undefined ? input.visible : true;
if (this.trackHistory) {
this._history.push(_objectSpread2(_objectSpread2({
msgid: msgid
}, input), {}, {
visible: visible,
timestamp: timestamp,
updatedtime: updatedtime,
messageDiv: messageDiv || null
}));
if (this._history.length > this._historyLimit) {
this._history.shift();
}
}
if (this._onMessageAdded) {
this._onMessageAdded(this, msgid);
}
return msgid;
}
/**
* Adds a new message to the chat (simplified version of messageAddFull)
* @param {string} [content=''] - Message content (HTML allowed)
* @param {string} [userString='user'] - Display name for the message sender
* @param {'left'|'right'|'center'} [align='right'] - Message alignment
* @param {string} [role='user'] - Role identifier (user, assistant, system)
* @param {boolean|'smart'} [scrollIntoView=true] - Scroll behavior
* @param {boolean} [visible=true] - Whether message is initially visible
* @param {string[]} [tags=[]] - Tags for message categorization
* @returns {number} Message ID for the newly added message
* @example
* // Simple message
* chat.messageAddNew('Hello!', 'User', 'right');
*
* // Bot message with smart scroll
* chat.messageAddNew('Hi there!', 'Bot', 'left', 'assistant', 'smart');
*/
}, {
key: "messageAddNew",
value: function messageAddNew() {
var content = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
var userString = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "user";
var align = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "right";
var role = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "user";
var scrollIntoView = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
var visible = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
var tags = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : [];
var retvalue = this.messageAddFull({
content: content,
userString: userString,
align: align,
role: role,
scrollIntoView: scrollIntoView,
visible: visible,
tags: tags
});
// this.messageScrollToBottom();
return retvalue;
}
/**
* Removes a message from the chat by its ID
* @param {number} n - Message ID to remove
* @returns {boolean} True if message was removed, false if not found
* @example
* const success = chat.messageRemove(5);
*/
}, {
key: "messageRemove",
value: function messageRemove(n) {
// use css selector to remove the message
var sucess = false;
try {
this._messagesArea.removeChild(this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0'))));
sucess = true;
} catch (error) {
console.log("{String(n)} : Message ID not found");
}
if (sucess) {
// slow way to remove from history
//this._history = this._history.filter((item) => item.msgid !== n); // todo make this more efficient
// better way to delete this from history
this._history.splice(this._history.findIndex(function (item) {
return item.msgid === n;
}), 1);
// Call the onMessageDelete callback if it exists
if (this._onMessageDelete) {
this._onMessageDelete(this, n);
}
}
return sucess;
}
/* returns the message html object from the DOM
*/
/**
* Gets the DOM element for a message by its ID
* @param {number} n - Message ID
* @returns {HTMLElement|null} The message DOM element or null if not found
*/
}, {
key: "messageGetDOMObject",
value: function messageGetDOMObject(n) {
var msg = null;
// now use css selector to get the message
try {
msg = this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0')));
} catch (error) {
console.log("{String(n)} : Message ID not found");
}
return msg;
}
/* returns the message content only
*/
/**
* Gets the content of a message by its ID
* @param {number} n - Message ID
* @returns {string} The message content or empty string if not found
*/
}, {
key: "messageGetContent",
value: function messageGetContent(n) {
var content = "";
// now use css selector to get the message
try {
// get from history..
content = this._history.filter(function (item) {
return item.msgid === n;
})[0].content;
//content = this._messagesArea.querySelector(`.quikchat-msgid-${String(n).padStart(10, '0')}`).lastChild.textContent;
} catch (error) {
console.log("{String(n)} : Message ID not found");
}
return content;
}
/* returns the DOM Content element of a given message
*/
}, {
key: "messageGetContentDOMElement",
value: function messageGetContentDOMElement(n) {
var contentElement = null;
// now use css selector to get the message
try {
//contentElement = this._messagesArea.querySelector(`.quikchat-msgid-${String(n).padStart(10, '0')}`).lastChild;
contentElement = this._history.filter(function (item) {
return item.msgid === n;
})[0].messageDiv.lastChild;
} catch (error) {
console.log("{String(n)} : Message ID not found");
}
return contentElement;
}
/* append message to the message content
*/
}, {
key: "messageAppendContent",
value: function messageAppendContent(n, content) {
var success = false;
try {
// Update history first
var item = this._history.filter(function (item) {
return item.msgid === n;
})[0];
item.content += content;
item.updatedtime = new Date().toISOString();
// If virtual scrolling is active, update the virtual scroller
if (this.virtualScroller) {
// Find the item index in virtual scroller
var index = this.virtualScroller.items.findIndex(function (item) {
return item.msgid === n;
});
if (index >= 0) {
this.virtualScroller.items[index].content += content; // Don't double-sanitize, it's done on render
// Re-render if the item is currently visible
this.virtualScroller.updateItem(index, {
content: this.virtualScroller.items[index].content
});
}
} else {
// Regular DOM manipulation
this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0'))).lastChild.innerHTML += this._sanitizeContent(content);
}
success = true;
// Call the onMessageAppend callback if it exists
if (this._onMessageAppend) {
this._onMessageAppend(this, n, content);
}
// Don't auto-scroll on append - let user control this
// Users can call messageScrollToBottom() if they want to scroll
} catch (error) {
console.log("".concat(String(n), " : Message ID not found"));
}
return success;
}
/* replace message content
*/
}, {
key: "messageReplaceContent",
value: function messageReplaceContent(n, content) {
var success = false;
try {
// Update history first
var item = this._history.filter(function (item) {
return item.msgid === n;
})[0];
item.content = content;
item.updatedtime = new Date().toISOString();
// If virtual scrolling is active, update the virtual scroller
if (this.virtualScroller) {
// Find the item index in virtual scroller
var index = this.virtualScroller.items.findIndex(function (item) {
return item.msgid === n;
});
if (index >= 0) {
this.virtualScroller.items[index].content = content; // Don't double-sanitize, it's done on render
// Re-render if the item is currently visible
this.virtualScroller.updateItem(index, {
content: content
});
}
} else {
// Regular DOM manipulation
this._messagesArea.querySelector(".quikchat-msgid-".concat(String(n).padStart(10, '0'))).lastChild.innerHTML = this._sanitizeContent(content);
}
success = true;
// Call the onMessageReplace callback if it exists
if (this._onMessageReplace) {
this._onMessageReplace(this, n, content);
}
// Don't auto-scroll on append - let user control this
// Users can call messageScrollToBottom() if they want to scroll
} catch (error) {
console.log("".concat(String(n), " : Message ID not found"));
}
return success;
}
/**
* Scrolls the messages area to the bottom.
*/
}, {
key: "messageScrollToBottom",
value: function messageScrollToBottom() {
// Always use scrollTop to avoid page jumping
// This ensures only the chat container scrolls, not the entire page
this._messagesArea.scrollTop = this._messagesArea.scrollHeight;
}
/**
* Removes the last message from the messages area.
*/
}, {
key: "messageRemo