diff --git a/app/Http/Controllers/ConversationsController.php b/app/Http/Controllers/ConversationsController.php index e1bbeeef4..ef41d2806 100755 --- a/app/Http/Controllers/ConversationsController.php +++ b/app/Http/Controllers/ConversationsController.php @@ -2821,7 +2821,6 @@ public function searchCustomers($request, $user) ->orWhere('customers.last_name', $like_op, $like) ->orWhere('customers.company', $like_op, $like) ->orWhere('customers.job_title', $like_op, $like) - ->orWhere('customers.phones', $like_op, '%"'.\Helper::phoneToNumeric($q).'"%') ->orWhere('customers.websites', $like_op, $like) ->orWhere('customers.social_profiles', $like_op, $like) ->orWhere('customers.address', $like_op, $like) @@ -2829,6 +2828,12 @@ public function searchCustomers($request, $user) ->orWhere('customers.state', $like_op, $like) ->orWhere('customers.zip', $like_op, $like) ->orWhere('emails.email', $like_op, $like); + + $phone_numeric = \Helper::phoneToNumeric($q); + + if ($phone_numeric) { + $query->orWhere('customers.phones', $like_op, '%"'.$phone_numeric.'"%'); + } }); if (!empty($filters['mailbox']) && in_array($filters['mailbox'], $mailbox_ids)) { diff --git a/app/Jobs/SendReplyToCustomer.php b/app/Jobs/SendReplyToCustomer.php index 7fd66d41f..97c8f14d5 100644 --- a/app/Jobs/SendReplyToCustomer.php +++ b/app/Jobs/SendReplyToCustomer.php @@ -421,6 +421,9 @@ public function handle() } try { + // https://github.com/freescout-helpdesk/freescout/issues/3502 + $imap_sent_folder = mb_convert_encoding($imap_sent_folder, "UTF7-IMAP","UTF-8"); + // https://github.com/Webklex/php-imap/issues/380 if (method_exists($client, 'getFolderByPath')) { $folder = $client->getFolderByPath($imap_sent_folder); @@ -431,18 +434,22 @@ public function handle() if ($folder) { try { $save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array); + // Sometimes emails with attachments by some reason are not saved. // https://github.com/freescout-helpdesk/freescout/issues/2749 if (!$save_result) { // Save without attachments. - $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array); + $save_result = $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array); + if (!$save_result) { + \Log::error('Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder); + } } } catch (\Exception $e) { // Just log error and continue. \Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: '); } } else { - \Log::error('Could not save outgoing reply to the IMAP folder (make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder); + \Log::error('Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder); } } catch (\Exception $e) { // Just log error and continue. diff --git a/app/Misc/Mail.php b/app/Misc/Mail.php index e4da973fe..0515982cb 100644 --- a/app/Misc/Mail.php +++ b/app/Misc/Mail.php @@ -896,6 +896,9 @@ public static function prepareMailable($mailable) public static function getImapFolder($client, $folder_name) { + // https://github.com/freescout-helpdesk/freescout/issues/3502 + $folder_name = mb_convert_encoding($folder_name, "UTF7-IMAP","UTF-8"); + if (method_exists($client, 'getFolderByPath')) { return $client->getFolderByPath($folder_name); } else { diff --git a/app/User.php b/app/User.php index b9c000436..67940a8ea 100644 --- a/app/User.php +++ b/app/User.php @@ -1212,18 +1212,18 @@ public static function getRobotsCondition() // https://github.com/freescout-helpdesk/freescout/issues/3489 public function setFirstNameAttribute($first_name) { - $this->attributes['first_name'] = mb_substr($first_name, 0, 20); + $this->attributes['first_name'] = mb_substr($first_name ?? '', 0, 20); } public function setLastNameAttribute($last_name) { - $this->attributes['last_name'] = mb_substr($last_name, 0, 30); + $this->attributes['last_name'] = mb_substr($last_name ?? '', 0, 30); } public function setEmailAttribute($email) { - $this->attributes['email'] = mb_substr($email, 0, 100); + $this->attributes['email'] = mb_substr($email ?? '', 0, 100); } public function setJobTitleAttribute($job_title) { - $this->attributes['job_title'] = mb_substr($job_title, 0, 100); + $this->attributes['job_title'] = mb_substr($job_title ?? '', 0, 100); } } diff --git a/config/app.php b/config/app.php index 3dd54c453..40af0f3cb 100644 --- a/config/app.php +++ b/config/app.php @@ -18,7 +18,7 @@ | or any other location as required by the application or its packages. */ - 'version' => '1.8.107', + 'version' => '1.8.108', /* |-------------------------------------------------------------------------- diff --git a/public/js/html5sortable.js b/public/js/html5sortable.js new file mode 100644 index 000000000..5603a2b19 --- /dev/null +++ b/public/js/html5sortable.js @@ -0,0 +1,1214 @@ +/* + * HTML5Sortable package + * https://github.com/lukasoppermann/html5sortable + * + * Maintained by Lukas Oppermann + * + * Released under the MIT license. + */ +var sortable = (function () { + 'use strict'; + + /** + * Get or set data on element + * @param {HTMLElement} element + * @param {string} key + * @param {any} value + * @return {*} + */ + function addData(element, key, value) { + if (value === undefined) { + return element && element.h5s && element.h5s.data && element.h5s.data[key]; + } + else { + element.h5s = element.h5s || {}; + element.h5s.data = element.h5s.data || {}; + element.h5s.data[key] = value; + } + } + /** + * Remove data from element + * @param {HTMLElement} element + */ + function removeData(element) { + if (element.h5s) { + delete element.h5s.data; + } + } + + /* eslint-env browser */ + /** + * Filter only wanted nodes + * @param {NodeList|HTMLCollection|Array} nodes + * @param {String} selector + * @returns {Array} + */ + var _filter = (function (nodes, selector) { + if (!(nodes instanceof NodeList || nodes instanceof HTMLCollection || nodes instanceof Array)) { + throw new Error('You must provide a nodeList/HTMLCollection/Array of elements to be filtered.'); + } + if (typeof selector !== 'string') { + return Array.from(nodes); + } + return Array.from(nodes).filter(function (item) { return item.nodeType === 1 && item.matches(selector); }); + }); + + /* eslint-env browser */ + var stores = new Map(); + /** + * Stores data & configurations per Sortable + * @param {Object} config + */ + var Store = /** @class */ (function () { + function Store() { + this._config = new Map(); // eslint-disable-line no-undef + this._placeholder = undefined; // eslint-disable-line no-undef + this._data = new Map(); // eslint-disable-line no-undef + } + Object.defineProperty(Store.prototype, "config", { + /** + * get the configuration map of a class instance + * @method config + * @return {object} + */ + get: function () { + // transform Map to object + var config = {}; + this._config.forEach(function (value, key) { + config[key] = value; + }); + // return object + return config; + }, + /** + * set the configuration of a class instance + * @method config + * @param {object} config object of configurations + */ + set: function (config) { + if (typeof config !== 'object') { + throw new Error('You must provide a valid configuration object to the config setter.'); + } + // combine config with default + var mergedConfig = Object.assign({}, config); + // add config to map + this._config = new Map(Object.entries(mergedConfig)); + }, + enumerable: true, + configurable: true + }); + /** + * set individual configuration of a class instance + * @method setConfig + * @param key valid configuration key + * @param value any value + * @return void + */ + Store.prototype.setConfig = function (key, value) { + if (!this._config.has(key)) { + throw new Error("Trying to set invalid configuration item: " + key); + } + // set config + this._config.set(key, value); + }; + /** + * get an individual configuration of a class instance + * @method getConfig + * @param key valid configuration key + * @return any configuration value + */ + Store.prototype.getConfig = function (key) { + if (!this._config.has(key)) { + throw new Error("Invalid configuration item requested: " + key); + } + return this._config.get(key); + }; + Object.defineProperty(Store.prototype, "placeholder", { + /** + * get the placeholder for a class instance + * @method placeholder + * @return {HTMLElement|null} + */ + get: function () { + return this._placeholder; + }, + /** + * set the placeholder for a class instance + * @method placeholder + * @param {HTMLElement} placeholder + * @return {void} + */ + set: function (placeholder) { + if (!(placeholder instanceof HTMLElement) && placeholder !== null) { + throw new Error('A placeholder must be an html element or null.'); + } + this._placeholder = placeholder; + }, + enumerable: true, + configurable: true + }); + /** + * set an data entry + * @method setData + * @param {string} key + * @param {any} value + * @return {void} + */ + Store.prototype.setData = function (key, value) { + if (typeof key !== 'string') { + throw new Error("The key must be a string."); + } + this._data.set(key, value); + }; + /** + * get an data entry + * @method getData + * @param {string} key an existing key + * @return {any} + */ + Store.prototype.getData = function (key) { + if (typeof key !== 'string') { + throw new Error("The key must be a string."); + } + return this._data.get(key); + }; + /** + * delete an data entry + * @method deleteData + * @param {string} key an existing key + * @return {boolean} + */ + Store.prototype.deleteData = function (key) { + if (typeof key !== 'string') { + throw new Error("The key must be a string."); + } + return this._data.delete(key); + }; + return Store; + }()); + /** + * @param {HTMLElement} sortableElement + * @returns {Class: Store} + */ + var store = (function (sortableElement) { + // if sortableElement is wrong type + if (!(sortableElement instanceof HTMLElement)) { + throw new Error('Please provide a sortable to the store function.'); + } + // create new instance if not avilable + if (!stores.has(sortableElement)) { + stores.set(sortableElement, new Store()); + } + // return instance + return stores.get(sortableElement); + }); + + /** + * @param {Array|HTMLElement} element + * @param {Function} callback + * @param {string} event + */ + function addEventListener(element, eventName, callback) { + if (element instanceof Array) { + for (var i = 0; i < element.length; ++i) { + addEventListener(element[i], eventName, callback); + } + return; + } + element.addEventListener(eventName, callback); + store(element).setData("event" + eventName, callback); + } + /** + * @param {Array|HTMLElement} element + * @param {string} eventName + */ + function removeEventListener(element, eventName) { + if (element instanceof Array) { + for (var i = 0; i < element.length; ++i) { + removeEventListener(element[i], eventName); + } + return; + } + element.removeEventListener(eventName, store(element).getData("event" + eventName)); + store(element).deleteData("event" + eventName); + } + + /** + * @param {Array|HTMLElement} element + * @param {string} attribute + * @param {string} value + */ + function addAttribute(element, attribute, value) { + if (element instanceof Array) { + for (var i = 0; i < element.length; ++i) { + addAttribute(element[i], attribute, value); + } + return; + } + element.setAttribute(attribute, value); + } + /** + * @param {Array|HTMLElement} element + * @param {string} attribute + */ + function removeAttribute(element, attribute) { + if (element instanceof Array) { + for (var i = 0; i < element.length; ++i) { + removeAttribute(element[i], attribute); + } + return; + } + element.removeAttribute(attribute); + } + + /** + * @param {HTMLElement} element + * @returns {Object} + */ + var _offset = (function (element) { + if (!element.parentElement || element.getClientRects().length === 0) { + throw new Error('target element must be part of the dom'); + } + var rect = element.getClientRects()[0]; + return { + left: rect.left + window.pageXOffset, + right: rect.right + window.pageXOffset, + top: rect.top + window.pageYOffset, + bottom: rect.bottom + window.pageYOffset + }; + }); + + /** + * Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed + * @param {Function} func to debounce + * @param {number} time to wait before calling function with latest arguments, 0 - no debounce + * @returns {function} - debounced function + */ + var _debounce = (function (func, wait) { + if (wait === void 0) { wait = 0; } + var timeout; + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + clearTimeout(timeout); + timeout = setTimeout(function () { + func.apply(void 0, args); + }, wait); + }; + }); + + /* eslint-env browser */ + /** + * Get position of the element relatively to its sibling elements + * @param {HTMLElement} element + * @returns {number} + */ + var _index = (function (element, elementList) { + if (!(element instanceof HTMLElement) || !(elementList instanceof NodeList || elementList instanceof HTMLCollection || elementList instanceof Array)) { + throw new Error('You must provide an element and a list of elements.'); + } + return Array.from(elementList).indexOf(element); + }); + + /* eslint-env browser */ + /** + * Test whether element is in DOM + * @param {HTMLElement} element + * @returns {boolean} + */ + var isInDom = (function (element) { + if (!(element instanceof HTMLElement)) { + throw new Error('Element is not a node element.'); + } + return element.parentNode !== null; + }); + + /* eslint-env browser */ + /** + * Insert node before or after target + * @param {HTMLElement} referenceNode - reference element + * @param {HTMLElement} newElement - element to be inserted + * @param {String} position - insert before or after reference element + */ + var insertNode = function (referenceNode, newElement, position) { + if (!(referenceNode instanceof HTMLElement) || !(referenceNode.parentElement instanceof HTMLElement)) { + throw new Error('target and element must be a node'); + } + referenceNode.parentElement.insertBefore(newElement, (position === 'before' ? referenceNode : referenceNode.nextElementSibling)); + }; + /** + * Insert before target + * @param {HTMLElement} target + * @param {HTMLElement} element + */ + var insertBefore = function (target, element) { return insertNode(target, element, 'before'); }; + /** + * Insert after target + * @param {HTMLElement} target + * @param {HTMLElement} element + */ + var insertAfter = function (target, element) { return insertNode(target, element, 'after'); }; + + /* eslint-env browser */ + /** + * Filter only wanted nodes + * @param {HTMLElement} sortableContainer + * @param {Function} customSerializer + * @returns {Array} + */ + var _serialize = (function (sortableContainer, customItemSerializer, customContainerSerializer) { + if (customItemSerializer === void 0) { customItemSerializer = function (serializedItem, sortableContainer) { return serializedItem; }; } + if (customContainerSerializer === void 0) { customContainerSerializer = function (serializedContainer) { return serializedContainer; }; } + // check for valid sortableContainer + if (!(sortableContainer instanceof HTMLElement) || !sortableContainer.isSortable === true) { + throw new Error('You need to provide a sortableContainer to be serialized.'); + } + // check for valid serializers + if (typeof customItemSerializer !== 'function' || typeof customContainerSerializer !== 'function') { + throw new Error('You need to provide a valid serializer for items and the container.'); + } + // get options + var options = addData(sortableContainer, 'opts'); + var item = options.items; + // serialize container + var items = _filter(sortableContainer.children, item); + var serializedItems = items.map(function (item) { + return { + parent: sortableContainer, + node: item, + html: item.outerHTML, + index: _index(item, items) + }; + }); + // serialize container + var container = { + node: sortableContainer, + itemCount: serializedItems.length + }; + return { + container: customContainerSerializer(container), + items: serializedItems.map(function (item) { return customItemSerializer(item, sortableContainer); }) + }; + }); + + /* eslint-env browser */ + /** + * create a placeholder element + * @param {HTMLElement} sortableElement a single sortable + * @param {string|undefined} placeholder a string representing an html element + * @param {string} placeholderClasses a string representing the classes that should be added to the placeholder + */ + var _makePlaceholder = (function (sortableElement, placeholder, placeholderClass) { + var _a; + if (placeholderClass === void 0) { placeholderClass = 'sortable-placeholder'; } + if (!(sortableElement instanceof HTMLElement)) { + throw new Error('You must provide a valid element as a sortable.'); + } + // if placeholder is not an element + if (!(placeholder instanceof HTMLElement) && placeholder !== undefined) { + throw new Error('You must provide a valid element as a placeholder or set ot to undefined.'); + } + // if no placeholder element is given + if (placeholder === undefined) { + if (['UL', 'OL'].includes(sortableElement.tagName)) { + placeholder = document.createElement('li'); + } + else if (['TABLE', 'TBODY'].includes(sortableElement.tagName)) { + placeholder = document.createElement('tr'); + // set colspan to always all rows, otherwise the item can only be dropped in first column + placeholder.innerHTML = ''; + } + else { + placeholder = document.createElement('div'); + } + } + // add classes to placeholder + if (typeof placeholderClass === 'string') { + (_a = placeholder.classList).add.apply(_a, placeholderClass.split(' ')); + } + return placeholder; + }); + + /* eslint-env browser */ + /** + * Get height of an element including padding + * @param {HTMLElement} element an dom element + */ + var _getElementHeight = (function (element) { + if (!(element instanceof HTMLElement)) { + throw new Error('You must provide a valid dom element'); + } + // get calculated style of element + var style = window.getComputedStyle(element); + // pick applicable properties, convert to int and reduce by adding + return ['height', 'padding-top', 'padding-bottom'] + .map(function (key) { + var int = parseInt(style.getPropertyValue(key), 10); + return isNaN(int) ? 0 : int; + }) + .reduce(function (sum, value) { return sum + value; }); + }); + + /* eslint-env browser */ + /** + * get handle or return item + * @param {Array} items + * @param {string} selector + */ + var _getHandles = (function (items, selector) { + if (!(items instanceof Array)) { + throw new Error('You must provide a Array of HTMLElements to be filtered.'); + } + if (typeof selector !== 'string') { + return items; + } + return items + // remove items without handle from array + .filter(function (item) { + return item.querySelector(selector) instanceof HTMLElement || + (item.shadowRoot && item.shadowRoot.querySelector(selector) instanceof HTMLElement); + }) + // replace item with handle in array + .map(function (item) { + return item.querySelector(selector) || (item.shadowRoot && item.shadowRoot.querySelector(selector)); + }); + }); + + /** + * @param {Event} event + * @returns {HTMLElement} + */ + var getEventTarget = (function (event) { + return (event.composedPath && event.composedPath()[0]) || event.target; + }); + + /* eslint-env browser */ + /** + * defaultDragImage returns the current item as dragged image + * @param {HTMLElement} draggedElement - the item that the user drags + * @param {object} elementOffset - an object with the offsets top, left, right & bottom + * @param {Event} event - the original drag event object + * @return {object} with element, posX and posY properties + */ + var defaultDragImage = function (draggedElement, elementOffset, event) { + return { + element: draggedElement, + posX: event.pageX - elementOffset.left, + posY: event.pageY - elementOffset.top + }; + }; + /** + * attaches an element as the drag image to an event + * @param {Event} event - the original drag event object + * @param {HTMLElement} draggedElement - the item that the user drags + * @param {Function} customDragImage - function to create a custom dragImage + * @return void + */ + var setDragImage = (function (event, draggedElement, customDragImage) { + // check if event is provided + if (!(event instanceof Event)) { + throw new Error('setDragImage requires a DragEvent as the first argument.'); + } + // check if draggedElement is provided + if (!(draggedElement instanceof HTMLElement)) { + throw new Error('setDragImage requires the dragged element as the second argument.'); + } + // set default function of none provided + if (!customDragImage) { + customDragImage = defaultDragImage; + } + // check if setDragImage method is available + if (event.dataTransfer && event.dataTransfer.setDragImage) { + // get the elements offset + var elementOffset = _offset(draggedElement); + // get the dragImage + var dragImage = customDragImage(draggedElement, elementOffset, event); + // check if custom function returns correct values + if (!(dragImage.element instanceof HTMLElement) || typeof dragImage.posX !== 'number' || typeof dragImage.posY !== 'number') { + throw new Error('The customDragImage function you provided must return and object with the properties element[string], posX[integer], posY[integer].'); + } + // needs to be set for HTML5 drag & drop to work + event.dataTransfer.effectAllowed = 'copyMove'; + // Firefox requires it to use the event target's id for the data + event.dataTransfer.setData('text/plain', getEventTarget(event).id); + // set the drag image on the event + event.dataTransfer.setDragImage(dragImage.element, dragImage.posX, dragImage.posY); + } + }); + + /** + * Check if curList accepts items from destList + * @param {sortable} destination the container an item is move to + * @param {sortable} origin the container an item comes from + */ + var _listsConnected = (function (destination, origin) { + // check if valid sortable + if (destination.isSortable === true) { + var acceptFrom = store(destination).getConfig('acceptFrom'); + // check if acceptFrom is valid + if (acceptFrom !== null && acceptFrom !== false && typeof acceptFrom !== 'string') { + throw new Error('HTML5Sortable: Wrong argument, "acceptFrom" must be "null", "false", or a valid selector string.'); + } + if (acceptFrom !== null) { + return acceptFrom !== false && acceptFrom.split(',').filter(function (sel) { + return sel.length > 0 && origin.matches(sel); + }).length > 0; + } + // drop in same list + if (destination === origin) { + return true; + } + // check if lists are connected with connectWith + if (store(destination).getConfig('connectWith') !== undefined && store(destination).getConfig('connectWith') !== null) { + return store(destination).getConfig('connectWith') === store(origin).getConfig('connectWith'); + } + } + return false; + }); + + /** + * default configurations + */ + var defaultConfiguration = { + items: null, + // deprecated + connectWith: null, + // deprecated + disableIEFix: null, + acceptFrom: null, + copy: false, + placeholder: null, + placeholderClass: 'sortable-placeholder', + draggingClass: 'sortable-dragging', + hoverClass: false, + debounce: 0, + throttleTime: 100, + maxItems: 0, + itemSerializer: undefined, + containerSerializer: undefined, + customDragImage: null + }; + + /** + * make sure a function is only called once within the given amount of time + * @param {Function} fn the function to throttle + * @param {number} threshold time limit for throttling + */ + // must use function to keep this context + function _throttle (fn, threshold) { + var _this = this; + if (threshold === void 0) { threshold = 250; } + // check function + if (typeof fn !== 'function') { + throw new Error('You must provide a function as the first argument for throttle.'); + } + // check threshold + if (typeof threshold !== 'number') { + throw new Error('You must provide a number as the second argument for throttle.'); + } + var lastEventTimestamp = null; + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var now = Date.now(); + if (lastEventTimestamp === null || now - lastEventTimestamp >= threshold) { + lastEventTimestamp = now; + fn.apply(_this, args); + } + }; + } + + /* eslint-env browser */ + /** + * enable or disable hoverClass on mouseenter/leave if container Items + * @param {sortable} sortableContainer a valid sortableContainer + * @param {boolean} enable enable or disable event + */ + // export default (sortableContainer: sortable, enable: boolean) => { + var enableHoverClass = (function (sortableContainer, enable) { + if (typeof store(sortableContainer).getConfig('hoverClass') === 'string') { + var hoverClasses_1 = store(sortableContainer).getConfig('hoverClass').split(' '); + // add class on hover + if (enable === true) { + addEventListener(sortableContainer, 'mousemove', _throttle(function (event) { + // check of no mouse button was pressed when mousemove started == no drag + if (event.buttons === 0) { + _filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) { + var _a, _b; + if (item !== event.target) { + (_a = item.classList).remove.apply(_a, hoverClasses_1); + } + else { + (_b = item.classList).add.apply(_b, hoverClasses_1); + } + }); + } + }, store(sortableContainer).getConfig('throttleTime'))); + // remove class on leave + addEventListener(sortableContainer, 'mouseleave', function () { + _filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(function (item) { + var _a; + (_a = item.classList).remove.apply(_a, hoverClasses_1); + }); + }); + // remove events + } + else { + removeEventListener(sortableContainer, 'mousemove'); + removeEventListener(sortableContainer, 'mouseleave'); + } + } + }); + + /* eslint-env browser */ + /* + * variables global to the plugin + */ + var dragging; + var draggingHeight; + /* + * Keeps track of the initialy selected list, where 'dragstart' event was triggered + * It allows us to move the data in between individual Sortable List instances + */ + // Origin List - data from before any item was changed + var originContainer; + var originIndex; + var originElementIndex; + var originItemsBeforeUpdate; + // Previous Sortable Container - we dispatch as sortenter event when a + // dragged item enters a sortableContainer for the first time + var previousContainer; + // Destination List - data from before any item was changed + var destinationItemsBeforeUpdate; + /** + * remove event handlers from items + * @param {Array|NodeList} items + */ + var _removeItemEvents = function (items) { + removeEventListener(items, 'dragstart'); + removeEventListener(items, 'dragend'); + removeEventListener(items, 'dragover'); + removeEventListener(items, 'dragenter'); + removeEventListener(items, 'drop'); + removeEventListener(items, 'mouseenter'); + removeEventListener(items, 'mouseleave'); + }; + /** + * _getDragging returns the current element to drag or + * a copy of the element. + * Is Copy Active for sortable + * @param {HTMLElement} draggedItem - the item that the user drags + * @param {HTMLElement} sortable a single sortable + */ + var _getDragging = function (draggedItem, sortable) { + var ditem = draggedItem; + if (store(sortable).getConfig('copy') === true) { + ditem = draggedItem.cloneNode(true); + addAttribute(ditem, 'aria-copied', 'true'); + draggedItem.parentElement.appendChild(ditem); + ditem.style.display = 'none'; + ditem.oldDisplay = draggedItem.style.display; + } + return ditem; + }; + /** + * Remove data from sortable + * @param {HTMLElement} sortable a single sortable + */ + var _removeSortableData = function (sortable) { + removeData(sortable); + removeAttribute(sortable, 'aria-dropeffect'); + }; + /** + * Remove data from items + * @param {Array|HTMLElement} items + */ + var _removeItemData = function (items) { + removeAttribute(items, 'aria-grabbed'); + removeAttribute(items, 'aria-copied'); + removeAttribute(items, 'draggable'); + removeAttribute(items, 'role'); + }; + /** + * find sortable from element. travels up parent element until found or null. + * @param {HTMLElement} element a single sortable + * @param {Event} event - the current event. We need to pass it to be able to + * find Sortable whith shadowRoot (document fragment has no parent) + */ + function findSortable(element, event) { + if (event.composedPath) { + return event.composedPath().find(function (el) { return el.isSortable; }); + } + while (element.isSortable !== true) { + element = element.parentElement; + } + return element; + } + /** + * Dragging event is on the sortable element. finds the top child that + * contains the element. + * @param {HTMLElement} sortableElement a single sortable + * @param {HTMLElement} element is that being dragged + */ + function findDragElement(sortableElement, element) { + var options = addData(sortableElement, 'opts'); + var items = _filter(sortableElement.children, options.items); + var itemlist = items.filter(function (ele) { + return ele.contains(element) || (ele.shadowRoot && ele.shadowRoot.contains(element)); + }); + return itemlist.length > 0 ? itemlist[0] : element; + } + /** + * Destroy the sortable + * @param {HTMLElement} sortableElement a single sortable + */ + var _destroySortable = function (sortableElement) { + var opts = addData(sortableElement, 'opts') || {}; + var items = _filter(sortableElement.children, opts.items); + var handles = _getHandles(items, opts.handle); + // remove event handlers & data from sortable + removeEventListener(sortableElement, 'dragover'); + removeEventListener(sortableElement, 'dragenter'); + removeEventListener(sortableElement, 'drop'); + // remove event data from sortable + _removeSortableData(sortableElement); + // remove event handlers & data from items + removeEventListener(handles, 'mousedown'); + _removeItemEvents(items); + _removeItemData(items); + }; + /** + * Enable the sortable + * @param {HTMLElement} sortableElement a single sortable + */ + var _enableSortable = function (sortableElement) { + var opts = addData(sortableElement, 'opts'); + var items = _filter(sortableElement.children, opts.items); + var handles = _getHandles(items, opts.handle); + addAttribute(sortableElement, 'aria-dropeffect', 'move'); + addData(sortableElement, '_disabled', 'false'); + addAttribute(handles, 'draggable', 'true'); + // @todo: remove this fix + // IE FIX for ghost + // can be disabled as it has the side effect that other events + // (e.g. click) will be ignored + if (opts.disableIEFix === false) { + var spanEl = (document || window.document).createElement('span'); + if (typeof spanEl.dragDrop === 'function') { + addEventListener(handles, 'mousedown', function () { + if (items.indexOf(this) !== -1) { + this.dragDrop(); + } + else { + var parent = this.parentElement; + while (items.indexOf(parent) === -1) { + parent = parent.parentElement; + } + parent.dragDrop(); + } + }); + } + } + }; + /** + * Disable the sortable + * @param {HTMLElement} sortableElement a single sortable + */ + var _disableSortable = function (sortableElement) { + var opts = addData(sortableElement, 'opts'); + var items = _filter(sortableElement.children, opts.items); + var handles = _getHandles(items, opts.handle); + addAttribute(sortableElement, 'aria-dropeffect', 'none'); + addData(sortableElement, '_disabled', 'true'); + addAttribute(handles, 'draggable', 'false'); + removeEventListener(handles, 'mousedown'); + }; + /** + * Reload the sortable + * @param {HTMLElement} sortableElement a single sortable + * @description events need to be removed to not be double bound + */ + var _reloadSortable = function (sortableElement) { + var opts = addData(sortableElement, 'opts'); + var items = _filter(sortableElement.children, opts.items); + var handles = _getHandles(items, opts.handle); + addData(sortableElement, '_disabled', 'false'); + // remove event handlers from items + _removeItemEvents(items); + removeEventListener(handles, 'mousedown'); + // remove event handlers from sortable + removeEventListener(sortableElement, 'dragover'); + removeEventListener(sortableElement, 'dragenter'); + removeEventListener(sortableElement, 'drop'); + }; + /** + * Public sortable object + * @param {Array|NodeList} sortableElements + * @param {object|string} options|method + */ + function sortable(sortableElements, options) { + // get method string to see if a method is called + var method = String(options); + options = options || {}; + // check if the user provided a selector instead of an element + if (typeof sortableElements === 'string') { + sortableElements = document.querySelectorAll(sortableElements); + } + // if the user provided an element, return it in an array to keep the return value consistant + if (sortableElements instanceof HTMLElement) { + sortableElements = [sortableElements]; + } + sortableElements = Array.prototype.slice.call(sortableElements); + if (/serialize/.test(method)) { + return sortableElements.map(function (sortableContainer) { + var opts = addData(sortableContainer, 'opts'); + return _serialize(sortableContainer, opts.itemSerializer, opts.containerSerializer); + }); + } + sortableElements.forEach(function (sortableElement) { + if (/enable|disable|destroy/.test(method)) { + return sortable[method](sortableElement); + } + // log deprecation + ['connectWith', 'disableIEFix'].forEach(function (configKey) { + if (options.hasOwnProperty(configKey) && options[configKey] !== null) { + console.warn("HTML5Sortable: You are using the deprecated configuration \"" + configKey + "\". This will be removed in an upcoming version, make sure to migrate to the new options when updating."); + } + }); + // merge options with default options + options = Object.assign({}, defaultConfiguration, store(sortableElement).config, options); + // init data store for sortable + store(sortableElement).config = options; + // set options on sortable + addData(sortableElement, 'opts', options); + // property to define as sortable + sortableElement.isSortable = true; + // reset sortable + _reloadSortable(sortableElement); + // initialize + var listItems = _filter(sortableElement.children, options.items); + // create element if user defined a placeholder element as a string + var customPlaceholder; + if (options.placeholder !== null && options.placeholder !== undefined) { + var tempContainer = document.createElement(sortableElement.tagName); + if (options.placeholder instanceof HTMLElement) { + tempContainer.appendChild(options.placeholder); + } + else { + tempContainer.innerHTML = options.placeholder; + } + customPlaceholder = tempContainer.children[0]; + } + // add placeholder + store(sortableElement).placeholder = _makePlaceholder(sortableElement, customPlaceholder, options.placeholderClass); + addData(sortableElement, 'items', options.items); + if (options.acceptFrom) { + addData(sortableElement, 'acceptFrom', options.acceptFrom); + } + else if (options.connectWith) { + addData(sortableElement, 'connectWith', options.connectWith); + } + _enableSortable(sortableElement); + addAttribute(listItems, 'role', 'option'); + addAttribute(listItems, 'aria-grabbed', 'false'); + // enable hover class + enableHoverClass(sortableElement, true); + /* + Handle drag events on draggable items + Handle is set at the sortableElement level as it will bubble up + from the item + */ + addEventListener(sortableElement, 'dragstart', function (e) { + // ignore dragstart events + var target = getEventTarget(e); + if (target.isSortable === true) { + return; + } + e.stopImmediatePropagation(); + if ((options.handle && !target.matches(options.handle)) || target.getAttribute('draggable') === 'false') { + return; + } + var sortableContainer = findSortable(target, e); + var dragItem = findDragElement(sortableContainer, target); + // grab values + originItemsBeforeUpdate = _filter(sortableContainer.children, options.items); + originIndex = originItemsBeforeUpdate.indexOf(dragItem); + originElementIndex = _index(dragItem, sortableContainer.children); + originContainer = sortableContainer; + // add transparent clone or other ghost to cursor + setDragImage(e, dragItem, options.customDragImage); + // cache selsection & add attr for dragging + draggingHeight = _getElementHeight(dragItem); + dragItem.classList.add(options.draggingClass); + dragging = _getDragging(dragItem, sortableContainer); + addAttribute(dragging, 'aria-grabbed', 'true'); + // dispatch sortstart event on each element in group + sortableContainer.dispatchEvent(new CustomEvent('sortstart', { + detail: { + origin: { + elementIndex: originElementIndex, + index: originIndex, + container: originContainer + }, + item: dragging, + originalTarget: target + } + })); + }); + /* + We are capturing targetSortable before modifications with 'dragenter' event + */ + addEventListener(sortableElement, 'dragenter', function (e) { + var target = getEventTarget(e); + var sortableContainer = findSortable(target, e); + if (sortableContainer && sortableContainer !== previousContainer) { + destinationItemsBeforeUpdate = _filter(sortableContainer.children, addData(sortableContainer, 'items')) + .filter(function (item) { return item !== store(sortableElement).placeholder; }); + sortableContainer.dispatchEvent(new CustomEvent('sortenter', { + detail: { + origin: { + elementIndex: originElementIndex, + index: originIndex, + container: originContainer + }, + destination: { + container: sortableContainer, + itemsBeforeUpdate: destinationItemsBeforeUpdate + }, + item: dragging, + originalTarget: target + } + })); + } + previousContainer = sortableContainer; + }); + /* + * Dragend Event - https://developer.mozilla.org/en-US/docs/Web/Events/dragend + * Fires each time dragEvent end, or ESC pressed + * We are using it to clean up any draggable elements and placeholders + */ + addEventListener(sortableElement, 'dragend', function (e) { + if (!dragging) { + return; + } + dragging.classList.remove(options.draggingClass); + addAttribute(dragging, 'aria-grabbed', 'false'); + if (dragging.getAttribute('aria-copied') === 'true' && addData(dragging, 'dropped') !== 'true') { + dragging.remove(); + } + dragging.style.display = dragging.oldDisplay; + delete dragging.oldDisplay; + var visiblePlaceholder = Array.from(stores.values()).map(function (data) { return data.placeholder; }) + .filter(function (placeholder) { return placeholder instanceof HTMLElement; }) + .filter(isInDom)[0]; + if (visiblePlaceholder) { + visiblePlaceholder.remove(); + } + // dispatch sortstart event on each element in group + sortableElement.dispatchEvent(new CustomEvent('sortstop', { + detail: { + origin: { + elementIndex: originElementIndex, + index: originIndex, + container: originContainer + }, + item: dragging + } + })); + previousContainer = null; + dragging = null; + draggingHeight = null; + }); + /* + * Drop Event - https://developer.mozilla.org/en-US/docs/Web/Events/drop + * Fires when valid drop target area is hit + */ + addEventListener(sortableElement, 'drop', function (e) { + if (!_listsConnected(sortableElement, dragging.parentElement)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + addData(dragging, 'dropped', 'true'); + // get the one placeholder that is currently visible + var visiblePlaceholder = Array.from(stores.values()).map(function (data) { + return data.placeholder; + }) + // filter only HTMLElements + .filter(function (placeholder) { return placeholder instanceof HTMLElement; }) + // filter only elements in DOM + .filter(isInDom)[0]; + // attach element after placeholder + insertAfter(visiblePlaceholder, dragging); + // remove placeholder from dom + visiblePlaceholder.remove(); + /* + * Fires Custom Event - 'sortstop' + */ + sortableElement.dispatchEvent(new CustomEvent('sortstop', { + detail: { + origin: { + elementIndex: originElementIndex, + index: originIndex, + container: originContainer + }, + item: dragging + } + })); + var placeholder = store(sortableElement).placeholder; + var originItems = _filter(originContainer.children, options.items) + .filter(function (item) { return item !== placeholder; }); + var destinationContainer = this.isSortable === true ? this : this.parentElement; + var destinationItems = _filter(destinationContainer.children, addData(destinationContainer, 'items')) + .filter(function (item) { return item !== placeholder; }); + var destinationElementIndex = _index(dragging, Array.from(dragging.parentElement.children) + .filter(function (item) { return item !== placeholder; })); + var destinationIndex = _index(dragging, destinationItems); + /* + * When a list item changed container lists or index within a list + * Fires Custom Event - 'sortupdate' + */ + if (originElementIndex !== destinationElementIndex || originContainer !== destinationContainer) { + sortableElement.dispatchEvent(new CustomEvent('sortupdate', { + detail: { + origin: { + elementIndex: originElementIndex, + index: originIndex, + container: originContainer, + itemsBeforeUpdate: originItemsBeforeUpdate, + items: originItems + }, + destination: { + index: destinationIndex, + elementIndex: destinationElementIndex, + container: destinationContainer, + itemsBeforeUpdate: destinationItemsBeforeUpdate, + items: destinationItems + }, + item: dragging + } + })); + } + }); + var debouncedDragOverEnter = _debounce(function (sortableElement, element, pageY) { + if (!dragging) { + return; + } + // set placeholder height if forcePlaceholderSize option is set + if (options.forcePlaceholderSize) { + store(sortableElement).placeholder.style.height = draggingHeight + 'px'; + } + // if element the draggedItem is dragged onto is within the array of all elements in list + // (not only items, but also disabled, etc.) + if (Array.from(sortableElement.children).indexOf(element) > -1) { + var thisHeight = _getElementHeight(element); + var placeholderIndex = _index(store(sortableElement).placeholder, element.parentElement.children); + var thisIndex = _index(element, element.parentElement.children); + // Check if `element` is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering + if (thisHeight > draggingHeight) { + // Dead zone? + var deadZone = thisHeight - draggingHeight; + var offsetTop = _offset(element).top; + if (placeholderIndex < thisIndex && pageY < offsetTop) { + return; + } + if (placeholderIndex > thisIndex && + pageY > offsetTop + thisHeight - deadZone) { + return; + } + } + if (dragging.oldDisplay === undefined) { + dragging.oldDisplay = dragging.style.display; + } + if (dragging.style.display !== 'none') { + dragging.style.display = 'none'; + } + // To avoid flicker, determine where to position the placeholder + // based on where the mouse pointer is relative to the elements + // vertical center. + var placeAfter = false; + try { + var elementMiddle = _offset(element).top + element.offsetHeight / 2; + placeAfter = pageY >= elementMiddle; + } + catch (e) { + placeAfter = placeholderIndex < thisIndex; + } + if (placeAfter) { + insertAfter(element, store(sortableElement).placeholder); + } + else { + insertBefore(element, store(sortableElement).placeholder); + } + // get placeholders from all stores & remove all but current one + Array.from(stores.values()) + // remove empty values + .filter(function (data) { return data.placeholder !== undefined; }) + // foreach placeholder in array if outside of current sorableContainer -> remove from DOM + .forEach(function (data) { + if (data.placeholder !== store(sortableElement).placeholder) { + data.placeholder.remove(); + } + }); + } + else { + // get all placeholders from store + var placeholders = Array.from(stores.values()) + .filter(function (data) { return data.placeholder !== undefined; }) + .map(function (data) { + return data.placeholder; + }); + // check if element is not in placeholders + if (placeholders.indexOf(element) === -1 && sortableElement === element && !_filter(element.children, options.items).length) { + placeholders.forEach(function (element) { return element.remove(); }); + element.appendChild(store(sortableElement).placeholder); + } + } + }, options.debounce); + // Handle dragover and dragenter events on draggable items + var onDragOverEnter = function (e) { + var element = e.target; + var sortableElement = element.isSortable === true ? element : findSortable(element, e); + element = findDragElement(sortableElement, element); + if (!dragging || !_listsConnected(sortableElement, dragging.parentElement) || addData(sortableElement, '_disabled') === 'true') { + return; + } + var options = addData(sortableElement, 'opts'); + if (parseInt(options.maxItems) && _filter(sortableElement.children, addData(sortableElement, 'items')).length >= parseInt(options.maxItems) && dragging.parentElement !== sortableElement) { + return; + } + e.preventDefault(); + e.stopPropagation(); + e.dataTransfer.dropEffect = store(sortableElement).getConfig('copy') === true ? 'copy' : 'move'; + debouncedDragOverEnter(sortableElement, element, e.pageY); + }; + addEventListener(listItems.concat(sortableElement), 'dragover', onDragOverEnter); + addEventListener(listItems.concat(sortableElement), 'dragenter', onDragOverEnter); + }); + return sortableElements; + } + sortable.destroy = function (sortableElement) { + _destroySortable(sortableElement); + }; + sortable.enable = function (sortableElement) { + _enableSortable(sortableElement); + }; + sortable.disable = function (sortableElement) { + _disableSortable(sortableElement); + }; + /* START.TESTS_ONLY */ + sortable.__testing = { + // add internal methods here for testing purposes + _data: addData, + _removeItemEvents: _removeItemEvents, + _removeItemData: _removeItemData, + _removeSortableData: _removeSortableData + }; + + return sortable; + +}());