Solving Vuetify "v-menu" appears as fixed if parent component is inside a scrolling container - scroll

There is a long standing issue with the v-menu component in Vuetify:
by default the popup is physically "detached" from the activator and created as a child of v-app, thus avoiding being clipped if some of the parent DOM nodes has overflow: hidden style; however, this leads to the issue that the popup behaves as "position: fixed" when the activator is inside a scrolling container - that is, it does not scroll with the activator and looks visually disconnected, just "hanging" over the page.
the Vuetify maintainers admit the fact and suggest using the "attach" prop - however, 9 times out of 10 when using "attach" the position of the popup is computed wrong.
After 2 hours of debugging I finally gave up on using the "attach" prop and decided to simply track the scrolling position of the parent container where the activator resides and take it into account when computing the position of the popup. I am sharing my solution to the issue below and hoping that it will be included in the mainstream Vuetify.

Here is the patch file which solves the above issue plus a few others. Create a folder named patches inside your project and save the patch file as patches/vuetify#2.6.4.patch. Then add a new script to the scripts group in your package.json
"scripts":
{
....
"prepare": "custompatch"
}
Then run npm -i -D custompatch (to install the patcher for your CI/CD) and npx custompatch (to actually patch Vuetify in your dev environment).
Index: \vuetify\lib\components\VDialog\VDialog.js
===================================================================
--- \vuetify\lib\components\VDialog\VDialog.js
+++ \vuetify\lib\components\VDialog\VDialog.js
## -43,17 +43,21 ##
transition: {
type: [String, Boolean],
default: 'dialog-transition'
},
- width: [String, Number]
+ width: [String, Number],
+ zIndex: {
+ type: [String, Number],
+ default: 200
+ }
},
data() {
return {
activatedBy: null,
animate: false,
animateTimeout: -1,
- stackMinZIndex: 200,
+ stackMinZIndex: this.zIndex || 200,
previousActiveElement: null
};
},
Index: \vuetify\lib\components\VMenu\VMenu.js
===================================================================
--- \vuetify\lib\components\VMenu\VMenu.js
+++ \vuetify\lib\components\VMenu\VMenu.js
## -120,10 +120,10 ##
return {
maxHeight: this.calculatedMaxHeight,
minWidth: this.calculatedMinWidth,
maxWidth: this.calculatedMaxWidth,
- top: this.calculatedTop,
- left: this.calculatedLeft,
+ top: `calc(${this.calculatedTop} - ${this.scrollY}px + ${this.originalScrollY}px)`,
+ left: `calc(${this.calculatedLeft} - ${this.scrollX}px + ${this.originalScrollX}px)`,
transformOrigin: this.origin,
zIndex: this.zIndex || this.activeZIndex
};
}
Index: \vuetify\lib\components\VSelect\VSelect.js
===================================================================
--- \vuetify\lib\components\VSelect\VSelect.js
+++ \vuetify\lib\components\VSelect\VSelect.js
## -678,12 +678,17 ##
},
onScroll() {
if (!this.isMenuActive) {
- requestAnimationFrame(() => this.getContent().scrollTop = 0);
+ requestAnimationFrame(() =>
+ {
+ const content = this.getContent();
+ if (content) content.scrollTop = 0;
+ });
} else {
if (this.lastItem > this.computedItems.length) return;
- const showMoreItems = this.getContent().scrollHeight - (this.getContent().scrollTop + this.getContent().clientHeight) < 200;
+ const content = this.getContent();
+ const showMoreItems = content ? this.getContent().scrollHeight - (this.getContent().scrollTop + this.getContent().clientHeight) < 200 : false;
if (showMoreItems) {
this.lastItem += 20;
}
Index: \vuetify\lib\components\VSlideGroup\VSlideGroup.js
===================================================================
--- \vuetify\lib\components\VSlideGroup\VSlideGroup.js
+++ \vuetify\lib\components\VSlideGroup\VSlideGroup.js
## -181,9 +181,9 ##
},
methods: {
onScroll() {
- this.$refs.wrapper.scrollLeft = 0;
+ if (this.$refs.wrapper) this.$refs.wrapper.scrollLeft = 0; // TMCDOS
},
onFocusin(e) {
if (!this.isOverflowing) return; // Focused element is likely to be the root of an item, so a
Index: \vuetify\lib\components\VTextField\VTextField.js
===================================================================
--- \vuetify\lib\components\VTextField\VTextField.js
+++ \vuetify\lib\components\VTextField\VTextField.js
## -441,8 +441,9 ##
this.$refs.input.focus();
},
onFocus(e) {
+ this.onResize();
if (!this.$refs.input) return;
const root = attachedRoot(this.$el);
if (!root) return;
Index: \vuetify\lib\directives\click-outside\index.js
===================================================================
--- \vuetify\lib\directives\click-outside\index.js
+++ \vuetify\lib\directives\click-outside\index.js
## -35,10 +35,11 ##
}
function directive(e, el, binding, vnode) {
const handler = typeof binding.value === 'function' ? binding.value : binding.value.handler;
+ const target = e.target;
el._clickOutside.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
- checkIsActive(e, binding) && handler && handler(e);
+ checkIsActive({...e, target}, binding) && handler && handler({...e, target});
}, 0);
}
function handleShadow(el, callback) {
Index: \vuetify\lib\directives\ripple\index.js
===================================================================
--- \vuetify\lib\directives\ripple\index.js
+++ \vuetify\lib\directives\ripple\index.js
## -119,9 +119,9 ##
el.style.position = el.dataset.previousPosition;
delete el.dataset.previousPosition;
}
- animation.parentNode && el.removeChild(animation.parentNode);
+ animation.parentNode && /* el */animation.parentNode.parentNode.removeChild(animation.parentNode);
}, 300);
}, delay);
}
Index: \vuetify\lib\mixins\detachable\index.js
===================================================================
--- \vuetify\lib\mixins\detachable\index.js
+++ \vuetify\lib\mixins\detachable\index.js
## -28,13 +28,23 ##
},
contentClass: {
type: String,
default: ''
+ },
+ scroller:
+ {
+ default: null,
+ validator: validateAttachTarget
}
},
data: () => ({
activatorNode: null,
- hasDetached: false
+ hasDetached: false,
+ scrollingNode: null,
+ scrollX: 0,
+ scrollY: 0,
+ originalScrollX: 0,
+ originalScrollY: 0
}),
watch: {
attach() {
this.hasDetached = false;
## -42,10 +52,38 ##
},
hasContent() {
this.$nextTick(this.initDetach);
+ },
+ isActive(val)
+ {
+ if (val)
+ {
+ if (typeof this.scroller === 'string') {
+ // CSS selector
+ this.scrollingNode = document.querySelector(this.scroller);
+ } else if (this.scroller && typeof this.scroller === 'object') {
+ // DOM Element
+ this.scrollingNode = this.scroller;
+ }
+ if (this.scrollingNode)
+ {
+ this.originalScrollX = this.scrollingNode.scrollLeft;
+ this.originalScrollY = this.scrollingNode.scrollTop;
+ this.scrollX = this.originalScrollX;
+ this.scrollY = this.originalScrollY;
+ this.scrollingNode.addEventListener('scroll', this.setScrollOffset, {passive: true});
+ }
+ }
+ else
+ {
+ if (this.scrollingNode)
+ {
+ this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+ }
+ this.scrollingNode = null;
+ }
}
-
},
beforeMount() {
this.$nextTick(() => {
## -95,8 +133,12 ##
} else {
removeActivator(activator);
}
}
+ if (this.scrollingNode)
+ {
+ this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+ }
},
methods: {
getScopeIdAttrs() {
## -132,9 +174,13 ##
}
target.appendChild(this.$refs.content);
this.hasDetached = true;
+ },
+ setScrollOffset(event)
+ {
+ this.scrollX = event.target.scrollLeft;
+ this.scrollY = event.target.scrollTop;
}
-
}
});
//# sourceMappingURL=index.js.map
\ No newline at end of file
Index: \vuetify\lib\mixins\menuable\index.js
===================================================================
--- \vuetify\lib\mixins\menuable\index.js
+++ \vuetify\lib\mixins\menuable\index.js
## -96,9 +96,9 ##
computed: {
computedLeft() {
const a = this.dimensions.activator;
const c = this.dimensions.content;
- const activatorLeft = (this.attach !== false ? a.offsetLeft : a.left) || 0;
+ const activatorLeft = (this.attach !== false ? this.getActivatorLeft() : a.left) || 0;
const minWidth = Math.max(a.width, c.width);
let left = 0;
left += activatorLeft;
if (this.left || this.$vuetify.rtl && !this.right) left -= minWidth - a.width;
## -117,9 +117,9 ##
const a = this.dimensions.activator;
const c = this.dimensions.content;
let top = 0;
if (this.top) top += a.height - c.height;
- if (this.attach !== false) top += a.offsetTop;else top += a.top + this.pageYOffset;
+ if (this.attach !== false) top += this.getActivatorTop(); else top += a.top + this.pageYOffset;
if (this.offsetY) top += this.top ? -a.height : a.height;
if (this.nudgeTop) top -= parseInt(this.nudgeTop);
if (this.nudgeBottom) top += parseInt(this.nudgeBottom);
return top;
## -130,10 +130,13 ##
},
absoluteYOffset() {
return this.pageYOffset - this.relativeYOffset;
+ },
+
+ windowContainer() {
+ return typeof this.attach === 'string' ? document.querySelector(this.attach) || document.body : typeof this.attach === 'object' ? this.attach : document.body;
}
-
},
watch: {
disabled(val) {
val && this.callDeactivate();
## -274,19 +277,19 ##
},
getInnerHeight() {
if (!this.hasWindow) return 0;
- return window.innerHeight || document.documentElement.clientHeight;
+ return this.attach !== false ? this.windowContainer.clientHeight : window.innerHeight || document.documentElement.clientHeight;
},
getOffsetLeft() {
if (!this.hasWindow) return 0;
- return window.pageXOffset || document.documentElement.scrollLeft;
+ return this.attach !== false ? this.windowContainer.scrollLeft : window.pageXOffset || document.documentElement.scrollLeft;
},
getOffsetTop() {
if (!this.hasWindow) return 0;
- return window.pageYOffset || document.documentElement.scrollTop;
+ return this.attach !== false ? this.windowContainer.scrollTop : window.pageYOffset || document.documentElement.scrollTop;
},
getRoundedBoundedClientRect(el) {
const rect = el.getBoundingClientRect();
## -368,19 +371,38 ##
this.sneakPeek(() => {
if (this.$refs.content) {
if (this.$refs.content.offsetParent) {
const offsetRect = this.getRoundedBoundedClientRect(this.$refs.content.offsetParent);
- this.relativeYOffset = window.pageYOffset + offsetRect.top;
+ this.relativeYOffset = (this.attach !== false ? this.windowContainer.scrollTop : window.pageYOffset) + offsetRect.top;
dimensions.activator.top -= this.relativeYOffset;
- dimensions.activator.left -= window.pageXOffset + offsetRect.left;
+ dimensions.activator.left -= (this.attach !== false ? this.windowContainer.scrollLeft : window.pageXOffset) + offsetRect.left;
}
dimensions.content = this.measure(this.$refs.content);
}
this.dimensions = dimensions;
});
+ },
+
+ getActivatorTop() {
+ let result = 0;
+ let elem = this.getActivator();
+ while (elem && elem !== this.windowContainer && this.windowContainer.contains(elem)) {
+ result += elem.offsetTop;
+ elem = elem.offsetParent;
+ }
+ return result;
+ },
+
+ getActivatorLeft() {
+ let result = 0;
+ let elem = this.getActivator();
+ while (elem && elem !== this.windowContainer && this.windowContainer.contains(elem)) {
+ result += elem.offsetLeft;
+ elem = elem.offsetParent;
+ }
+ return result;
}
-
}
});
//# sourceMappingURL=index.js.map
\ No newline at end of file

Related

Captcha comes out at any time in a test

I am doing a test in which a capchat can come out at any time but I cannot know the exact moment. I just need to grab it and run my script at that point *
cy.get('.captcha-modal', { timeout: 60000 }).then(($loading) => {
expect($loading).length > 0
cy.get('.captcha-modal', { timeout: 60000 }).click({ force: true })
cy.get('.captcha-modal__content .captcha-modal__question').invoke('text').then((text) => {
let textop = text
let finaltx = textop.trim()
let finaladd = 0
let newtext = finaltx.split(" ")
if (newtext[1] == '+') {
finaladd = parseInt(newtext[0]) + parseInt(newtext[2].trim())
// cy.log(finaladd + " plus")
} else if (newtext[1] == '-') {
finaladd = parseInt(newtext[0]) - parseInt(newtext[2].trim())
// cy.log(finaladd + " minus")
}
cy.get('[name="captchaVal"]').first().type(finaladd)
cy.get("[type='Submit']").click()
})
})

maps.google API max 256 visible markers

Breaks my head everytime I think about a solution:
I can process (placing) more than 256 markers using google.maps.Map(), each with an eventListener (InfoWindow), but I can only make max 256 markers visible.I toggle markers with a few buttons.If I go over 256, the markers will be visible but not clickable anymore (breaks the functionality) and a reload of the page is neccessary.
var arrayLength = points.length;
for (var i = 0; i < arrayLength; i++) {
var marker = addMarker(points[i], map);
marker.setMap(map);
}
function addMarker(point, map) {
var infowindow = new google.maps.InfoWindow();
var iconColor = (point.type == 'circle') ? "/images/circle.png" : "/images/pointer_" + point.clr.replace("#","") + ".png";
var size = (point.type == 'circle') ? '20' : '10';
var goldStar = {
url: iconColor,
scaledSize: new google.maps.Size(size,15)
};
var marker = new google.maps.Marker({
position: point.latlng,
title: "" + point.crDate + "",
icon: goldStar,
visible: false,
});
google.maps.event.addListener(marker, "click", function() {
infowindow.close();
infowindow.setContent(
'<div style="cursor:pointer;text-decoration:underline;" onClick="window.opener.getQIDData('
+ point.qid
+ ',\'\','
+ point.t
+',\''
+ point.afd
+ '\')">'
+ point.crDate
+ '<br>'
+ point.afd
+ '<br>'
+ point.pers
+ '<br>'
+ point.qid
+'</div>' );
infowindow.open(map, marker);
});
return marker;
}
var markerGroups = {
"l-m": [],
"o-m": [],
"o-m2": [],
"r-m": [],
"r-b": [],
"r-bb": [],
"p-2": [],
"p-3": [],
"a-i": [],
"a-b": [],
"a-bl": [],
"a-on": [],
"a-old": [],
"a-youri": [],
"sms": [],
"l-1": [],
"l-2": []
};
// 'type' is the selected (pressed) button, corresponding to the markerGroups
function toggleGroup(type) {
var count = 0;
// buttons with .selected have no points visible (counter-intuitive, sorry ;-))
$("." + type).toggleClass("selected");
for (var group in markerGroups) {
if (!$("." + group).hasClass("selected")) {
count = (count + markerGroups[group].length);
}
}
// clear all markers before putting new on screen if count > 256
if (count > 256) {
for (var group in markerGroups) {
markerGroups[group].forEach(marker => marker.setVisible(false));
$("." + group).addClass("selected");
}
}
for (var group in markerGroups) {
var state = ($("." + group).hasClass("selected"))
? false
: true;
if (type == group) {
markerGroups[group].forEach(marker => marker.setVisible(state));
}
}
}
This
scaledSize: new google.maps.Size(size,15)
broke the code. Has nothing to do with maximum eventlisteners.
Making more than 256 markers visible with scaled images is impossible.
Thank you for reading.

Kendo Grid Sorting issues for numeric

I am using kendo grid to display data, but while sorting(ascending or descending) it's sorting perfectly for string values. But for numeric it's not sorting properly it's taking only first character to do sorting, not taking as string values even it's in numeric. How to solve this issue ?
You can use the gird column sortable.compare property to assign your own compare function.
Then what you are looking for is a Natural sort, like the one described here: http://web.archive.org/web/20130826203933/http://my.opera.com/GreyWyvern/blog/show.dml/1671288 and implemented here: http://www.davekoelle.com/files/alphanum.js
Here is a demo using a case insensitive version of the natural sort:
https://dojo.telerik.com/eReHUReH
function AlphaNumericCaseInsensitive(a, b) {
if (!a || a.length < 1) return -1;
var anum = Number(a);
var bnum = Number(b);
if (!isNaN(anum) && !isNaN(bnum)) {
return anum - bnum;
}
function chunkify(t) {
var tz = new Array();
var x = 0, y = -1, n = 0, i, j;
while (i = (j = t.charAt(x++)).charCodeAt(0)) {
var m = (i == 46 || (i >= 48 && i <= 57));
if (m !== n) {
tz[++y] = "";
n = m;
}
tz[y] += j;
}
return tz;
}
var aa = chunkify(a ? a.toLowerCase() : "");
var bb = chunkify(b ? b.toLowerCase() : "");
for (x = 0; aa[x] && bb[x]; x++) {
if (aa[x] !== bb[x]) {
var c = Number(aa[x]), d = Number(bb[x]);
if (!isNaN(c) && !isNaN(d)) {
return c - d;
} else return (aa[x] > bb[x]) ? 1 : -1;
}
}
return aa.length - bb.length;
}
var dataSource = new kendo.data.DataSource({
data: [
{ id: 1, item: "item101" },
{ id: 2, item: "item2" },
{ id: 3, item: "item11" },
{ id: 4, item: "item1" }
]
});
$("#grid").kendoGrid({
dataSource: dataSource,
sortable: true,
columns: [{
field: "item",
sortable: {
compare: function(a, b) {
return AlphaNumericCaseInsensitive(a.item, b.item);
}
}
}]
});

Can I use #for loop in emotion-js, similarly to #for in sass?

In sass if I write:
#for $i from 1 through 3
li:nth-child(#{$i})
transition-delay: #{$i * 0.3}s
, I can get a nice progressive transition delay for each list element.
Is it possible to do this with emotion-js ?
Okay I have figured it.
First I create a JS function, which does my loop and then returns the styles as an object
const menuListTrans = () => {
let styles = {};
for (let $i = 0; $i < 10; $i++) {
styles["&:nth-child(" + $i + ")"] = {
transitionDelay: "1s," + $i * 0.08 + "s",
};
}
return styles;
};
and then interpolate it in the styled component
const MenuList = styled.ul`
&.expanded > li {
transform: translateY(0);
${menuListTrans}
}
`;
here is the same approach but with variables.
export const nthChildDelay = ({ count = 10, delay = 100, multiplier = 80 }) => {
const styles = {};
[...Array(count).keys()].forEach((_, index) => {
if (index !== 0) {
styles[`&:nth-child(${index})`] = {
transitionDelay: `${delay + (index - 1) * multiplier}ms`,
};
}
});
return styles;
};
And then use it as
${nthChildDelay({ count: 10, delay: 100, multiplier: 100 })};

Found this code on a public institution's website and have some concerns. Seeking opinions

I found this code on a clients website, loaded the URL which returned the attached script. This appears to me (and I could be wrong) to be some sport of dataLayer info capture to spreadsheet auto-fill script. Naturally, what concerned my was the "userInfo", "user", "password" sections of this. I'm not overly versed in this level of code so I am reaching out here! Thanks!
! function(r, e, t) {
var n = function(e) {
return "string" == typeof e
},
o = function() {
return function(e) {
for (var n = {
strictMode: !1,
key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:#]*)(?::([^:#]*))?)?#)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:#]+:[^:#\/]*#)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:#?]*)(?::([^:#]*))?)?#)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
}, r = n.parser[n.strictMode ? "strict" : "loose"].exec(e), o = {}, t = 14; t--;) o[n.key[t]] = r[t] || "";
o[n.q.name] = {}, o[n.key[12]].replace(n.q.parser, function(e, r, t) {
r && (r = decodeURIComponent(r), o[n.q.name][r] && o[n.q.name][r].constructor === Array ? o[n.q.name][r].push(decodeURIComponent(t)) : o[n.q.name][r] ? o[n.q.name][r] = [o[n.q.name][r], decodeURIComponent(t)] : o[n.q.name][r] = decodeURIComponent(t))
});
var s = o.host.split(".");
return o.rootDomain = 2 <= s.length ? s[s.length - 2] + "." + s[s.length - 1] : "", o.href = e, o
}(r.location.href)
},
s = function() {
if (r.rl_widget_cfg) return r.rl_widget_cfg.id;
if (r.rl_siteid) return r.rl_siteid;
var e = o().queryKey.rl_siteid;
return e || ((e = localStorage.getItem("capture_previous_site_id")) || null)
},
c = e.createElement("script");
if (r.rl_widget_cfg || r.test_mode) c.src = "https://cdn.rlets.com/capture_static/mms/capture.js";
else {
var i = function() {
var e, r, t = s();
if (t && n(t) && 32 === (t = (e = t, n(e) ? !1 !== r && e.trim ? e.trim() : e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : e).replace(/-/g, "")).length) return "/" + t.substr(0, 3) + "/" + t.substr(3, 3) + "/" + t.substr(6, 3) + "/" + t.substr(9) + ".js"
}();
i && (c.src = "https://cdn.rlets.com/capture_configs" + i)
}
e.head.appendChild(c)
}(window, document);
cdn.rlets.com is for a tracking pixel for reachlocal.com. It's used for marketing purposes. (I've seen it used specifically as an integration with Facebook ads.)
It's minified, so hard to say exactly what it's doing, but I don't think it's malicious (any more than marketing pixels in general are).

Resources