Last active
September 11, 2022 09:01
-
-
Save janwirth/b65903d19caf919698ef72a79b521ad8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* common */ | |
.funk-dropdown-contents { | |
z-index: 1000; | |
transition: all 0.05s ease-in-out; | |
} | |
/* .funk-dropdown-contents[style] { */ | |
/* transition: .05s all ease-in-out; */ | |
/* } */ | |
funk-dropdown:not(.open) .funk-dropdown-contents { | |
pointer-events: none !important; | |
opacity: 0; | |
transform: translateX(4px); | |
transition-duration: 0.05s; | |
} | |
funk-dropdown:not(.open) .funk-dropdown-contents * { | |
pointer-events: none; | |
} | |
/* base */ | |
funk-dropdown { | |
user-select: none; | |
z-index: 200; | |
} | |
funk-dropdown.open .funk-dropdown-contents { | |
opacity: 1; | |
transition-delay: 0.05s; | |
pointer-events: all !important; | |
z-index: 1000; | |
} | |
funk-dropdown.open { | |
z-index: 1000; | |
} | |
funk-dropdown.fixed-z-index.fixed-z-index:not(.open) { | |
z-index: 0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "./Dropdown.css"; | |
import { usePopout } from "./floating"; | |
import { autoUpdate } from "@floating-ui/dom"; | |
import { runOnIntersect } from "./runOnIntersect.js"; | |
/** | |
* Dropdown module | |
* @module customElements/Dropdown | |
* @description | |
* Given button and content will produce a dropdown. | |
* @example | |
* <funk-dropdown> | |
* <div class="funk-dropdown-button">Click me</div> | |
* <div class="funk-dropdown-contents">Some options...</div> | |
* </funk-dropdown> | |
*/ | |
class Dropdown extends HTMLElement { | |
connectedCallback() { | |
runOnIntersect(this, () => this.init()); | |
} | |
// set up events | |
init() { | |
// autoUpdate(document.body, this, x => {console.log('EV', x) | |
// }) | |
usePopout(this, "TimezonePicker", (cleanupPopout) => { | |
cleanupPopout(); | |
}); | |
this.classList.add("closed"); | |
this.handleButtonClick = async (ev) => { | |
// closed | |
if (this.classList.contains("open")) { | |
this.classList.remove("open"); | |
this.classList.add("closed"); | |
this.cleanupPopout && this.cleanupPopout(); | |
// opened | |
} else { | |
this.classList.add("open"); | |
await usePopout(this, "TimezonePicker", (cleanupPopout) => { | |
this.cleanupPopout = cleanupPopout; | |
}); | |
this.classList.remove("closed"); | |
} | |
}; | |
this.handleClickOutside = (ev) => { | |
this.classList.remove("open"); | |
this.classList.add("closed"); | |
this.cleanupPopout && this.cleanupPopout(); | |
}; | |
// when an item in the content was selected, close the dropdown | |
// exception: if the clicked item is 'insensitive', do not close the dropdown | |
this.handleContentClick = (ev) => { | |
// recursive function to detect if it's an insensitive row | |
const isInsensitiveRow = (el) => { | |
const isInsensitive = el.classList.contains("funk-dropdown-keep-open"); | |
const isContainer = el.classList.contains("funk-dropdown-contents"); | |
if (isInsensitive) { | |
return true; | |
} else if (isContainer) { | |
// we have checked everywhere but could not find a class that tells that we are in an insensitive row | |
return false; | |
} else { | |
// when we are not at the container yet but have not found the class we go up by one | |
return isInsensitiveRow(el.parentElement); | |
} | |
}; | |
// only close if the row that was clicked is not sensitive to closing on click | |
if (isInsensitiveRow(ev.target)) { | |
return; | |
} else { | |
this.classList.remove("open"); | |
this.classList.add("closed"); | |
} | |
}; | |
this.button = this.querySelector(".funk-dropdown-button"); | |
if (!this.button) { | |
throw new Error(".funk-dropdown-button not found"); | |
} | |
this.contents = this.querySelector(".funk-dropdown-contents"); | |
if (!this.contents) { | |
throw new Error(".funk-dropdown-contents not found"); | |
} | |
this.button = this.querySelector(".funk-dropdown-button"); | |
this.contents = this.querySelector(".funk-dropdown-contents"); | |
this.button.addEventListener("click", this.handleButtonClick); | |
this.addEventListener("clickoutside", this.handleClickOutside); | |
this.contents.addEventListener("click", this.handleContentClick); | |
} | |
disconnectedCallback() { | |
try { | |
this.button.removeEventListener("click", this.handleButtonClick); | |
this.removeEventListener("clickoutside", this.handleClickOutside); | |
this.contents.removeEventListener("click", this.handleContentClick); | |
this.cleanupPopout && this.cleanupPopout(); | |
} catch (e) {} | |
} | |
} | |
customElements.define("funk-dropdown", Dropdown); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
computePosition, | |
offset, | |
shift, | |
arrow, | |
autoUpdate, | |
} from "@floating-ui/dom"; | |
export const usePopout = (referenceEl, name, callback, count = 0) => { | |
// make sure element is mounted | |
if (count > 3) { | |
return; | |
} | |
try { | |
const el = referenceEl.querySelector(".funk-dropdown-contents"); | |
// apply floating UI to break out of overflow: scroll | |
const reference = | |
el.parentElement.querySelector(":scope > .funk-dropdown-button") || | |
el.previousSibling; | |
if (el && reference) { | |
bind({ reference, el, callback })(); | |
} else { | |
throw new Error("dropdown contents not found"); | |
} | |
} catch (e) { | |
// in case the element did not appear yet | |
requestAnimationFrame(() => | |
usePopout(referenceEl, name, callback, count + 1) | |
); | |
} | |
}; | |
export const bind = (args) => () => { | |
const { reference, el, callback, arrowEl } = args; | |
const placement = `bottom-${args.placement || "start"}`; | |
var middleware = [offset(8), shift({ padding: 8 })]; | |
// if (arrowEl) { | |
// middleware.push(arrow({element: arrowEl, strategy: "fixed"})) | |
// } | |
const update = async () => { | |
computePosition(reference, el, { | |
placement, | |
middleware, | |
strategy: "fixed", | |
}).then(({ x, y, middlewareData }) => { | |
// disable transition to prevent el from zooming across screen | |
el.style.transition = "none"; | |
Object.assign(el.style, { | |
left: `${x}px`, | |
top: `${y}px`, | |
}); | |
// if (arrowEl) { | |
// Object.assign(arrowEl.style, { | |
// left: middlewareData.arrow.x != null ? `${middlewareData.arrow.x}px` : '', | |
// top: middlewareData.arrow.y != null ? `${middlewareData.arrow.y}px` : '', | |
// }); | |
// } | |
// re-enable transition | |
setTimeout(() => { | |
el.style.transition = null; | |
}, 10); | |
}); | |
}; | |
update(); | |
setTimeout(() => { | |
callback( | |
autoUpdate(reference, el, update, { | |
ancestorResize: false, | |
elementResize: false, | |
}) | |
); | |
}, 10); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const options = { | |
rootMargin: "50px", | |
threshold: 1.0, | |
}; | |
const q = []; | |
var toRun = null; | |
const advanceQ = () => { | |
if (!toRun) { | |
toRun = q.pop(); | |
if (toRun) { | |
requestAnimationFrame(() => { | |
toRun(); | |
toRun = null; | |
advanceQ(); | |
}); | |
} | |
} | |
}; | |
const io = new IntersectionObserver((entries) => { | |
entries.forEach((entry) => { | |
if (entry.intersectionRatio > 0) { | |
io.unobserve(entry.target); | |
q.push(() => { | |
entry.target.__init__(); | |
entry.target.style.background = null; | |
}); | |
advanceQ(); | |
} | |
}); | |
}, options); | |
export const runOnIntersect = (el, fn) => { | |
io.observe(el); | |
el.__init__ = fn; | |
// el.style.background = "red"; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
classicDropdown label contents_ = | |
let | |
dropdownButton = | |
Html.Styled.button | |
[ Html.Styled.Attributes.class "funk-dropdown-button" | |
, Html.Styled.Attributes.css | |
[ Css.fontSize (Css.px smallFont) | |
, Css.padding (gridBaseSize |> Css.px) | |
, Css.backgroundColor Css.transparent | |
, Css.borderWidth (Css.px 0) | |
, Css.cursor Css.pointer | |
, Css.borderRadius (Css.px 2) | |
, Css.displayFlex | |
, Css.hover | |
[ Css.backgroundColor <| Css.transparent | |
] | |
] | |
] | |
[ Html.Styled.div | |
[ Html.Styled.Attributes.css [ Css.alignSelf Css.center ] | |
] | |
[ Html.Styled.text label | |
] | |
, [ Html.Styled.fromUnstyled Ui.BoxIcons.bxCaretDown ] | |
|> Html.Styled.div | |
[ Html.Styled.Attributes.class "contain-svg" | |
, Html.Styled.Attributes.css | |
[ Css.width (Css.px (gridBaseSize * 2)) | |
, Css.height (Css.px (gridBaseSize * 2)) | |
, Css.marginLeft (Css.px gridBaseSize) | |
, Css.transform (Css.translateY (Css.px 1)) | |
] | |
] | |
] | |
in | |
Html.Styled.node "funk-dropdown" | |
[ Html.Styled.Attributes.class "funk-dropdown-purple-active" | |
] | |
[ dropdownButton | |
, contents_ | |
] | |
dropdownContents = | |
dropdownContents_ Css.left [] | |
dropdownContents_ align extraStyles items = | |
Html.Styled.div | |
[ Html.Styled.Attributes.css | |
[ Css.position Css.fixed | |
, Css.bottom <| Css.px -gridBaseSize | |
, Css.height <| Css.px 0 | |
, align <| Css.px 0 | |
, Css.backgroundColor lightGreyCss | |
, Css.fontWeight Css.normal | |
, Css.zIndex (Css.int 100) | |
, Css.fontSize (Css.px 14) | |
-- , Css.minWidth (gridBaseSize * 30 |> Css.px) | |
] | |
, Html.Styled.Attributes.class "funk-dropdown-contents" | |
] | |
[ Html.Styled.div | |
[ Html.Styled.Attributes.css | |
([ Css.borderColor primaryCss | |
, Css.borderWidth (Css.px 1) | |
, Css.borderRadius (gridBaseSize / 2 |> Css.px) | |
, Css.borderStyle Css.solid | |
, Css.overflow Css.hidden | |
, shadow 2 | |
, Css.maxWidth (Css.px (gridBaseSize * 45)) | |
, Css.property "width" "min-content" | |
, Css.maxHeight (Css.px 250) | |
, Css.overflowY Css.auto | |
] | |
++ extraStyles | |
) | |
, Html.Styled.Attributes.class "scroll-hint" | |
] | |
items | |
] | |
dropdownItemStyles_ { canSelect, active, flex } = | |
let | |
canSelectStyles = | |
if canSelect then | |
[ Css.hover | |
[ Css.backgroundColor primaryCss | |
, Css.color whiteCss | |
, Css.cursor Css.pointer | |
] | |
] | |
else | |
[ Css.cursor Css.default, Css.color darkGreyCss ] | |
in | |
Html.Styled.Attributes.css <| | |
[ Css.padding (Css.px (gridBaseSize * 1.5)) | |
, Css.textDecoration Css.none | |
, Css.color primaryCss | |
, if flex then | |
Css.displayFlex | |
else | |
Css.display Css.block | |
, Css.width (Css.pct 100) | |
, Css.boxSizing Css.borderBox | |
, Css.textAlign Css.left | |
, Css.alignItems Css.center | |
] | |
++ canSelectStyles | |
++ (if active then | |
[ Css.backgroundColor lightBlueCss | |
, Css.color blueCss | |
] | |
else | |
[] | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment