From 766287403615a47145683fda954e38914809c157 Mon Sep 17 00:00:00 2001 From: jos Date: Wed, 11 Jul 2018 10:54:08 +0200 Subject: [PATCH] Integrated `mobius1-selectr` --- README.md | 2 +- gulpfile.js | 3 +- package-lock.json | 5 + package.json | 1 + src/css/contextmenu.css | 80 +- src/js/assets/selectr/README.md | 6 + src/js/assets/selectr/selectr.css | 473 +++++++ src/js/assets/selectr/selectr.js | 2173 +++++++++++++++++++++++++++++ src/js/showSortModal.js | 2 +- src/js/showTransformModal.js | 51 +- test/test_large_array.html | 7 +- 11 files changed, 2731 insertions(+), 72 deletions(-) create mode 100644 src/js/assets/selectr/README.md create mode 100644 src/js/assets/selectr/selectr.css create mode 100644 src/js/assets/selectr/selectr.js diff --git a/README.md b/README.md index 20f9c87..0f09b2e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ editor. The editor can be used as a component in your own web application. The library can be loaded as CommonJS module, AMD module, or as a regular javascript file. -Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 9+. +Supported browsers: Chrome, Firefox, Safari, Opera, Edge, Internet Explorer 11. json editor   code editor diff --git a/gulpfile.js b/gulpfile.js index dbbd761..0e3126e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -138,7 +138,8 @@ gulp.task('bundle-css', ['mkdir'], function () { 'src/css/autocomplete.css', 'src/css/treepath.css', 'src/css/statusbar.css', - 'src/css/navigationbar.css' + 'src/css/navigationbar.css', + 'src/js/assets/selectr/selectr.css' ]) .pipe(concatCss(NAME + '.css')) .pipe(gulp.dest(DIST)) diff --git a/package-lock.json b/package-lock.json index 2b5a514..c17b152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2910,6 +2910,11 @@ } } }, + "mobius1-selectr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/mobius1-selectr/-/mobius1-selectr-2.4.1.tgz", + "integrity": "sha512-neWBV/ew/wxUvj1mVkTKpFXsMIC2T5/usTFmlsOUXt1919y6IMhRMULXTqFxMoAihaRtI98zCE5ckgTMj9aDBQ==" + }, "mocha": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", diff --git a/package.json b/package.json index 67210dc..3bf2bcf 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "brace": "0.11.0", "javascript-natural-sort": "0.7.1", "jmespath": "0.15.0", + "mobius1-selectr": "2.4.1", "picomodal": "3.0.0" }, "devDependencies": { diff --git a/src/css/contextmenu.css b/src/css/contextmenu.css index 8a99b0a..4418170 100644 --- a/src/css/contextmenu.css +++ b/src/css/contextmenu.css @@ -281,7 +281,8 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { .jsoneditor-modal { position: absolute !important; - max-width: 100% !important; + max-width: 95% !important; + width: auto !important; border-radius: 2px !important; padding: 45px 15px 15px 15px !important; @@ -291,6 +292,10 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { line-height: 1.3em; } +.jsoneditor-modal.jsoneditor-modal-transform { + width: 600px !important; +} + .jsoneditor-modal .pico-modal-header { position: absolute; box-sizing: border-box; @@ -308,6 +313,10 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { color: white; } +.jsoneditor-modal table { + width: 100%; +} + .jsoneditor-modal table th, .jsoneditor-modal table td { padding: 5px 20px 5px 0; @@ -401,38 +410,6 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { color: #808080; } -.jsoneditor-modal .jsoneditor-select-wrapper { - position: relative; - display: inline-block; -} - -.jsoneditor-modal .jsoneditor-select-wrapper:after { - content: ""; - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 6px solid #666; - position: absolute; - right: 8px; - top: 10px; - pointer-events: none; -} - -.jsoneditor-modal select { - padding: 3px 24px 3px 10px; - width: 100%; - max-width: 300px; - - -webkit-appearance:none; - -moz-appearance:none; - appearance: none; - - text-indent: 0; - text-overflow: ""; - line-height: inherit; -} - .jsoneditor-modal select::-ms-expand { display: none; } @@ -464,7 +441,8 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { .jsoneditor-modal #query, .jsoneditor-modal .jsoneditor-transform-preview { - width: 400px; + width: 100%; + box-sizing: border-box; } .jsoneditor-modal .jsoneditor-transform-preview { @@ -490,26 +468,40 @@ div.jsoneditor-contextmenu button.jsoneditor-type-modes > div.jsoneditor-icon { margin: 4px 0 2px 0; } +.jsoneditor-modal .jsoneditor-inline { + position: relative; + display: inline-block; + width: 100%; + padding: 2px 0; +} + .jsoneditor-modal .jsoneditor-jmespath-filter-field { - width: auto; - max-width: 200px; + width: 170px; } .jsoneditor-modal .jsoneditor-jmespath-filter-relation { - width: auto; + width: 100px; } .jsoneditor-modal .jsoneditor-jmespath-filter-value { - width: 100px; + width: 170px; } .jsoneditor-modal .jsoneditor-jmespath-sort-field { - width: auto; - max-width: 200px; + width: 170px; } .jsoneditor-modal .jsoneditor-jmespath-sort-order { - width: auto; + width: 150px; } .jsoneditor-modal .jsoneditor-jmespath-select-fields { - width: auto; - max-width: 100%; -} \ No newline at end of file + width: 100%; +} + +.jsoneditor-modal .selectr-selected { + border-color: #d3d3d3; + padding: 3px 28px 4px 8px; +} + +.jsoneditor-modal .selectr-selected .selectr-tag { + background-color: #3883fa; + border-radius: 5px; +} diff --git a/src/js/assets/selectr/README.md b/src/js/assets/selectr/README.md new file mode 100644 index 0000000..637be8c --- /dev/null +++ b/src/js/assets/selectr/README.md @@ -0,0 +1,6 @@ +This is a copy of the Selectr project + +https://github.com/Mobius1/Selectr + +Reason is that the project is not maintained and has some issues +loading it via `require` in a webpack project. diff --git a/src/js/assets/selectr/selectr.css b/src/js/assets/selectr/selectr.css new file mode 100644 index 0000000..5832311 --- /dev/null +++ b/src/js/assets/selectr/selectr.css @@ -0,0 +1,473 @@ +/*! + * Selectr 2.4.0 + * https://github.com/Mobius1/Selectr + * + * Released under the MIT license + */ + +.selectr-container { + position: relative; +} + +.selectr-container li { + list-style: none; +} + +.selectr-hidden { + position: absolute; + overflow: hidden; + clip: rect(0px, 0px, 0px, 0px); + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0 none; +} + +.selectr-visible { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 0; + z-index: 11; +} + +.selectr-desktop.multiple .selectr-visible { + display: none; +} + +.selectr-desktop.multiple.native-open .selectr-visible { + top: 100%; + min-height: 200px !important; + height: auto; + opacity: 1; + display: block; +} + +.selectr-container.multiple.selectr-mobile .selectr-selected { + z-index: 0; +} + +.selectr-selected { + position: relative; + z-index: 1; + box-sizing: border-box; + width: 100%; + padding: 7px 28px 7px 14px; + cursor: pointer; + border: 1px solid #999; + border-radius: 3px; + background-color: #fff; +} + +.selectr-selected::before { + position: absolute; + top: 50%; + right: 10px; + width: 0; + height: 0; + content: ''; + -o-transform: rotate(0deg) translate3d(0px, -50%, 0px); + -ms-transform: rotate(0deg) translate3d(0px, -50%, 0px); + -moz-transform: rotate(0deg) translate3d(0px, -50%, 0px); + -webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px); + transform: rotate(0deg) translate3d(0px, -50%, 0px); + border-width: 4px 4px 0 4px; + border-style: solid; + border-color: #6c7a86 transparent transparent; +} + +.selectr-container.open .selectr-selected::before, +.selectr-container.native-open .selectr-selected::before { + border-width: 0 4px 4px 4px; + border-style: solid; + border-color: transparent transparent #6c7a86; +} + +.selectr-label { + display: none; + overflow: hidden; + width: 100%; + white-space: nowrap; + text-overflow: ellipsis; +} + +.selectr-placeholder { + color: #6c7a86; +} + +.selectr-tags { + margin: 0; + padding: 0; + white-space: normal; +} + +.has-selected .selectr-tags { + margin: 0 0 -2px; +} + +.selectr-tag { + list-style: none; + position: relative; + float: left; + padding: 2px 25px 2px 8px; + margin: 0 2px 2px 0; + cursor: default; + color: #fff; + border: medium none; + border-radius: 10px; + background: #acb7bf none repeat scroll 0 0; +} + +.selectr-container.multiple.has-selected .selectr-selected { + padding: 5px 28px 5px 5px; +} + +.selectr-options-container { + position: absolute; + z-index: 10000; + top: calc(100% - 1px); + left: 0; + display: none; + box-sizing: border-box; + width: 100%; + border-width: 0 1px 1px; + border-style: solid; + border-color: transparent #999 #999; + border-radius: 0 0 3px 3px; + background-color: #fff; +} + +.selectr-container.open .selectr-options-container { + display: block; +} + +.selectr-input-container { + position: relative; + display: none; +} + +.selectr-clear, +.selectr-input-clear, +.selectr-tag-remove { + position: absolute; + top: 50%; + right: 22px; + width: 20px; + height: 20px; + padding: 0; + cursor: pointer; + -o-transform: translate3d(0px, -50%, 0px); + -ms-transform: translate3d(0px, -50%, 0px); + -moz-transform: translate3d(0px, -50%, 0px); + -webkit-transform: translate3d(0px, -50%, 0px); + transform: translate3d(0px, -50%, 0px); + border: medium none; + background-color: transparent; + z-index: 11; +} + +.selectr-clear, +.selectr-input-clear { + display: none; +} + +.selectr-container.has-selected .selectr-clear, +.selectr-input-container.active .selectr-input-clear { + display: block; +} + +.selectr-selected .selectr-tag-remove { + right: 2px; +} + +.selectr-clear::before, +.selectr-clear::after, +.selectr-input-clear::before, +.selectr-input-clear::after, +.selectr-tag-remove::before, +.selectr-tag-remove::after { + position: absolute; + top: 5px; + left: 9px; + width: 2px; + height: 10px; + content: ' '; + background-color: #6c7a86; +} + +.selectr-tag-remove::before, +.selectr-tag-remove::after { + top: 4px; + width: 3px; + height: 12px; + background-color: #fff; +} + +.selectr-clear:before, +.selectr-input-clear::before, +.selectr-tag-remove::before { + -o-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + transform: rotate(45deg); +} + +.selectr-clear:after, +.selectr-input-clear::after, +.selectr-tag-remove::after { + -o-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +.selectr-input-container.active, +.selectr-input-container.active .selectr-clear { + display: block; +} + +.selectr-input { + top: 5px; + left: 5px; + box-sizing: border-box; + width: calc(100% - 30px); + margin: 10px 15px; + padding: 7px 30px 7px 9px; + border: 1px solid #999; + border-radius: 3px; +} + +.selectr-notice { + display: none; + box-sizing: border-box; + width: 100%; + padding: 8px 16px; + border-top: 1px solid #999; + border-radius: 0 0 3px 3px; + background-color: #fff; +} + +.selectr-container.notice .selectr-notice { + display: block; +} + +.selectr-container.notice .selectr-selected { + border-radius: 3px 3px 0 0; +} + +.selectr-options { + position: relative; + top: calc(100% + 2px); + display: none; + overflow-x: auto; + overflow-y: scroll; + max-height: 200px; + margin: 0; + padding: 0; +} + +.selectr-container.open .selectr-options, +.selectr-container.open .selectr-input-container, +.selectr-container.notice .selectr-options-container { + display: block; +} + +.selectr-option { + position: relative; + display: block; + padding: 5px 20px; + list-style: outside none none; + cursor: pointer; + font-weight: normal; +} + +.selectr-options.optgroups > .selectr-option { + padding-left: 25px; +} + +.selectr-optgroup { + font-weight: bold; + padding: 0; +} + +.selectr-optgroup--label { + font-weight: bold; + margin-top: 10px; + padding: 5px 15px; +} + +.selectr-match { + text-decoration: underline; +} + +.selectr-option.selected { + background-color: #ddd; +} + +.selectr-option.active { + color: #fff; + background-color: #5897fb; +} + +.selectr-option.disabled { + opacity: 0.4; +} + +.selectr-option.excluded { + display: none; +} + +.selectr-container.open .selectr-selected { + border-color: #999 #999 transparent #999; + border-radius: 3px 3px 0 0; +} + +.selectr-container.open .selectr-selected::after { + -o-transform: rotate(180deg) translate3d(0px, 50%, 0px); + -ms-transform: rotate(180deg) translate3d(0px, 50%, 0px); + -moz-transform: rotate(180deg) translate3d(0px, 50%, 0px); + -webkit-transform: rotate(180deg) translate3d(0px, 50%, 0px); + transform: rotate(180deg) translate3d(0px, 50%, 0px); +} + +.selectr-disabled { + opacity: .6; +} + +.selectr-empty, +.has-selected .selectr-placeholder { + display: none; +} + +.has-selected .selectr-label { + display: block; +} + +/* TAGGABLE */ +.taggable .selectr-selected { + padding: 4px 28px 4px 4px; +} + +.taggable .selectr-selected::after { + display: table; + content: " "; + clear: both; +} + +.taggable .selectr-label { + width: auto; +} + +.taggable .selectr-tags { + float: left; + display: block; +} + +.taggable .selectr-placeholder { + display: none; +} + +.input-tag { + float: left; + min-width: 90px; + width: auto; +} + +.selectr-tag-input { + border: medium none; + padding: 3px 10px; + width: 100%; + font-family: inherit; + font-weight: inherit; + font-size: inherit; +} + +.selectr-input-container.loading::after { + position: absolute; + top: 50%; + right: 20px; + width: 20px; + height: 20px; + content: ''; + -o-transform: translate3d(0px, -50%, 0px); + -ms-transform: translate3d(0px, -50%, 0px); + -moz-transform: translate3d(0px, -50%, 0px); + -webkit-transform: translate3d(0px, -50%, 0px); + transform: translate3d(0px, -50%, 0px); + + -o-transform-origin: 50% 0 0; + -ms-transform-origin: 50% 0 0; + -moz-transform-origin: 50% 0 0; + -webkit-transform-origin: 50% 0 0; + transform-origin: 50% 0 0; + + -moz-animation: 500ms linear 0s normal forwards infinite running spin; + -webkit-animation: 500ms linear 0s normal forwards infinite running spin; + animation: 500ms linear 0s normal forwards infinite running spin; + border-width: 3px; + border-style: solid; + border-color: #aaa #ddd #ddd; + border-radius: 50%; +} + +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px); + transform: rotate(0deg) translate3d(0px, -50%, 0px); + } + 100% { + -webkit-transform: rotate(360deg) translate3d(0px, -50%, 0px); + transform: rotate(360deg) translate3d(0px, -50%, 0px); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px); + transform: rotate(0deg) translate3d(0px, -50%, 0px); + } + 100% { + -webkit-transform: rotate(360deg) translate3d(0px, -50%, 0px); + transform: rotate(360deg) translate3d(0px, -50%, 0px); + } +} +.selectr-container.open.inverted .selectr-selected { + border-color: transparent #999 #999; + border-radius: 0 0 3px 3px; +} + +.selectr-container.inverted .selectr-options-container { + border-width: 1px 1px 0; + border-color: #999 #999 transparent; + border-radius: 3px 3px 0 0; + background-color: #fff; +} + +.selectr-container.inverted .selectr-options-container { + top: auto; + bottom: calc(100% - 1px); +} + +.selectr-container ::-webkit-input-placeholder { + color: #6c7a86; + opacity: 1; +} + +.selectr-container ::-moz-placeholder { + color: #6c7a86; + opacity: 1; +} + +.selectr-container :-ms-input-placeholder { + color: #6c7a86; + opacity: 1; +} + +.selectr-container ::placeholder { + color: #6c7a86; + opacity: 1; +} diff --git a/src/js/assets/selectr/selectr.js b/src/js/assets/selectr/selectr.js new file mode 100644 index 0000000..7452cf8 --- /dev/null +++ b/src/js/assets/selectr/selectr.js @@ -0,0 +1,2173 @@ +/*! + * Selectr 2.4.0 + * https://github.com/Mobius1/Selectr + * + * Released under the MIT license + */ + +'use strict'; + +/** + * Default configuration options + * @type {Object} + */ +var defaultConfig = { + /** + * Emulates browser behaviour by selecting the first option by default + * @type {Boolean} + */ + defaultSelected: true, + + /** + * Sets the width of the container + * @type {String} + */ + width: "auto", + + /** + * Enables/ disables the container + * @type {Boolean} + */ + disabled: false, + + /** + * Enables / disables the search function + * @type {Boolean} + */ + searchable: true, + + /** + * Enable disable the clear button + * @type {Boolean} + */ + clearable: false, + + /** + * Sort the tags / multiselect options + * @type {Boolean} + */ + sortSelected: false, + + /** + * Allow deselecting of select-one options + * @type {Boolean} + */ + allowDeselect: false, + + /** + * Close the dropdown when scrolling (@AlexanderReiswich, #11) + * @type {Boolean} + */ + closeOnScroll: false, + + /** + * Allow the use of the native dropdown (@jonnyscholes, #14) + * @type {Boolean} + */ + nativeDropdown: false, + + /** + * Set the main placeholder + * @type {String} + */ + placeholder: "Select an option...", + + /** + * Allow the tagging feature + * @type {Boolean} + */ + taggable: false, + + /** + * Set the tag input placeholder (@labikmartin, #21, #22) + * @type {String} + */ + tagPlaceholder: "Enter a tag..." +}; + +/** + * Event Emitter + */ +var Events = function() {}; + +/** + * Event Prototype + * @type {Object} + */ +Events.prototype = { + /** + * Add custom event listener + * @param {String} event Event type + * @param {Function} func Callback + * @return {Void} + */ + on: function(event, func) { + this._events = this._events || {}; + this._events[event] = this._events[event] || []; + this._events[event].push(func); + }, + + /** + * Remove custom event listener + * @param {String} event Event type + * @param {Function} func Callback + * @return {Void} + */ + off: function(event, func) { + this._events = this._events || {}; + if (event in this._events === false) return; + this._events[event].splice(this._events[event].indexOf(func), 1); + }, + + /** + * Fire a custom event + * @param {String} event Event type + * @return {Void} + */ + emit: function(event /* , args... */ ) { + this._events = this._events || {}; + if (event in this._events === false) return; + for (var i = 0; i < this._events[event].length; i++) { + this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } +}; + +/** + * Event mixin + * @param {Object} obj + * @return {Object} + */ +Events.mixin = function(obj) { + var props = ['on', 'off', 'emit']; + for (var i = 0; i < props.length; i++) { + if (typeof obj === 'function') { + obj.prototype[props[i]] = Events.prototype[props[i]]; + } else { + obj[props[i]] = Events.prototype[props[i]]; + } + } + return obj; +}; + +/** + * Helpers + * @type {Object} + */ +var util = { + extend: function(src, props) { + props = props || {}; + var p; + for (p in src) { + if (src.hasOwnProperty(p)) { + if (!props.hasOwnProperty(p)) { + props[p] = src[p]; + } + } + } + return props; + }, + each: function(a, b, c) { + if ("[object Object]" === Object.prototype.toString.call(a)) { + for (var d in a) { + if (Object.prototype.hasOwnProperty.call(a, d)) { + b.call(c, d, a[d], a); + } + } + } else { + for (var e = 0, f = a.length; e < f; e++) { + b.call(c, e, a[e], a); + } + } + }, + createElement: function(e, a) { + var d = document, + el = d.createElement(e); + if (a && "[object Object]" === Object.prototype.toString.call(a)) { + var i; + for (i in a) + if (i in el) el[i] = a[i]; + else if ("html" === i) el.innerHTML = a[i]; + else if ("text" === i) { + var t = d.createTextNode(a[i]); + el.appendChild(t); + } else el.setAttribute(i, a[i]); + } + return el; + }, + hasClass: function(a, b) { + if (a) + return a.classList ? a.classList.contains(b) : !!a.className && !!a.className.match(new RegExp("(\\s|^)" + b + "(\\s|$)")); + }, + addClass: function(a, b) { + if (!util.hasClass(a, b)) { + if (a.classList) { + a.classList.add(b); + } else { + a.className = a.className.trim() + " " + b; + } + } + }, + removeClass: function(a, b) { + if (util.hasClass(a, b)) { + if (a.classList) { + a.classList.remove(b); + } else { + a.className = a.className.replace(new RegExp("(^|\\s)" + b.split(" ").join("|") + "(\\s|$)", "gi"), " "); + } + } + }, + closest: function(el, fn) { + return el && el !== document.body && (fn(el) ? el : util.closest(el.parentNode, fn)); + }, + isInt: function(val) { + return typeof val === 'number' && isFinite(val) && Math.floor(val) === val; + }, + debounce: function(a, b, c) { + var d; + return function() { + var e = this, + f = arguments, + g = function() { + d = null; + if (!c) a.apply(e, f); + }, + h = c && !d; + clearTimeout(d); + d = setTimeout(g, b); + if (h) { + a.apply(e, f); + } + }; + }, + rect: function(el, abs) { + var w = window; + var r = el.getBoundingClientRect(); + var x = abs ? w.pageXOffset : 0; + var y = abs ? w.pageYOffset : 0; + + return { + bottom: r.bottom + y, + height: r.height, + left: r.left + x, + right: r.right + x, + top: r.top + y, + width: r.width + }; + }, + includes: function(a, b) { + return a.indexOf(b) > -1; + }, + truncate: function(el) { + while (el.firstChild) { + el.removeChild(el.firstChild); + } + } +}; + + +function isset(obj, prop) { + return obj.hasOwnProperty(prop) && (obj[prop] === true || obj[prop].length); +} + +/** + * Append an item to the list + * @param {Object} item + * @param {Object} custom + * @return {Void} + */ +function appendItem(item, parent, custom) { + if (item.parentNode) { + if (!item.parentNode.parentNode) { + parent.appendChild(item.parentNode); + } + } else { + parent.appendChild(item); + } + + util.removeClass(item, "excluded"); + if (!custom) { + item.innerHTML = item.textContent; + } +} + +/** + * Render the item list + * @return {Void} + */ +var render = function() { + if (this.items.length) { + var f = document.createDocumentFragment(); + + if (this.config.pagination) { + var pages = this.pages.slice(0, this.pageIndex); + + util.each(pages, function(i, items) { + util.each(items, function(j, item) { + appendItem(item, f, this.customOption); + }, this); + }, this); + } else { + util.each(this.items, function(i, item) { + appendItem(item, f, this.customOption); + }, this); + } + + if (f.childElementCount) { + util.removeClass(this.items[this.navIndex], "active"); + this.navIndex = f.querySelector(".selectr-option").idx; + util.addClass(this.items[this.navIndex], "active"); + } + + this.tree.appendChild(f); + } +}; + +/** + * Dismiss / close the dropdown + * @param {obj} e + * @return {void} + */ +var dismiss = function(e) { + var target = e.target; + if (!this.container.contains(target) && (this.opened || util.hasClass(this.container, "notice"))) { + this.close(); + } +}; + +/** + * Build a list item from the HTMLOptionElement + * @param {int} i HTMLOptionElement index + * @param {HTMLOptionElement} option + * @param {bool} group Has parent optgroup + * @return {void} + */ +var createItem = function(option, data) { + data = data || option; + var content = this.customOption ? this.config.renderOption(data) : option.textContent; + var opt = util.createElement("li", { + class: "selectr-option", + html: content, + role: "treeitem", + "aria-selected": false + }); + + opt.idx = option.idx; + + this.items.push(opt); + + if (option.defaultSelected) { + this.defaultSelected.push(option.idx); + } + + if (option.disabled) { + opt.disabled = true; + util.addClass(opt, "disabled"); + } + + return opt; +}; + +/** + * Build the container + * @return {Void} + */ +var build = function() { + + this.requiresPagination = this.config.pagination && this.config.pagination > 0; + + // Set width + if (isset(this.config, "width")) { + if (util.isInt(this.config.width)) { + this.width = this.config.width + "px"; + } else { + if (this.config.width === "auto") { + this.width = "100%"; + } else if (util.includes(this.config.width, "%")) { + this.width = this.config.width; + } + } + } + + this.container = util.createElement("div", { + class: "selectr-container" + }); + + // Custom className + if (this.config.customClass) { + util.addClass(this.container, this.config.customClass); + } + + // Mobile device + if (this.mobileDevice) { + util.addClass(this.container, "selectr-mobile"); + } else { + util.addClass(this.container, "selectr-desktop"); + } + + // Hide the HTMLSelectElement and prevent focus + this.el.tabIndex = -1; + + // Native dropdown + if (this.config.nativeDropdown || this.mobileDevice) { + util.addClass(this.el, "selectr-visible"); + } else { + util.addClass(this.el, "selectr-hidden"); + } + + this.selected = util.createElement("div", { + class: "selectr-selected", + disabled: this.disabled, + tabIndex: 1, // enable tabIndex (#9) + "aria-expanded": false + }); + + this.label = util.createElement(this.el.multiple ? "ul" : "span", { + class: "selectr-label" + }); + + var dropdown = util.createElement("div", { + class: "selectr-options-container" + }); + + this.tree = util.createElement("ul", { + class: "selectr-options", + role: "tree", + "aria-hidden": true, + "aria-expanded": false + }); + + this.notice = util.createElement("div", { + class: "selectr-notice" + }); + + this.el.setAttribute("aria-hidden", true); + + if (this.disabled) { + this.el.disabled = true; + } + + if (this.el.multiple) { + util.addClass(this.label, "selectr-tags"); + util.addClass(this.container, "multiple"); + + // Collection of tags + this.tags = []; + + // Collection of selected values + this.selectedValues = this.getSelectedProperties('value'); + + // Collection of selected indexes + this.selectedIndexes = this.getSelectedProperties('idx'); + } + + this.selected.appendChild(this.label); + + if (this.config.clearable) { + this.selectClear = util.createElement("button", { + class: "selectr-clear", + type: "button" + }); + + this.container.appendChild(this.selectClear); + + util.addClass(this.container, "clearable"); + } + + if (this.config.taggable) { + var li = util.createElement('li', { + class: 'input-tag' + }); + this.input = util.createElement("input", { + class: "selectr-tag-input", + placeholder: this.config.tagPlaceholder, + tagIndex: 0, + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + spellcheck: "false", + role: "textbox", + type: "search" + }); + + li.appendChild(this.input); + this.label.appendChild(li); + util.addClass(this.container, "taggable"); + + this.tagSeperators = [","]; + if (this.config.tagSeperators) { + this.tagSeperators = this.tagSeperators.concat(this.config.tagSeperators); + } + } + + if (this.config.searchable) { + this.input = util.createElement("input", { + class: "selectr-input", + tagIndex: -1, + autocomplete: "off", + autocorrect: "off", + autocapitalize: "off", + spellcheck: "false", + role: "textbox", + type: "search" + }); + this.inputClear = util.createElement("button", { + class: "selectr-input-clear", + type: "button" + }); + this.inputContainer = util.createElement("div", { + class: "selectr-input-container" + }); + + this.inputContainer.appendChild(this.input); + this.inputContainer.appendChild(this.inputClear); + dropdown.appendChild(this.inputContainer); + } + + dropdown.appendChild(this.notice); + dropdown.appendChild(this.tree); + + // List of items for the dropdown + this.items = []; + + // Establish options + this.options = []; + + // Check for options in the element + if (this.el.options.length) { + this.options = [].slice.call(this.el.options); + } + + // Element may have optgroups so + // iterate element.children instead of element.options + var group = false, + j = 0; + if (this.el.children.length) { + util.each(this.el.children, function(i, element) { + if (element.nodeName === "OPTGROUP") { + + group = util.createElement("ul", { + class: "selectr-optgroup", + role: "group", + html: "
  • " + element.label + "
  • " + }); + + util.each(element.children, function(x, el) { + el.idx = j; + group.appendChild(createItem.call(this, el, group)); + j++; + }, this); + } else { + element.idx = j; + createItem.call(this, element); + j++; + } + }, this); + } + + // Options defined by the data option + if (this.config.data && Array.isArray(this.config.data)) { + this.data = []; + var optgroup = false, + option; + + group = false; + j = 0; + + util.each(this.config.data, function(i, opt) { + // Check for group options + if (isset(opt, "children")) { + optgroup = util.createElement("optgroup", { + label: opt.text + }); + + group = util.createElement("ul", { + class: "selectr-optgroup", + role: "group", + html: "
  • " + opt.text + "
  • " + }); + + util.each(opt.children, function(x, data) { + option = new Option(data.text, data.value, false, data.hasOwnProperty("selected") && data.selected === true); + + option.disabled = isset(data, "disabled"); + + this.options.push(option); + + optgroup.appendChild(option); + + option.idx = j; + + group.appendChild(createItem.call(this, option, data)); + + this.data[j] = data; + + j++; + }, this); + } else { + option = new Option(opt.text, opt.value, false, opt.hasOwnProperty("selected") && opt.selected === true); + + option.disabled = isset(opt, "disabled"); + + this.options.push(option); + + option.idx = j; + + createItem.call(this, option, opt); + + this.data[j] = opt; + + j++; + } + }, this); + } + + this.setSelected(true); + + var first; + this.navIndex = 0; + for (var i = 0; i < this.items.length; i++) { + first = this.items[i]; + + if (!util.hasClass(first, "disabled")) { + + util.addClass(first, "active"); + this.navIndex = i; + break; + } + } + + // Check for pagination / infinite scroll + if (this.requiresPagination) { + this.pageIndex = 1; + + // Create the pages + this.paginate(); + } + + this.container.appendChild(this.selected); + this.container.appendChild(dropdown); + + this.placeEl = util.createElement("div", { + class: "selectr-placeholder" + }); + + // Set the placeholder + this.setPlaceholder(); + + this.selected.appendChild(this.placeEl); + + // Disable if required + if (this.disabled) { + this.disable(); + } + + this.el.parentNode.insertBefore(this.container, this.el); + this.container.appendChild(this.el); +}; + +/** + * Navigate through the dropdown + * @param {obj} e + * @return {void} + */ +var navigate = function(e) { + e = e || window.event; + + // Filter out the keys we don"t want + if (!this.items.length || !this.opened || !util.includes([13, 38, 40], e.which)) { + this.navigating = false; + return; + } + + e.preventDefault(); + + if (e.which === 13) { + + if (this.config.taggable && this.input.value.length > 0) { + return false; + } + + return this.change(this.navIndex); + } + + var direction, prevEl = this.items[this.navIndex]; + + switch (e.which) { + case 38: + direction = 0; + if (this.navIndex > 0) { + this.navIndex--; + } + break; + case 40: + direction = 1; + if (this.navIndex < this.items.length - 1) { + this.navIndex++; + } + } + + this.navigating = true; + + + // Instead of wasting memory holding a copy of this.items + // with disabled / excluded options omitted, skip them instead + while (util.hasClass(this.items[this.navIndex], "disabled") || util.hasClass(this.items[this.navIndex], "excluded")) { + if (direction) { + this.navIndex++; + } else { + this.navIndex--; + } + + if (this.searching) { + if (this.navIndex > this.tree.lastElementChild.idx) { + this.navIndex = this.tree.lastElementChild.idx; + break; + } else if (this.navIndex < this.tree.firstElementChild.idx) { + this.navIndex = this.tree.firstElementChild.idx; + break; + } + } + } + + // Autoscroll the dropdown during navigation + var r = util.rect(this.items[this.navIndex]); + + if (!direction) { + if (this.navIndex === 0) { + this.tree.scrollTop = 0; + } else if (r.top - this.optsRect.top < 0) { + this.tree.scrollTop = this.tree.scrollTop + (r.top - this.optsRect.top); + } + } else { + if (this.navIndex === 0) { + this.tree.scrollTop = 0; + } else if ((r.top + r.height) > (this.optsRect.top + this.optsRect.height)) { + this.tree.scrollTop = this.tree.scrollTop + ((r.top + r.height) - (this.optsRect.top + this.optsRect.height)); + } + + // Load another page if needed + if (this.navIndex === this.tree.childElementCount - 1 && this.requiresPagination) { + load.call(this); + } + } + + if (prevEl) { + util.removeClass(prevEl, "active"); + } + + util.addClass(this.items[this.navIndex], "active"); +}; + +/** + * Add a tag + * @param {HTMLElement} item + */ +var addTag = function(item) { + var that = this, + r; + + var docFrag = document.createDocumentFragment(); + var option = this.options[item.idx]; + var data = this.data ? this.data[item.idx] : option; + var content = this.customSelected ? this.config.renderSelection(data) : option.textContent; + + var tag = util.createElement("li", { + class: "selectr-tag", + html: content + }); + var btn = util.createElement("button", { + class: "selectr-tag-remove", + type: "button" + }); + + tag.appendChild(btn); + + // Set property to check against later + tag.idx = item.idx; + tag.tag = option.value; + + this.tags.push(tag); + + if (this.config.sortSelected) { + + var tags = this.tags.slice(); + + // Deal with values that contain numbers + r = function(val, arr) { + val.replace(/(\d+)|(\D+)/g, function(that, $1, $2) { + arr.push([$1 || Infinity, $2 || ""]); + }); + }; + + tags.sort(function(a, b) { + var x = [], + y = [], + ac, bc; + if (that.config.sortSelected === true) { + ac = a.tag; + bc = b.tag; + } else if (that.config.sortSelected === 'text') { + ac = a.textContent; + bc = b.textContent; + } + + r(ac, x); + r(bc, y); + + while (x.length && y.length) { + var ax = x.shift(); + var by = y.shift(); + var nn = (ax[0] - by[0]) || ax[1].localeCompare(by[1]); + if (nn) return nn; + } + + return x.length - y.length; + }); + + util.each(tags, function(i, tg) { + docFrag.appendChild(tg); + }); + + this.label.innerHTML = ""; + + } else { + docFrag.appendChild(tag); + } + + if (this.config.taggable) { + this.label.insertBefore(docFrag, this.input.parentNode); + } else { + this.label.appendChild(docFrag); + } +}; + +/** + * Remove a tag + * @param {HTMLElement} item + * @return {void} + */ +var removeTag = function(item) { + var tag = false; + + util.each(this.tags, function(i, t) { + if (t.idx === item.idx) { + tag = t; + } + }, this); + + if (tag) { + this.label.removeChild(tag); + this.tags.splice(this.tags.indexOf(tag), 1); + } +}; + +/** + * Load the next page of items + * @return {void} + */ +var load = function() { + var tree = this.tree; + var scrollTop = tree.scrollTop; + var scrollHeight = tree.scrollHeight; + var offsetHeight = tree.offsetHeight; + var atBottom = scrollTop >= (scrollHeight - offsetHeight); + + if ((atBottom && this.pageIndex < this.pages.length)) { + var f = document.createDocumentFragment(); + + util.each(this.pages[this.pageIndex], function(i, item) { + appendItem(item, f, this.customOption); + }, this); + + tree.appendChild(f); + + this.pageIndex++; + + this.emit("selectr.paginate", { + items: this.items.length, + total: this.data.length, + page: this.pageIndex, + pages: this.pages.length + }); + } +}; + +/** + * Clear a search + * @return {void} + */ +var clearSearch = function() { + if (this.config.searchable || this.config.taggable) { + this.input.value = null; + this.searching = false; + if (this.config.searchable) { + util.removeClass(this.inputContainer, "active"); + } + + if (util.hasClass(this.container, "notice")) { + util.removeClass(this.container, "notice"); + util.addClass(this.container, "open"); + this.input.focus(); + } + + util.each(this.items, function(i, item) { + // Items that didn't match need the class + // removing to make them visible again + util.removeClass(item, "excluded"); + // Remove the span element for underlining matched items + if (!this.customOption) { + item.innerHTML = item.textContent; + } + }, this); + } +}; + +/** + * Query matching for searches + * @param {string} query + * @param {HTMLOptionElement} option + * @return {bool} + */ +var match = function(query, option) { + var result = new RegExp(query, "i").exec(option.textContent); + if (result) { + return option.textContent.replace(result[0], "" + result[0] + ""); + } + return false; +}; + +// Main Lib +var Selectr = function(el, config) { + + config = config || {}; + + if (!el) { + throw new Error("You must supply either a HTMLSelectElement or a CSS3 selector string."); + } + + this.el = el; + + // CSS3 selector string + if (typeof el === "string") { + this.el = document.querySelector(el); + } + + if (this.el === null) { + throw new Error("The element you passed to Selectr can not be found."); + } + + if (this.el.nodeName.toLowerCase() !== "select") { + throw new Error("The element you passed to Selectr is not a HTMLSelectElement."); + } + + this.render(config); +}; + +/** + * Render the instance + * @param {object} config + * @return {void} + */ +Selectr.prototype.render = function(config) { + + if (this.rendered) return; + + // Merge defaults with user set config + this.config = util.extend(defaultConfig, config); + + // Store type + this.originalType = this.el.type; + + // Store tabIndex + this.originalIndex = this.el.tabIndex; + + // Store defaultSelected options for form reset + this.defaultSelected = []; + + // Store the original option count + this.originalOptionCount = this.el.options.length; + + if (this.config.multiple || this.config.taggable) { + this.el.multiple = true; + } + + // Disabled? + this.disabled = isset(this.config, "disabled"); + + this.opened = false; + + if (this.config.taggable) { + this.config.searchable = false; + } + + this.navigating = false; + + this.mobileDevice = false; + if (/Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(navigator.userAgent)) { + this.mobileDevice = true; + } + + this.customOption = this.config.hasOwnProperty("renderOption") && typeof this.config.renderOption === "function"; + this.customSelected = this.config.hasOwnProperty("renderSelection") && typeof this.config.renderSelection === "function"; + + // Enable event emitter + Events.mixin(this); + + build.call(this); + + this.bindEvents(); + + this.update(); + + this.optsRect = util.rect(this.tree); + + this.rendered = true; + + // Fixes macOS Safari bug #28 + if (!this.el.multiple) { + this.el.selectedIndex = this.selectedIndex; + } + + var that = this; + setTimeout(function() { + that.emit("selectr.init"); + }, 20); +}; + +Selectr.prototype.getSelected = function () { + var selected = this.el.querySelectorAll('option:checked'); + return selected; +}; + +Selectr.prototype.getSelectedProperties = function (prop) { + var selected = this.getSelected(); + var values = [].slice.call(selected) + .map(function(option) { return option[prop]; }) + .filter(function(i) { return i!==null && i!==undefined; }); + return values; +}; + +/** + * Attach the required event listeners + */ +Selectr.prototype.bindEvents = function() { + + var that = this; + + this.events = {}; + + this.events.dismiss = dismiss.bind(this); + this.events.navigate = navigate.bind(this); + this.events.reset = this.reset.bind(this); + + if (this.config.nativeDropdown || this.mobileDevice) { + + this.container.addEventListener("touchstart", function(e) { + if (e.changedTouches[0].target === that.el) { + that.toggle(); + } + }); + + if (this.config.nativeDropdown || this.mobileDevice) { + this.container.addEventListener("click", function(e) { + e.preventDefault(); // Jos: Added to prevent emitting clear directly after select + e.stopPropagation(); // Jos: Added to prevent emitting clear directly after select + + if (e.target === that.el) { + that.toggle(); + } + }); + } + + var getChangedOptions = function(last, current) { + var added=[], removed=last.slice(0); + var idx; + for (var i=0; i -1) + removed.splice(idx, 1); + else + added.push(current[i]); + } + return [added, removed]; + }; + + // Listen for the change on the native select + // and update accordingly + this.el.addEventListener("change", function(e) { + if (that.el.multiple) { + var indexes = that.getSelectedProperties('idx'); + var changes = getChangedOptions(that.selectedIndexes, indexes); + + util.each(changes[0], function(i, idx) { + that.select(idx); + }, that); + + util.each(changes[1], function(i, idx) { + that.deselect(idx); + }, that); + + } else { + if (that.el.selectedIndex > -1) { + that.select(that.el.selectedIndex); + } + } + }); + + } + + // Open the dropdown with Enter key if focused + if (this.config.nativeDropdown) { + this.container.addEventListener("keydown", function(e) { + if (e.key === "Enter" && that.selected === document.activeElement) { + // Show the native + that.toggle(); + + // Focus on the native multiselect + setTimeout(function() { + that.el.focus(); + }, 200); + } + }); + } + + // Non-native dropdown + this.selected.addEventListener("click", function(e) { + + if (!that.disabled) { + that.toggle(); + } + + e.preventDefault(); + e.stopPropagation(); // Jos: Added to prevent emitting clear directly after select + }); + + // Remove tag + this.label.addEventListener("click", function(e) { + if (util.hasClass(e.target, "selectr-tag-remove")) { + that.deselect(e.target.parentNode.idx); + } + }); + + // Clear input + if (this.selectClear) { + this.selectClear.addEventListener("click", this.clear.bind(this)); + } + + // Prevent text selection + this.tree.addEventListener("mousedown", function(e) { + e.preventDefault(); + }); + + // Select / deselect items + this.tree.addEventListener("click", function(e) { + e.preventDefault(); // Jos: Added to prevent emitting clear directly after select + e.stopPropagation(); // Jos: Added to prevent emitting clear directly after select + + var item = util.closest(e.target, function(el) { + return el && util.hasClass(el, "selectr-option"); + }); + + if (item) { + if (!util.hasClass(item, "disabled")) { + if (util.hasClass(item, "selected")) { + if (that.el.multiple || !that.el.multiple && that.config.allowDeselect) { + that.deselect(item.idx); + } + } else { + that.select(item.idx); + } + + if (that.opened && !that.el.multiple) { + that.close(); + } + } + } + }); + + // Mouseover list items + this.tree.addEventListener("mouseover", function(e) { + if (util.hasClass(e.target, "selectr-option")) { + if (!util.hasClass(e.target, "disabled")) { + util.removeClass(that.items[that.navIndex], "active"); + + util.addClass(e.target, "active"); + + that.navIndex = [].slice.call(that.items).indexOf(e.target); + } + } + }); + + // Searchable + if (this.config.searchable) { + // Show / hide the search input clear button + + this.input.addEventListener("focus", function(e) { + that.searching = true; + }); + + this.input.addEventListener("blur", function(e) { + that.searching = false; + }); + + this.input.addEventListener("keyup", function(e) { + that.search(); + + if (!that.config.taggable) { + // Show / hide the search input clear button + if (this.value.length) { + util.addClass(this.parentNode, "active"); + } else { + util.removeClass(this.parentNode, "active"); + } + } + }); + + // Clear the search input + this.inputClear.addEventListener("click", function(e) { + that.input.value = null; + clearSearch.call(that); + + if (!that.tree.childElementCount) { + render.call(that); + } + }); + } + + if (this.config.taggable) { + this.input.addEventListener("keyup", function(e) { + + that.search(); + + if (that.config.taggable && this.value.length) { + var val = this.value.trim(); + + if (e.which === 13 || util.includes(that.tagSeperators, e.key)) { + + util.each(that.tagSeperators, function(i, k) { + val = val.replace(k, ''); + }); + + var option = that.add({ + value: val, + text: val, + selected: true + }, true); + + if (!option) { + this.value = ''; + that.setMessage('That tag is already in use.'); + } else { + that.close(); + clearSearch.call(that); + } + } + } + }); + } + + this.update = util.debounce(function() { + // Optionally close dropdown on scroll / resize (#11) + if (that.opened && that.config.closeOnScroll) { + that.close(); + } + if (that.width) { + that.container.style.width = that.width; + } + that.invert(); + }, 50); + + if (this.requiresPagination) { + this.paginateItems = util.debounce(function() { + load.call(this); + }, 50); + + this.tree.addEventListener("scroll", this.paginateItems.bind(this)); + } + + // Dismiss when clicking outside the container + document.addEventListener("click", this.events.dismiss); + window.addEventListener("keydown", this.events.navigate); + + window.addEventListener("resize", this.update); + window.addEventListener("scroll", this.update); + + // Listen for form.reset() (@ambrooks, #13) + if (this.el.form) { + this.el.form.addEventListener("reset", this.events.reset); + } +}; + +/** + * Check for selected options + * @param {bool} reset + */ +Selectr.prototype.setSelected = function(reset) { + + // Select first option as with a native select-one element - #21, #24 + if (!this.config.data && !this.el.multiple && this.el.options.length) { + // Browser has selected the first option by default + if (this.el.selectedIndex === 0) { + if (!this.el.options[0].defaultSelected && !this.config.defaultSelected) { + this.el.selectedIndex = -1; + } + } + + this.selectedIndex = this.el.selectedIndex; + + if (this.selectedIndex > -1) { + this.select(this.selectedIndex); + } + } + + // If we're changing a select-one to select-multiple via the config + // and there are no selected options, the first option will be selected by the browser + // Let's prevent that here. + if (this.config.multiple && this.originalType === "select-one" && !this.config.data) { + if (this.el.options[0].selected && !this.el.options[0].defaultSelected) { + this.el.options[0].selected = false; + } + } + + util.each(this.options, function(i, option) { + if (option.selected && option.defaultSelected) { + this.select(option.idx); + } + }, this); + + if (this.config.selectedValue) { + this.setValue(this.config.selectedValue); + } + + if (this.config.data) { + + + if (!this.el.multiple && this.config.defaultSelected && this.el.selectedIndex < 0) { + this.select(0); + } + + var j = 0; + util.each(this.config.data, function(i, opt) { + // Check for group options + if (isset(opt, "children")) { + util.each(opt.children, function(x, item) { + if (item.hasOwnProperty("selected") && item.selected === true) { + this.select(j); + } + j++; + }, this); + } else { + if (opt.hasOwnProperty("selected") && opt.selected === true) { + this.select(j); + } + j++; + } + }, this); + } +}; + +/** + * Destroy the instance + * @return {void} + */ +Selectr.prototype.destroy = function() { + + if (!this.rendered) return; + + this.emit("selectr.destroy"); + + // Revert to select-single if programtically set to multiple + if (this.originalType === 'select-one') { + this.el.multiple = false; + } + + if (this.config.data) { + this.el.innerHTML = ""; + } + + // Remove the className from select element + util.removeClass(this.el, 'selectr-hidden'); + + // Remove reset listener from parent form + if (this.el.form) { + util.off(this.el.form, "reset", this.events.reset); + } + + // Remove event listeners attached to doc and win + util.off(document, "click", this.events.dismiss); + util.off(document, "keydown", this.events.navigate); + util.off(window, "resize", this.update); + util.off(window, "scroll", this.update); + + // Replace the container with the original select element + this.container.parentNode.replaceChild(this.el, this.container); + + this.rendered = false; +}; + +/** + * Change an options state + * @param {Number} index + * @return {void} + */ +Selectr.prototype.change = function(index) { + var item = this.items[index], + option = this.options[index]; + + if (option.disabled) { + return; + } + + if (option.selected && util.hasClass(item, "selected")) { + this.deselect(index); + } else { + this.select(index); + } + + if (this.opened && !this.el.multiple) { + this.close(); + } +}; + +/** + * Select an option + * @param {Number} index + * @return {void} + */ +Selectr.prototype.select = function(index) { + + var item = this.items[index], + options = [].slice.call(this.el.options), + option = this.options[index]; + + if (this.el.multiple) { + if (util.includes(this.selectedIndexes, index)) { + return false; + } + + if (this.config.maxSelections && this.tags.length === this.config.maxSelections) { + this.setMessage("A maximum of " + this.config.maxSelections + " items can be selected.", true); + return false; + } + + this.selectedValues.push(option.value); + this.selectedIndexes.push(index); + + addTag.call(this, item); + } else { + var data = this.data ? this.data[index] : option; + this.label.innerHTML = this.customSelected ? this.config.renderSelection(data) : option.textContent; + + this.selectedValue = option.value; + this.selectedIndex = index; + + util.each(this.options, function(i, o) { + var opt = this.items[i]; + + if (i !== index) { + if (opt) { + util.removeClass(opt, "selected"); + } + o.selected = false; + o.removeAttribute("selected"); + } + }, this); + } + + if (!util.includes(options, option)) { + this.el.add(option); + } + + item.setAttribute("aria-selected", true); + + util.addClass(item, "selected"); + util.addClass(this.container, "has-selected"); + + option.selected = true; + option.setAttribute("selected", ""); + + this.emit("selectr.change", option); + + this.emit("selectr.select", option); +}; + +/** + * Deselect an option + * @param {Number} index + * @return {void} + */ +Selectr.prototype.deselect = function(index, force) { + var item = this.items[index], + option = this.options[index]; + + if (this.el.multiple) { + var selIndex = this.selectedIndexes.indexOf(index); + this.selectedIndexes.splice(selIndex, 1); + + var valIndex = this.selectedValues.indexOf(option.value); + this.selectedValues.splice(valIndex, 1); + + removeTag.call(this, item); + + if (!this.tags.length) { + util.removeClass(this.container, "has-selected"); + } + } else { + + if (!force && !this.config.clearable && !this.config.allowDeselect) { + return false; + } + + this.label.innerHTML = ""; + this.selectedValue = null; + + this.el.selectedIndex = this.selectedIndex = -1; + + util.removeClass(this.container, "has-selected"); + } + + + this.items[index].setAttribute("aria-selected", false); + + util.removeClass(this.items[index], "selected"); + + option.selected = false; + + option.removeAttribute("selected"); + + this.emit("selectr.change", null); + + this.emit("selectr.deselect", option); +}; + +/** + * Programmatically set selected values + * @param {String|Array} value - A string or an array of strings + */ +Selectr.prototype.setValue = function(value) { + var isArray = Array.isArray(value); + + if (!isArray) { + value = value.toString().trim(); + } + + // Can't pass array to select-one + if (!this.el.multiple && isArray) { + return false; + } + + util.each(this.options, function(i, option) { + if (isArray && util.includes(value.toString(), option.value) || option.value === value) { + this.change(option.idx); + } + }, this); +}; + +/** + * Set the selected value(s) + * @param {bool} toObject Return only the raw values or an object + * @param {bool} toJson Return the object as a JSON string + * @return {mixed} Array or String + */ +Selectr.prototype.getValue = function(toObject, toJson) { + var value; + + if (this.el.multiple) { + if (toObject) { + if (this.selectedIndexes.length) { + value = {}; + value.values = []; + util.each(this.selectedIndexes, function(i, index) { + var option = this.options[index]; + value.values[i] = { + value: option.value, + text: option.textContent + }; + }, this); + } + } else { + value = this.selectedValues.slice(); + } + } else { + if (toObject) { + var option = this.options[this.selectedIndex]; + value = { + value: option.value, + text: option.textContent + }; + } else { + value = this.selectedValue; + } + } + + if (toObject && toJson) { + value = JSON.stringify(value); + } + + return value; +}; + +/** + * Add a new option or options + * @param {object} data + */ +Selectr.prototype.add = function(data, checkDuplicate) { + if (data) { + + this.data = this.data || []; + this.items = this.items || []; + this.options = this.options || []; + + if (Array.isArray(data)) { + // We have an array on items + util.each(data, function(i, obj) { + this.add(obj, checkDuplicate); + }, this); + } + // User passed a single object to the method + // or Selectr passed an object from an array + else if ("[object Object]" === Object.prototype.toString.call(data)) { + + if (checkDuplicate) { + var dupe = false; + + util.each(this.options, function(i, option) { + if (option.value.toLowerCase() === data.value.toLowerCase()) { + dupe = true; + } + }); + + if (dupe) { + return false; + } + } + + var option = util.createElement('option', data); + + this.data.push(data); + + // Add the new option to the list + this.options.push(option); + + // Add the index for later use + option.idx = this.options.length > 0 ? this.options.length - 1 : 0; + + // Create a new item + createItem.call(this, option); + + // Select the item if required + if (data.selected) { + this.select(option.idx); + } + + return option; + } + + // We may have had an empty select so update + // the placeholder to reflect the changes. + this.setPlaceholder(); + + // Recount the pages + if (this.config.pagination) { + this.paginate(); + } + + return true; + } +}; + +/** + * Remove an option or options + * @param {Mixed} o Array, integer (index) or string (value) + * @return {Void} + */ +Selectr.prototype.remove = function(o) { + var options = []; + if (Array.isArray(o)) { + util.each(o, function(i, opt) { + if (util.isInt(opt)) { + options.push(this.getOptionByIndex(opt)); + } else if (typeof o === "string") { + options.push(this.getOptionByValue(opt)); + } + }, this); + + } else if (util.isInt(o)) { + options.push(this.getOptionByIndex(o)); + } else if (typeof o === "string") { + options.push(this.getOptionByValue(o)); + } + + if (options.length) { + var index; + util.each(options, function(i, option) { + index = option.idx; + + // Remove the HTMLOptionElement + this.el.remove(option); + + // Remove the reference from the option array + this.options.splice(index, 1); + + // If the item has a parentNode (group element) it needs to be removed + // otherwise the render function will still append it to the dropdown + var parentNode = this.items[index].parentNode; + + if (parentNode) { + parentNode.removeChild(this.items[index]); + } + + // Remove reference from the items array + this.items.splice(index, 1); + + // Reset the indexes + util.each(this.options, function(i, opt) { + opt.idx = i; + this.items[i].idx = i; + }, this); + }, this); + + // We may have had an empty select now so update + // the placeholder to reflect the changes. + this.setPlaceholder(); + + // Recount the pages + if (this.config.pagination) { + this.paginate(); + } + } +}; + +/** + * Remove all options + */ +Selectr.prototype.removeAll = function() { + + // Clear any selected options + this.clear(true); + + // Remove the HTMLOptionElements + util.each(this.el.options, function(i, option) { + this.el.remove(option); + }, this); + + // Empty the dropdown + util.truncate(this.tree); + + // Reset variables + this.items = []; + this.options = []; + this.data = []; + + this.navIndex = 0; + + if (this.requiresPagination) { + this.requiresPagination = false; + + this.pageIndex = 1; + this.pages = []; + } + + // Update the placeholder + this.setPlaceholder(); +}; + +/** + * Perform a search + * @param {string} query The query string + */ +Selectr.prototype.search = function(string) { + + if (this.navigating) return; + + string = string || this.input.value; + + var f = document.createDocumentFragment(); + + // Remove message + this.removeMessage(); + + // Clear the dropdown + util.truncate(this.tree); + + if (string.length > 1) { + // Check the options for the matching string + util.each(this.options, function(i, option) { + var item = this.items[option.idx]; + var includes = util.includes(option.textContent.toLowerCase(), string.toLowerCase()); + + if (includes && !option.disabled) { + + appendItem(item, f, this.customOption); + + util.removeClass(item, "excluded"); + + // Underline the matching results + if (!this.customOption) { + item.innerHTML = match(string, option); + } + } else { + util.addClass(item, "excluded"); + } + }, this); + + + if (!f.childElementCount) { + if (!this.config.taggable) { + this.setMessage("no results."); + } + } else { + // Highlight top result (@binary-koan #26) + var prevEl = this.items[this.navIndex]; + var firstEl = f.firstElementChild; + + util.removeClass(prevEl, "active"); + + this.navIndex = firstEl.idx; + + util.addClass(firstEl, "active"); + } + + } else { + render.call(this); + } + + this.tree.appendChild(f); +}; + +/** + * Toggle the dropdown + * @return {void} + */ +Selectr.prototype.toggle = function() { + if (!this.disabled) { + if (this.opened) { + this.close(); + } else { + this.open(); + } + } +}; + +/** + * Open the dropdown + * @return {void} + */ +Selectr.prototype.open = function() { + + var that = this; + + if (!this.options.length) { + return false; + } + + if (!this.opened) { + this.emit("selectr.open"); + } + + this.opened = true; + + if (this.mobileDevice || this.config.nativeDropdown) { + util.addClass(this.container, "native-open"); + + if (this.config.data) { + // Dump the options into the select + // otherwise the native dropdown will be empty + util.each(this.options, function(i, option) { + this.el.add(option); + }, this); + } + + return; + } + + util.addClass(this.container, "open"); + + render.call(this); + + this.invert(); + + this.tree.scrollTop = 0; + + util.removeClass(this.container, "notice"); + + this.selected.setAttribute("aria-expanded", true); + + this.tree.setAttribute("aria-hidden", false); + this.tree.setAttribute("aria-expanded", true); + + if (this.config.searchable && !this.config.taggable) { + setTimeout(function() { + that.input.focus(); + // Allow tab focus + that.input.tabIndex = 0; + }, 10); + } +}; + +/** + * Close the dropdown + * @return {void} + */ +Selectr.prototype.close = function() { + + if (this.opened) { + this.emit("selectr.close"); + } + + this.opened = false; + + if (this.mobileDevice || this.config.nativeDropdown) { + util.removeClass(this.container, "native-open"); + return; + } + + var notice = util.hasClass(this.container, "notice"); + + if (this.config.searchable && !notice) { + this.input.blur(); + // Disable tab focus + this.input.tabIndex = -1; + this.searching = false; + } + + if (notice) { + util.removeClass(this.container, "notice"); + this.notice.textContent = ""; + } + + util.removeClass(this.container, "open"); + util.removeClass(this.container, "native-open"); + + this.selected.setAttribute("aria-expanded", false); + + this.tree.setAttribute("aria-hidden", true); + this.tree.setAttribute("aria-expanded", false); + + util.truncate(this.tree); + clearSearch.call(this); +}; + + +/** + * Enable the element + * @return {void} + */ +Selectr.prototype.enable = function() { + this.disabled = false; + this.el.disabled = false; + + this.selected.tabIndex = this.originalIndex; + + if (this.el.multiple) { + util.each(this.tags, function(i, t) { + t.lastElementChild.tabIndex = 0; + }); + } + + util.removeClass(this.container, "selectr-disabled"); +}; + +/** + * Disable the element + * @param {boolean} container Disable the container only (allow value submit with form) + * @return {void} + */ +Selectr.prototype.disable = function(container) { + if (!container) { + this.el.disabled = true; + } + + this.selected.tabIndex = -1; + + if (this.el.multiple) { + util.each(this.tags, function(i, t) { + t.lastElementChild.tabIndex = -1; + }); + } + + this.disabled = true; + util.addClass(this.container, "selectr-disabled"); +}; + + +/** + * Reset to initial state + * @return {void} + */ +Selectr.prototype.reset = function() { + if (!this.disabled) { + this.clear(); + + this.setSelected(true); + + util.each(this.defaultSelected, function(i, idx) { + this.select(idx); + }, this); + + this.emit("selectr.reset"); + } +}; + +/** + * Clear all selections + * @return {void} + */ +Selectr.prototype.clear = function(force) { + + if (this.el.multiple) { + // Loop over the selectedIndexes so we don't have to loop over all the options + // which can be costly if there are a lot of them + + if (this.selectedIndexes.length) { + // Copy the array or we'll get an error + var indexes = this.selectedIndexes.slice(); + + util.each(indexes, function(i, idx) { + this.deselect(idx); + }, this); + } + } else { + if (this.selectedIndex > -1) { + this.deselect(this.selectedIndex, force); + } + } + + this.emit("selectr.clear"); +}; + +/** + * Return serialised data + * @param {boolean} toJson + * @return {mixed} Returns either an object or JSON string + */ +Selectr.prototype.serialise = function(toJson) { + var data = []; + util.each(this.options, function(i, option) { + var obj = { + value: option.value, + text: option.textContent + }; + + if (option.selected) { + obj.selected = true; + } + if (option.disabled) { + obj.disabled = true; + } + data[i] = obj; + }); + + return toJson ? JSON.stringify(data) : data; +}; + +/** + * Localised version of serialise() method + */ +Selectr.prototype.serialize = function(toJson) { + return this.serialise(toJson); +}; + +/** + * Sets the placeholder + * @param {String} placeholder + */ +Selectr.prototype.setPlaceholder = function(placeholder) { + // Set the placeholder + placeholder = placeholder || this.config.placeholder || this.el.getAttribute("placeholder"); + + if (!this.options.length) { + placeholder = "No options available"; + } + + this.placeEl.innerHTML = placeholder; +}; + +/** + * Paginate the option list + * @return {Array} + */ +Selectr.prototype.paginate = function() { + if (this.items.length) { + var that = this; + + this.pages = this.items.map(function(v, i) { + return i % that.config.pagination === 0 ? that.items.slice(i, i + that.config.pagination) : null; + }).filter(function(pages) { + return pages; + }); + + return this.pages; + } +}; + +/** + * Display a message + * @param {String} message The message + */ +Selectr.prototype.setMessage = function(message, close) { + if (close) { + this.close(); + } + util.addClass(this.container, "notice"); + this.notice.textContent = message; +}; + +/** + * Dismiss the current message + */ +Selectr.prototype.removeMessage = function() { + util.removeClass(this.container, "notice"); + this.notice.innerHTML = ""; +}; + +/** + * Keep the dropdown within the window + * @return {void} + */ +Selectr.prototype.invert = function() { + var rt = util.rect(this.selected), + oh = this.tree.parentNode.offsetHeight, + wh = window.innerHeight, + doInvert = rt.top + rt.height + oh > wh; + + if (doInvert) { + util.addClass(this.container, "inverted"); + this.isInverted = true; + } else { + util.removeClass(this.container, "inverted"); + this.isInverted = false; + } + + this.optsRect = util.rect(this.tree); +}; + +/** + * Get an option via it's index + * @param {Integer} index The index of the HTMLOptionElement required + * @return {HTMLOptionElement} + */ +Selectr.prototype.getOptionByIndex = function(index) { + return this.options[index]; +}; + +/** + * Get an option via it's value + * @param {String} value The value of the HTMLOptionElement required + * @return {HTMLOptionElement} + */ +Selectr.prototype.getOptionByValue = function(value) { + var option = false; + + for (var i = 0, l = this.options.length; i < l; i++) { + if (this.options[i].value.trim() === value.toString().trim()) { + option = this.options[i]; + break; + } + } + + return option; +}; + +module.exports = Selectr; diff --git a/src/js/showSortModal.js b/src/js/showSortModal.js index 45e9ed3..fc825f7 100644 --- a/src/js/showSortModal.js +++ b/src/js/showSortModal.js @@ -53,7 +53,7 @@ function showSortModal (node, container) { parent: container, content: content, overlayClass: 'jsoneditor-modal-overlay', - modalClass: 'jsoneditor-modal' + modalClass: 'jsoneditor-modal jsoneditor-modal-sort' }) .afterCreate(function (modal) { var form = modal.modalElem().querySelector('form'); diff --git a/src/js/showTransformModal.js b/src/js/showTransformModal.js index d990f60..aaf7df5 100644 --- a/src/js/showTransformModal.js +++ b/src/js/showTransformModal.js @@ -1,5 +1,6 @@ var jmespath = require('jmespath'); var picoModal = require('picomodal'); +var Selectr = require('./assets/selectr/selectr'); var translate = require('./i18n').translate; var debounce = require('./util').debounce; @@ -14,7 +15,7 @@ var MAX_PREVIEW_LINES = 100; function showTransformModal (node, container) { var value = node.getValue(); - var content = '