Просмотр исходного кода

add multi-select web component - extension

221V 1 год назад
Родитель
Сommit
9bca9b2c1c

+ 1 - 1
include/nitro.hrl

@@ -64,7 +64,7 @@
 -record(option,   {?ELEMENT_BASE(element_select), disabled = [], label = [], selected = false, value = []} ).
 -record(output,   {?ELEMENT_BASE(element_output), for, form, name} ).
 -record(progress, {?ELEMENT_BASE(element_progress), max = [], value = []} ).
--record(select,   {?ELEMENT_BASE(element_select), autofocus = [], disabled = [], form = [], multiple = [], name = [], required = [], size = []} ).
+-record(select,   {?ELEMENT_BASE(element_select), multi_select = false, autofocus = [], disabled = [], form = [], multiple = [], name = [], required = [], size = []} ).
 -record(textarea, {?ELEMENT_BASE(element_textarea), autofocus = [], cols = [], dirname = [], disabled = [], form = [], maxlength, name, placeholder, readonly = [], required = [], rows = [], wrap = [], value = []} ).
 
 %% HTML Form inputs

+ 82 - 0
priv/css/multi-select-webcomponent.css

@@ -0,0 +1,82 @@
+
+multi-select{
+  display: flex;
+  align-items: end;
+  height: max-content;
+  border: 1px solid #ccc;
+  border-radius: 5px;
+  box-shadow: inset 0 1px 3px #ddd;
+}
+/*
+button.msw-clearbutton{
+  width: 40px;
+  height: 40px;
+  border: 1px solid rgba(91,192,222,.6);
+  cursor: pointer;
+  background: #fff url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M19.4958 6.49499C19.7691 6.22162 19.7691 5.7784 19.4958 5.50504C19.2224 5.23167 18.7792 5.23167 18.5058 5.50504L12.5008 11.5101L6.49576 5.50504C6.22239 5.23167 5.77917 5.23167 5.50581 5.50504C5.23244 5.7784 5.23244 6.22162 5.50581 6.49499L11.5108 12.5L5.50581 18.505C5.23244 18.7784 5.23244 19.2216 5.50581 19.495C5.77917 19.7684 6.22239 19.7684 6.49576 19.495L12.5008 13.49L18.5058 19.495C18.7792 19.7684 19.2224 19.7684 19.4958 19.495C19.7691 19.2216 19.7691 18.7784 19.4958 18.505L13.4907 12.5L19.4958 6.49499Z' fill='%23f00'/%3E%3C/svg%3E") 50% 50% no-repeat;
+}
+button.msw-selectallbutton{
+  width: 40px;
+  height: 40px;
+  border: 1px solid rgba(91,192,222,.6);
+  cursor: pointer;
+  background: #fff url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 448 512' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M413.505 91.951L133.49 371.966l-98.995-98.995c-4.686-4.686-12.284-4.686-16.971 0L6.211 284.284c-4.686 4.686-4.686 12.284 0 16.971l118.794 118.794c4.686 4.686 12.284 4.686 16.971 0l299.813-299.813c4.686-4.686 4.686-12.284 0-16.971l-11.314-11.314c-4.686-4.686-12.284-4.686-16.97 0z' fill='%230f0'/%3E%3C/svg%3E") 50% 50% no-repeat;
+}
+*/
+input.msw-searchbox{
+  width: 100%;
+  flex-grow: 1;
+  border: none !important;
+  box-shadow: none !important;
+  outline: none;
+}
+div.msw-selected{
+  display: flex;
+  flex-wrap: wrap;
+  flex-grow: 1;
+}
+div.msw-selecteditem{
+  display: inline-block;
+  padding: 0 0.35em 0.8em 0.8em;
+  margin: 0.25em;
+  font-size: 0.75em;
+  font-weight: 700;
+  line-height: 1;
+  color: #fff;
+  text-align: center;
+  white-space: nowrap;
+  background-color: #0d6efd;
+  border-radius: 0.25em;
+  cursor: pointer;
+  -webkit-user-select: none;
+  user-select: none;
+}
+div.msw-selecteditem::after {
+  content: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M19.4958 6.49499C19.7691 6.22162 19.7691 5.7784 19.4958 5.50504C19.2224 5.23167 18.7792 5.23167 18.5058 5.50504L12.5008 11.5101L6.49576 5.50504C6.22239 5.23167 5.77917 5.23167 5.50581 5.50504C5.23244 5.7784 5.23244 6.22162 5.50581 6.49499L11.5108 12.5L5.50581 18.505C5.23244 18.7784 5.23244 19.2216 5.50581 19.495C5.77917 19.7684 6.22239 19.7684 6.49576 19.495L12.5008 13.49L18.5058 19.495C18.7792 19.7684 19.2224 19.7684 19.4958 19.495C19.7691 19.2216 19.7691 18.7784 19.4958 18.505L13.4907 12.5L19.4958 6.49499Z' fill='%23f00'/%3E%3C/svg%3E");
+  display: inline-block;
+  position: relative;
+  top: 5px;
+  margin: 0 3px 0 5px;
+  width: 15px;
+  height: 15px;
+  /* background-color: #fff; */
+}
+div.msw-dropdown{
+  display: none;
+  width: 100%;
+  position: relative;
+  z-index: 2;
+}
+div.msw-dropdownitem{
+  background: #fff; /* #7DD9F5 */
+  padding: 5px;
+  -webkit-user-select: none;
+  user-select: none;
+}
+div.msw-dropdownitem:hover{
+  background: #AEE4F5;
+}
+div.msw-disabled{
+  background: #F0F0F0;
+}
+

+ 64 - 0
priv/js/multi-select-webcomponent.min.js

@@ -0,0 +1,64 @@
+/* @honatas/multi-select-webcomponent 1.2.0 edited */
+
+function htmlEntities(str) {
+  return String(str)
+  .replace(/&/g, '&')
+  .replace(/"/g, '"')
+  .replace(/'/g, ''')
+  .replace(/`/g, '`')
+  .replace(/</g, '&lt;')
+  .replace(/>/g, '&gt;')
+  .replace(/\|/g, '&#124;');
+}
+
+function is_mobile(){if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){return true}else{return false}}
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).MultiselectWebcomponent=e()}(this,(function(){"use strict";class t extends HTMLElement{constructor(){var t,e;super(),this.options=[],this.is_modile=is_mobile(),this.optgroup=this.getAttribute("optgroup")||false, /* multi-select parameter 'optgroup="true"' for use disabled as optgroup '<option value="" disabled>Локації ---</option>' */
+this.searchbox=document.createElement("input"),this.dropdown=document.createElement("div"),this.selected=document.createElement("div"),
+this.querySelectorAll("option").forEach((t=>this.options.push(t.cloneNode(!0)))),this.setValuesOnConstructor( htmlEntities( this.getAttribute("value") ) ),this.searchbox.type="text",
+this.searchbox.className=`msw-searchbox ${this.getAttribute("searchbox")||""}`,this.searchbox.addEventListener("keyup",(t=>this.onSearchboxKeyup(t))),
+this.selected.className=`msw-selected ${this.getAttribute("selected")||""}`,
+this.dropdown.className=`msw-dropdown ${this.getAttribute("dropdown")||""}`,this.dropdown.addEventListener("click",(()=>this.onDropdownClick())),this.innerHTML="",
+this.appendChild(this.selected),null===(t=this.parentNode)||void 0===t||t.insertBefore(this.dropdown,this.nextSibling),
+this.addEventListener("click",(()=>this.onMultiselectClick())),
+null===(e=this.parentElement)||void 0===e||e.addEventListener("mouseleave",(()=>this.dropdown.style.display="none")),
+this.searchbox.placeholder=this.getAttribute("placeholder")||"";
+const s=this.getAttribute("disabled");s&&"false"!==s&&(this.searchbox.disabled=!0),this.build()}
+set value(t){for(const t of this.options)t.selected=!1;if(t&&0!=t.length){for(const e of this.options)t.includes(e.value)&&(e.selected=!0);this.build()}}
+setValuesOnConstructor(t){if(!t)return;const e=t.split(",");for(const t of this.options)e.includes(t.value)&&(t.selected=!0)}
+get value(){const t=[];for(const e of this.options)e.selected&&t.push( htmlEntities(e.value) );return t}
+set disabled(t){this.setAttribute("disabled",t?"true":"false"),this.searchbox.disabled=t,this.build()}
+get disabled(){const t=this.getAttribute("disabled");return!(!t||"false"===t)}
+set placeholder(t){this.setAttribute("placeholder",t),this.searchbox.placeholder=t}
+get placeholder(){return this.getAttribute("placeholder")}
+clear(){this.options=[],this.build()}
+build(){
+this.selected.innerHTML="",this.dropdown.innerHTML="";
+if(!this.optgroup){/* without optgroups */ for(const t of this.options)t.selected?this.selected.appendChild(this.buildSelectedItem(t)):this.dropdown.appendChild(this.buildDropdownItem(t));
+}else{ /* with optgroups */
+for(let j=0;j<this.options.length;j++){let t = this.options[j];if(t.selected){this.selected.appendChild(this.buildSelectedItem(t))}else{
+let is_next=!!this.options[j+1],skip=true;
+if(t.disabled){if(is_next){
+  for(let k=j+1;k<this.options.length;k++){if(this.options[k].selected)continue;if(!this.options[k].disabled){skip=false;break}break} /* break when disabled/not selected and not disabled */
+  }else{/* disabled and no next */}}else skip=false;
+  if(!skip)/* not disabled or not skip */ this.dropdown.appendChild(this.buildDropdownItem(t));
+}}}
+this.selected.appendChild(this.searchbox),this.dispatchEvent(new Event("change"));
+if(!this.dropdown.childElementCount){this.searchbox.style.display="none"}else{this.searchbox.style.display="block"}}
+buildSelectedItem(t){const e=document.createElement("div");return e.className=`msw-selecteditem ${this.getAttribute("selecteditem")||""}`,e.title="видалити",e.innerText=t.textContent,e.dataset.value=htmlEntities(t.value),this.disabled||e.addEventListener("click",(t=>this.onSelectedClick(t))),e}
+buildDropdownItem(t){var classd;classd=(t.disabled?" msw-disabled":"");const e=document.createElement("div");return e.className=`msw-dropdownitem${classd} ${this.getAttribute("dropdownitem")||this.getAttribute("item")||""}`,e.innerText=t.textContent,e.dataset.value=htmlEntities(t.value),t.disabled||e.addEventListener("click",(t=>this.onItemClick(t))),e}
+findOptionByValue(t){for(const e of this.options)if(t===e.value)return e}
+chooseOption(t){t&&(t.selected=!0),this.searchbox.value="",this.build()}
+onItemClick(t){this.chooseOption(this.findOptionByValue(t.currentTarget.dataset.value));if(this.is_modile)this.dropdown.style.display="none"}
+onSelectedClick(t){const e=this.findOptionByValue(t.currentTarget.dataset.value);e&&(e.selected=!1),this.build()}
+onClearClick(t){this.options.forEach((t=>t.selected=!1)),this.build(),this.searchbox.value="",this.searchbox.focus(),t.stopPropagation()}
+onSelectAllClick(t){this.options.forEach((t=>t.selected=!0)),this.build(),this.dropdown.style.display="none",this.searchbox.value="",this.searchbox.focus(),t.stopPropagation()}
+onMultiselectClick(){!0!==this.disabled&&(this.dropdown.style.display="block",this.searchbox.focus())}
+onDropdownClick(){this.searchbox.focus()}
+onSearchboxKeyup(t){if(("Enter"===t.key||"NumpadEnter"===t.key)&&""!==this.searchbox.value&&(this.dropdown.firstChild)){
+return this.chooseOption(this.findOptionByValue(this.dropdown.firstChild.dataset.value)),void this.searchbox.focus();}
+if(this.dropdown.innerHTML="",""===this.searchbox.value)
+for(const t of this.options)t.selected||this.dropdown.appendChild(this.buildDropdownItem(t));
+else{ for(const t of this.options){
+!t.selected&&!t.disabled&&t.textContent&&t.textContent.toLocaleUpperCase().indexOf(this.searchbox.value.toLocaleUpperCase())>=0&&this.dropdown.appendChild(this.buildDropdownItem(t));}}
+this.dropdown.style.display="block"}}return t}));
+

+ 10 - 0
priv/js/multi-select-webcomponent__use.js

@@ -0,0 +1,10 @@
+
+/*
+
+https://github.com/Honatas/multi-select-webcomponent
+https://jsfiddle.net/Honatas/k2fsy4Lc/29
+
+*/
+
+window.customElements.define('multi-select', MultiselectWebcomponent);
+

+ 7 - 1
src/elements/element_select.erl

@@ -8,6 +8,8 @@
   render_element/1
 ]).
 
+%% select, multi-select, option, optgroup
+
 
 render_element(Record) when Record#select.show_if == false -> [<<>>];
 
@@ -49,7 +51,11 @@ render_element(Record = #select{}) ->
          _ -> []
        end} | Record#select.data_fields
   ],
-  wf_tags:emit_tag(<<"select">>, nitro:render(Record#select.body), Props);
+  wf_tags:emit_tag(
+    case Record#select.multi_select of
+      true -> <<"multi-select">>;
+      _ -> <<"select">>
+    end, nitro:render(Record#select.body), Props);
 
 
 render_element(Group = #optgroup{}) ->