UNPKG

quikchat

Version:

quikchat is a simple vanilla (no dependancies) JavaScript chat control for browsers

1,339 lines (1,260 loc) 80.4 kB
'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