소스 검색

Fix #43 Add an exclude filter

isundil 1 년 전
부모
커밋
2fd211189a
6개의 변경된 파일214개의 추가작업 그리고 42개의 파일을 삭제
  1. 15 2
      static/public/css/style.css
  2. 29 5
      static/public/js/filters.js
  3. 160 0
      static/public/js/multiSelect.js
  4. 9 33
      static/public/js/uiFilter.js
  5. 1 1
      templates/_footer.js
  6. 0 1
      templates/_header.js

+ 15 - 2
static/public/css/style.css

@@ -55,12 +55,12 @@ body.filter-active #pch-navbar .bt-filter-inactive {
     display: none;
 }
 
-#pch-filterbar .dashboardcode-bsmultiselect .dropdown-menu {
+#pch-filterbar .multiselect-dropdown {
     max-height: 55vh;
     overflow-y: auto;
 }
 
-.dashboardcode-bsmultiselect ul.form-control input {
+.multiselect-input {
     color: #dedede;
 }
 
@@ -390,3 +390,16 @@ body.login-visible .login-wrapper {
     flex: 1;
 }
 
+.multiselect-wrapper { display: flex; flex-direction: column; background: var(--bs-body-bg); position: relative; }
+.multiselect-inputlist { border: 1px solid grey; padding: .3rem; display: inline-block; margin: 0; border-radius: 4px; }
+.multiselect-inputlist > li.checked { border: 1px solid grey; display: inline-block; border-radius: 4px; }
+.multiselect-inputlist > li.checked { margin-right: .3rem; padding: .2rem; }
+.multiselect-inputlist > li.checked > a { padding: .2rem; }
+.multiselect-inputlist > li.checked.indeterminate:before { content: "-"; background-color: red; color: white; height: 1rem; width: 1rem; display: inline-block; border-radius: 1rem; text-align: center; margin-right: .2rem; line-height: 1rem; }
+.multiselect-inputlist > .multiselect-input { border: none; padding: .3rem; background: inherit; }
+.multiselect-inputlist > .multiselect-input:focus { outline: none; }
+.multiselect-dropdown { margin: 0; padding: 0; list-style-type: none; display: inline-block; overflow: auto; background: inherit; border: 1px solid grey; position: absolute; z-index: 10000; top: 100%; width: 100%; }
+.multiselect-dropdown > li > label { display: block; margin: .3rem 0; }
+.multiselect-dropdown > li input { margin: 0 .5rem;}
+.multiselect-dropdown.hidden { display: none; }
+

+ 29 - 5
static/public/js/filters.js

@@ -1,7 +1,7 @@
 
 window.FilterManager = (() => {
     class FilterManager extends EventTarget {
-        #updateFilter() {
+        updateFilter() {
             for (let i of MediaStorage.Instance.medias) {
                 if (!i.ui)
                     continue;
@@ -20,7 +20,10 @@ window.FilterManager = (() => {
 
         setFilterValue(key, val) {
             this.#filters[key] = val;
-            this.#updateFilter();
+        }
+
+        setExclusionFilter(key, val) {
+            this.#excludeFilters[key] = val;
         }
 
         setChronologyRange(min, max) {
@@ -34,7 +37,7 @@ window.FilterManager = (() => {
                 this.#maxDate = max;
                 updated = true;
             }
-            updated && this.#updateFilter();
+            updated && this.updateFilter();
         }
 
         isFiltering() {
@@ -43,6 +46,10 @@ window.FilterManager = (() => {
                 if (this.#minDate > chronoRange.min || this.#maxDate < chronoRange.max)
                     return true;
             }
+            for (let i in this.#excludeFilters) {
+                if (this.#excludeFilters[i]?.length)
+                    return true;
+            }
             for (let i in this.#filters) {
                 if (this.#filters[i]?.length)
                     return true;
@@ -67,21 +74,38 @@ window.FilterManager = (() => {
                     continue;
                 if (i === "Tags" && this.#filters[i]) {
                     const mediaTags = mediaItem.allTags();
-                    if (this.#filters[i].indexOf(undefined) !== -1 && !mediaTags.length)
+                    if (this.#filters[i].indexOf("") !== -1 && !mediaTags.length)
                         continue;
                     if (this.#matchTags(mediaTags, this.#filters[i].filter(i => !!i)))
                         continue;
                     return false;
                 }
-                if (this.#filters[i].indexOf(undefined) >= 0 && !mediaItem.meta[i]?.value)
+                if (this.#filters[i].indexOf("") >= 0 && !mediaItem.meta[i]?.value)
                     continue;
                 if (this.#filters[i].indexOf(""+mediaItem.meta[i]?.value) === -1)
                     return false;
             }
+            for (let i in this.#excludeFilters) {
+                if (!this.#excludeFilters[i].length)
+                    continue;
+                if (i === "Tags" && this.#excludeFilters[i]) {
+                    const mediaTags = mediaItem.allTags();
+                    if (this.#excludeFilters[i].indexOf("") !== -1 && !mediaTags.length)
+                        return false;
+                    if (this.#matchTags(mediaTags, this.#excludeFilters[i].filter(i => !!i)))
+                        return false;
+                    continue;
+                }
+                if (this.#excludeFilters[i].indexOf("") >= 0 && !mediaItem.meta[i]?.value)
+                    return false;
+                if (this.#excludeFilters[i].indexOf(""+mediaItem.meta[i]?.value) >= 0)
+                    return false;;
+            }
             return true;
         }
 
         #filters = {};
+        #excludeFilters = {};
         #minDate = 0;
         #maxDate = Infinity;
     }

+ 160 - 0
static/public/js/multiSelect.js

@@ -0,0 +1,160 @@
+
+
+window.makeMultiselect = (items => {
+	class MultiSelect {
+		#htmlSelectElement;
+		#htmlRootElement;
+		#htmlInput;
+		#htmlInputList;
+		#htmlWrapper;
+		#htmlDropdown;
+		#ignoreMouseOut = false;
+		#options = [];
+		#eventHandlers = [];
+
+		constructor(htmlElement) {
+			this.#htmlSelectElement = htmlElement;
+			this.#htmlRootElement = htmlElement.parentElement;
+			for (let i of this.#htmlSelectElement.querySelectorAll("option"))
+				this.#options.push({
+					value: i.textContent,
+					checked: !!i.selected,
+					indeterminate: !!i.indeterminate,
+					htmlListItem: null
+				});
+		}
+		addEventListener(fncHandler) {
+			this.#eventHandlers.push(fncHandler);
+		}
+		#onUpdate() {
+			for (let i of this.#eventHandlers)
+				i(this.#options);
+		}
+		#updateTextInputValue() {
+			this.#htmlInputList.textContent = "";
+			this.#htmlInputList.appendChild(this.#htmlInput);
+			for (let i in this.#options) {
+				if (this.#options[i].checked) {
+					let li = document.createElement("li");
+					let closeBt = document.createElement("a");
+					closeBt.href = "#";
+					closeBt.textContent = "x";
+					closeBt.addEventListener("click", e => {
+						this.#options[i].checked = false;
+						this.#options[i].indeterminate = false;
+						this.#rebuildDropdownContent();
+						this.#updateTextInputValue();
+						this.#onUpdate();
+						e.preventDefault();
+					});
+					li.textContent = this.#options[i].value;
+					li.className = "checked";
+					li.appendChild(closeBt);
+					if (this.#options[i].indeterminate)
+						li.classList.add("indeterminate");
+					this.#htmlInputList.insertBefore(li, this.#htmlInput);
+				}
+			}
+		}
+		#rebuildDropdownContent() {
+			this.#htmlDropdown.textContent = "";
+			for (let i in this.#options) {
+				let wrapper = document.createElement("li");
+				let lbl = document.createElement("label");
+				let chk = document.createElement("input");
+				let span = document.createElement("span");
+				span.textContent = this.#options[i].value;
+				chk.type = "checkbox";
+				chk.checked = !!this.#options[i].checked;
+				chk.indeterminate = !!this.#options[i].indeterminate;
+				chk.index = i;
+				lbl.appendChild(chk);
+				lbl.appendChild(span);
+				wrapper.appendChild(lbl);
+				this.#htmlDropdown.appendChild(wrapper);
+				this.#options[i].htmlListItem = wrapper;
+				chk.addEventListener("change", e => {
+					let state = this.#options[e.currentTarget.index];
+					if (!state.checked) {
+						e.currentTarget.checked = true;
+						e.currentTarget.indeterminate = false;
+					} else if (!state.indeterminate) {
+						e.currentTarget.checked = true;
+						e.currentTarget.indeterminate = true;
+					} else {
+						e.currentTarget.checked = false;
+						e.currentTarget.indeterminate = false;
+					}
+					state.checked = e.currentTarget.checked;
+					state.indeterminate = e.currentTarget.indeterminate;
+					e.stopImmediatePropagation();
+					this.#updateTextInputValue();
+					this.#onUpdate();
+				});
+			}
+		}
+		render() {
+			this.#htmlSelectElement.style.display = "none";
+			if (!this.#htmlInput) {
+				this.#htmlInput = document.createElement("input");
+				this.#htmlInput.className = "multiselect-input";
+				this.#htmlInput.type = "text";
+				this.#htmlInput.addEventListener("input", e => {
+					let input = e.currentTarget.value.toLocaleLowerCase();
+					for (let i of this.#options) {
+						if (i.value.toLocaleLowerCase().indexOf(input) >= 0) {
+							i.htmlListItem.classList.remove("hidden");
+						} else {
+							i.htmlListItem.classList.add("hidden");
+						}
+					}
+				});
+				this.#htmlInputList && this.#htmlInputList.appendChild(this.#htmlInput);
+			}
+			if (!this.#htmlInputList) {
+				this.#htmlInputList = document.createElement("ul");
+				this.#htmlInputList.className = "multiselect-inputlist";
+				this.#htmlRootElement && this.#htmlRootElement.appendChild(this.#htmlInputList);
+				this.#htmlInputList.appendChild(this.#htmlInput);
+			}
+			if (!this.#htmlDropdown) {
+				this.#htmlDropdown = document.createElement("ul");
+				this.#htmlDropdown.className = "multiselect-dropdown";
+				this.#htmlDropdown.classList.add("hidden");
+				this.#htmlRootElement && this.#htmlRootElement.appendChild(this.#htmlDropdown);
+			}
+			if (!this.#htmlWrapper) {
+				this.#htmlWrapper = document.createElement("div");
+				this.#htmlWrapper.className = "multiselect-wrapper";
+				this.#htmlWrapper.appendChild(this.#htmlInputList);
+				this.#htmlWrapper.appendChild(this.#htmlDropdown);
+				this.#htmlRootElement.appendChild(this.#htmlWrapper);
+				this.#htmlRootElement.addEventListener("mousedown", e => {
+					this.#ignoreMouseOut = setTimeout(() => { this.#ignoreMouseOut = false; }, 500);
+				});
+				this.#htmlWrapper.addEventListener("focusin", e => {
+					this.#htmlDropdown.classList.remove("hidden");
+				});
+				this.#htmlWrapper.addEventListener("focusout", e => {
+					this.#ignoreMouseOut === false && this.#htmlDropdown.classList.add("hidden");
+				});
+				document.body.addEventListener("click", e => {
+					this.#ignoreMouseOut === false && this.#htmlDropdown.classList.add("hidden");
+				});
+			}
+			this.#rebuildDropdownContent();
+			this.#updateTextInputValue();
+		}
+	}
+
+	_makeMultiSelect = htmlElement => {
+		let select = new MultiSelect(htmlElement);
+		select.render();
+		return select;
+	};
+	if (items instanceof HTMLElement)
+		return _makeMultiSelect(items);
+	else if (items.length)
+		return Array.from(items).map(i => window.makeMultiselect(i));
+});
+

+ 9 - 33
static/public/js/uiFilter.js

@@ -53,6 +53,8 @@ $(() => {
         };
     }
 
+    const EMPTY_STRING = "(Empty)";
+
     window.ReloadFilters = function(mediaManager) {
         let buildFilterBar = (labelText, canBeEmpty, possibleValues) => {
             let result = document.createElement("div");
@@ -67,44 +69,18 @@ $(() => {
             let allPossibleValues = [].concat(canBeEmpty ? [undefined] : [], possibleValues);
             for (let i of allPossibleValues) {
                 let opt = document.createElement("option");
-                opt.textContent = i ? i : "(Empty)";
+                opt.textContent = i ? i : EMPTY_STRING;
                 opt.value = i ?? "";
                 select.appendChild(opt);
             }
-            $(select).change(e => {
-                let val = Array.from(select.children).filter(i => i.selected).map(i => i.value === '' ? undefined : i.value);
-                Array.from(select.parentNode.querySelectorAll(".dropdown-menu li .form-check")).forEach((inputGroup, i) => {
-                    let input = inputGroup.querySelector("input");
-                    input.previousStatus = input.previousStatus || 0;
-                    if ((input.previousStatus == 0 && !input.checked && !input.indeterminate) ||
-                        (input.previousStatus == 1 && input.checked && !input.indeterminate) ||
-                        (input.previousStatus == 2 && input.checked && input.indeterminate))
-                        return;
-                    if (input.previousStatus == 0) {
-                        input.checked = true;
-                        input.indeterminate = false;
-                        input.previousStatus = 1;
-                    }
-                    else if (input.previousStatus == 1) {
-                        input.checked = true;
-                        input.indeterminate = true;
-                        input.previousStatus = 2;
-                    } else {
-                        input.checked = false;
-                        input.indeterminate = false;
-                        input.previousStatus = 0;
-                    }
-                    select.querySelectorAll("option")[i].selected = input.checked ? "selected" : "";
-                    console.log(select.querySelectorAll("option")[i]);
-                    $(select).bsMultiSelect("UpdateAppearance");
-                });
+            let multiselect = makeMultiselect(select);
+            multiselect.addEventListener(e => {
+                let val = e.filter(i => i.checked && !i.indeterminate).map(i => i.value === EMPTY_STRING ? "" : i.value);
                 window.FilterManager.setFilterValue(labelText, val);
+                val = e.filter(i => i.checked && i.indeterminate).map(i => i.value === EMPTY_STRING ? "" : i.value);
+                window.FilterManager.setExclusionFilter(labelText, val);
+                window.FilterManager.updateFilter();
             });
-            $(select).bsMultiSelect({cssPatch: {
-                picks: { backgroundColor: 'inherit' },
-                pick: { color: 'var(--bs-body-color)' },
-                choiceContent: { color: 'var(--bs-body-color)' }
-            }});
             return {
                 content: result
             };

+ 1 - 1
templates/_footer.js

@@ -1,10 +1,10 @@
 
 module.exports = `
 <script src="/public/js/jquery-3.6.1.min.js"></script>
+<script src="/public/js/multiSelect.js"></script>
 <script src="/public/bootstrap/bootstrap.bundle.min.js"></script>
 <script src="/public/bootstrap/bootstrap-slider.min.js"></script>
 <script src="/public/js/popper.min.js"></script>
-<script src="/public/js/BsMultiSelect.min.js"></script>
 <script src="/public/leaflet/leaflet-src.js"></script>
 <script src="/public/js/Control.Geocoder.js"></script>
 

+ 0 - 1
templates/_header.js

@@ -9,7 +9,6 @@ module.exports = `
 <link type="text/css" rel="stylesheet" href="/public/bootstrap/bootstrap-icons.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/bootstrap/bootstrap-slider.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/leaflet/leaflet.css"/>
-<link type="text/css" rel="stylesheet" href="/public/css/BsMultiSelect.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/css/Control.Geocoder.css" />
 <link type="text/css" rel="stylesheet" href="/public/css/style.css"/>
 <meta name="viewport" content="width=device-width, initial-scale=1">