|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function makeDraggable(element) { |
|
let dragging = false; |
|
let offset = {x: 0, y: 0}; |
|
|
|
const margin = 10; |
|
|
|
|
|
const fixPos = () => { |
|
const dbb = element.getBoundingClientRect(); |
|
if (dbb.left < margin) element.style.left = margin + "px"; |
|
else if (dbb.right > window.innerWidth - margin) |
|
element.style.left = |
|
dbb.left + (window.innerWidth - margin - dbb.right) + "px"; |
|
|
|
if (dbb.top < margin) element.style.top = margin + "px"; |
|
else if (dbb.bottom > window.innerHeight - margin) |
|
element.style.top = |
|
dbb.top + (window.innerHeight - margin - dbb.bottom) + "px"; |
|
}; |
|
|
|
|
|
mouse.listen.window.btn.left.onpaintstart.on((evn) => { |
|
if ( |
|
element.contains(evn.target) && |
|
evn.target.classList.contains("draggable") |
|
) { |
|
const bb = element.getBoundingClientRect(); |
|
offset.x = evn.x - bb.x; |
|
offset.y = evn.y - bb.y; |
|
dragging = true; |
|
} |
|
}); |
|
|
|
|
|
mouse.listen.window.btn.left.onpaint.on((evn) => { |
|
if (dragging) { |
|
element.style.right = null; |
|
element.style.bottom = null; |
|
element.style.top = evn.y - offset.y + "px"; |
|
element.style.left = evn.x - offset.x + "px"; |
|
|
|
fixPos(); |
|
} |
|
}); |
|
|
|
|
|
mouse.listen.window.btn.left.onpaintend.on((evn) => { |
|
dragging = false; |
|
}); |
|
|
|
|
|
window.addEventListener("resize", () => { |
|
fixPos(); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createSlider(name, wrapper, options = {}) { |
|
defaultOpt(options, { |
|
valuecb: null, |
|
min: 0, |
|
max: 1, |
|
step: 0.1, |
|
defaultValue: 0.7, |
|
textStep: null, |
|
}); |
|
|
|
let value = options.defaultValue; |
|
|
|
|
|
const phantomRange = document.createElement("input"); |
|
phantomRange.type = "range"; |
|
phantomRange.min = options.min; |
|
phantomRange.max = options.max; |
|
phantomRange.step = options.step; |
|
|
|
let phantomTextRange = phantomRange; |
|
if (options.textStep) { |
|
phantomTextRange = document.createElement("input"); |
|
phantomTextRange.type = "range"; |
|
phantomTextRange.min = options.min; |
|
phantomTextRange.max = options.max; |
|
phantomTextRange.step = options.textStep; |
|
} |
|
|
|
|
|
const underEl = document.createElement("div"); |
|
underEl.classList.add("under"); |
|
const textEl = document.createElement("input"); |
|
textEl.type = "text"; |
|
textEl.classList.add("text"); |
|
|
|
const overEl = document.createElement("div"); |
|
overEl.classList.add("over"); |
|
|
|
wrapper.classList.add("slider-wrapper"); |
|
wrapper.appendChild(underEl); |
|
wrapper.appendChild(textEl); |
|
wrapper.appendChild(overEl); |
|
|
|
const bar = document.createElement("div"); |
|
bar.classList.add("slider-bar"); |
|
underEl.appendChild(bar); |
|
underEl.appendChild(document.createElement("div")); |
|
|
|
|
|
|
|
const onchange = new Observer(); |
|
|
|
|
|
const setValue = (val) => { |
|
phantomTextRange.value = val; |
|
value = parseFloat(phantomTextRange.value); |
|
bar.style.width = `${ |
|
100 * ((value - options.min) / (options.max - options.min)) |
|
}%`; |
|
textEl.value = `${name}: ${value}`; |
|
options.valuecb && options.valuecb(value); |
|
onchange.emit({value: val}); |
|
}; |
|
|
|
setValue(options.defaultValue); |
|
|
|
|
|
textEl.addEventListener("blur", () => { |
|
overEl.style.pointerEvents = "auto"; |
|
textEl.value = `${name}: ${value}`; |
|
}); |
|
textEl.addEventListener("focus", () => { |
|
overEl.style.pointerEvents = "none"; |
|
textEl.value = value; |
|
}); |
|
|
|
textEl.addEventListener("change", () => { |
|
try { |
|
if (Number.isNaN(parseFloat(textEl.value))) setValue(value); |
|
else setValue(parseFloat(textEl.value)); |
|
} catch (e) {} |
|
}); |
|
|
|
keyboard.listen.onkeyclick.on((evn) => { |
|
if (evn.target === textEl && evn.code === "Enter") { |
|
textEl.blur(); |
|
} |
|
}); |
|
|
|
mouse.listen.window.btn.left.onclick.on((evn) => { |
|
if (evn.target === overEl) { |
|
textEl.select(); |
|
} |
|
}); |
|
|
|
mouse.listen.window.btn.left.ondrag.on((evn) => { |
|
if (evn.initialTarget === overEl) { |
|
const newv = Math.max( |
|
options.min, |
|
Math.min( |
|
options.max, |
|
((evn.evn.clientX - evn.initialTarget.getBoundingClientRect().left) / |
|
wrapper.offsetWidth) * |
|
(options.max - options.min) + |
|
options.min |
|
) |
|
); |
|
phantomRange.value = newv; |
|
setValue(parseFloat(phantomRange.value)); |
|
} |
|
}); |
|
|
|
return { |
|
onchange, |
|
set value(val) { |
|
setValue(val); |
|
}, |
|
get value() { |
|
return value; |
|
}, |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createAutoComplete( |
|
name, |
|
wrapper, |
|
options = {}, |
|
extraEl = null, |
|
extraClass = null |
|
) { |
|
defaultOpt(options, { |
|
multiple: false, |
|
options: [], |
|
}); |
|
|
|
wrapper.classList.add("autocomplete"); |
|
|
|
const inputEl = document.createElement("input"); |
|
inputEl.type = "text"; |
|
inputEl.classList.add("autocomplete-text"); |
|
if (extraClass != null) { |
|
inputEl.classList.add(extraClass); |
|
} |
|
|
|
const autocompleteEl = document.createElement("div"); |
|
autocompleteEl.classList.add("autocomplete-list", "display-none"); |
|
|
|
let timeout = null; |
|
let ontext = false; |
|
let onlist = false; |
|
|
|
wrapper.appendChild(inputEl); |
|
wrapper.appendChild(autocompleteEl); |
|
if (extraEl != null) { |
|
wrapper.appendChild(extraEl); |
|
} |
|
|
|
const acobj = { |
|
name, |
|
wrapper, |
|
_selectedOptions: new Set(), |
|
_options: [], |
|
|
|
|
|
onchange: new Observer(), |
|
|
|
get value() { |
|
const v = Array.from(this._selectedOptions).map((opt) => opt.value); |
|
return options.multiple ? v : v[0]; |
|
}, |
|
set value(values) { |
|
this._selectedOptions.clear(); |
|
|
|
for (const val of options.multiple ? values : [values]) { |
|
const opt = this.options.find((option) => option.value === val); |
|
|
|
if (!opt) continue; |
|
|
|
this._selectedOptions.add(opt); |
|
} |
|
|
|
this._sync(); |
|
}, |
|
|
|
_sync() { |
|
const val = Array.from(this._selectedOptions).map((opt) => opt.value); |
|
const name = Array.from(this._selectedOptions).map((opt) => opt.name); |
|
|
|
for (const opt of this._options) { |
|
if (acobj._selectedOptions.has(opt)) |
|
opt.optionElement.classList.add("selected"); |
|
else opt.optionElement.classList.remove("selected"); |
|
} |
|
|
|
updateInputField(); |
|
|
|
this.onchange.emit({ |
|
name: options.multiple ? name : name[0], |
|
value: options.multiple ? val : val[0], |
|
}); |
|
}, |
|
|
|
get options() { |
|
return this._options; |
|
}, |
|
set options(val) { |
|
this._options = []; |
|
|
|
while (autocompleteEl.lastChild) { |
|
autocompleteEl.removeChild(autocompleteEl.lastChild); |
|
} |
|
|
|
|
|
val.forEach((opt) => { |
|
const {name, value, title} = opt; |
|
|
|
const optionEl = document.createElement("option"); |
|
optionEl.classList.add("autocomplete-option"); |
|
optionEl.title = title || name; |
|
if (opt.optionelcb) opt.optionelcb(optionEl); |
|
|
|
const option = {name, value, optionElement: optionEl}; |
|
|
|
this._options.push(option); |
|
|
|
optionEl.addEventListener("click", () => select(option)); |
|
|
|
autocompleteEl.appendChild(optionEl); |
|
}); |
|
|
|
updateOptions(""); |
|
}, |
|
}; |
|
|
|
function updateInputField() { |
|
inputEl.value = Array.from(acobj._selectedOptions) |
|
.map((o) => o.name) |
|
.join(", "); |
|
inputEl.title = Array.from(acobj._selectedOptions) |
|
.map((o) => o.name) |
|
.join(", "); |
|
} |
|
|
|
function updateOptions(value = null) { |
|
const text = value ?? inputEl.value.toLowerCase().trim(); |
|
|
|
acobj._options.forEach((opt) => { |
|
const textLocation = opt.name.toLowerCase().indexOf(text); |
|
|
|
while (opt.optionElement.lastChild) { |
|
opt.optionElement.removeChild(opt.optionElement.lastChild); |
|
} |
|
|
|
opt.optionElement.append( |
|
document.createTextNode(opt.name.substring(0, textLocation)) |
|
); |
|
const span = document.createElement("span"); |
|
span.style.fontWeight = "bold"; |
|
span.textContent = opt.name.substring( |
|
textLocation, |
|
textLocation + text.length |
|
); |
|
opt.optionElement.appendChild(span); |
|
opt.optionElement.appendChild( |
|
document.createTextNode( |
|
opt.name.substring(textLocation + text.length, opt.name.length) |
|
) |
|
); |
|
|
|
if (textLocation !== -1) { |
|
opt.optionElement.classList.remove("display-none"); |
|
} else opt.optionElement.classList.add("display-none"); |
|
}); |
|
} |
|
|
|
function select(opt) { |
|
ontext = false; |
|
if (!options.multiple) { |
|
onlist = false; |
|
acobj._selectedOptions.clear(); |
|
autocompleteEl.classList.add("display-none"); |
|
for (const child of autocompleteEl.children) { |
|
child.classList.remove("selected"); |
|
} |
|
} |
|
|
|
if (options.multiple && acobj._selectedOptions.has(opt)) { |
|
acobj._selectedOptions.delete(opt); |
|
opt.optionElement.classList.remove("selected"); |
|
} else { |
|
acobj._selectedOptions.add(opt); |
|
opt.optionElement.classList.add("selected"); |
|
} |
|
|
|
acobj._sync(); |
|
} |
|
|
|
inputEl.addEventListener("focus", () => { |
|
ontext = true; |
|
|
|
autocompleteEl.classList.remove("display-none"); |
|
inputEl.select(); |
|
}); |
|
inputEl.addEventListener("blur", () => { |
|
ontext = false; |
|
|
|
if (!onlist && !ontext) { |
|
updateInputField(); |
|
|
|
autocompleteEl.classList.add("display-none"); |
|
} |
|
}); |
|
|
|
autocompleteEl.addEventListener("mouseenter", () => { |
|
onlist = true; |
|
}); |
|
|
|
autocompleteEl.addEventListener("mouseleave", () => { |
|
onlist = false; |
|
|
|
if (!onlist && !ontext) { |
|
updateInputField(); |
|
|
|
autocompleteEl.classList.add("display-none"); |
|
} |
|
}); |
|
|
|
|
|
inputEl.addEventListener("input", () => { |
|
updateOptions(); |
|
}); |
|
|
|
acobj.options = options.options; |
|
|
|
return acobj; |
|
} |
|
|