How to make fixed header availabel at the focus an focusable element? - dom

I have a fixed header, when user scroll to an offset 100px, a class add to header to make it fixed:
fixed-header {
position: fixed:
top: 0,
left: 0,
right: 0,
width: 100%;
}
$( window ).on( 'scroll', function() {
if ( $( window ).scrollTop() >= 100 ) {
navigation.classList.add('.fixed-header');
document.body.style.paddingTop = "100px" // prevent page jump when fixed header
} else {
navigation.classList.remove('.fixed-header');
document.body.style.paddingTop = "0"
}
});
On top of page, when user focus a link via tab key, I use focus event listener to check the fixed class in DOM to scroll focusable element to re-position it below fixed header:
const elements = [...document.querySelectorAll(
"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])"
)];
header = document.body.querySelector('.header-top'); // Fixed header height: 100px
const handleFocus = (e) => {
console.log(header.classList.contains('.fixed-header')); // This will false
if (header.classList.contains('.fixed-header')) {
var windowScrollTop = $( window ).scrollTop(),
focusableScrollTop = $(e.target).offset().top;
if (focusableScrollTop - windowScrollTop < 100) {
window.scrollTo({ top: focusableScrollTop - 100, behavior: 'smooth' });
}
}
};
for (const element of elements) {
element.addEventListener('focus', handleFocus, { once: true });
}
But focus callback handler fire before DOM repaint (when fixed class added to header), so it can not detect fixed class in DOM:
Wrap focus handler inside requestAnimationFrame()', don't work, if I wrap focus handler inside setTimeout()' function with 300ms, it will detect correct fixed class, but cause page scroll jump (focusable element scroll up and down).
for (const element of elements) {
element.addEventListener('focus', setTimeout(() => {
handleFocus()
}, 300), { once: true });
}
Any way to make header.classList.contains('.fixed-header') come true at the focusable link focused?
I hope someone know this issue and help to fix this problem. Thank you very much.

Related

Is there a way to make an ion-modal-sheet to snap the header in Ionic V6

I'm currently developing an app using Ionic V6 and i'm figured out if it was possible to make an ion-modal-sheet snap the header when it fully swiped like the Uber eats application ?
Thanks in advance for your answers and have a nice day !
For those who wondering how to do it, here's how i managed to get something similar:
first of all i've added an id to my header (my ion-header is wrapped inside a component called header) if your header is always in the same page of where you're creating the modal, use a template reference variable instead of an id:
<header
id="getHeight"
[title]="'Solutions disponibles'"
[displayGoBack]="true"></header>
then i've created a helper to:
Get the height of the header (can be different between iOS and Android)
Get the body height
Use the body height and the header height to return the height my modal should have in px and in %
export const getContentHeight = (): any => {
const header = document.querySelector('#getHeight');
const body = document.querySelector('body');
if (body && header) {
const percent = (100 - (header.clientHeight * 100 / body.clientHeight));
return {
height: body.clientHeight - header.clientHeight,
percent: percent
}
}
}
After that i've created another helper to change the visual aspect of my modal when breakpoint reach 0.9:
export const setModalSheetContentHeight = (modal: HTMLIonModalElement, breakpoint: number, expand: boolean): any => {
if (breakpoint >= 0.9 && !expand) {
modal.classList.add('no-radius');
modal.setCurrentBreakpoint(1);
expand = true;
} else if (breakpoint <= 0.9 && expand) {
modal.classList.remove('no-radius');
expand = false;
}
return expand;
}
The boolean "expand" avoid the helper to always set the modal breakpoint to 1.
Then i've created a method that add some listeners on the modal:
initModalListener(): void {
let expand = false;
// Define if the modal is fully expand
const content = getContentHeight();
// Return an object with content height in px and in %
addEventListener('ionModalWillPresent', () => {
this.modal.setAttribute('style', `--height: ${content.height}px`);
// Set the modal height before it shown to avoid visual bug
});
addEventListener('ionBreakpointDidChange', (e: any) => {
const breakpoint = e.detail.breakpoint;
expand = setModalSheetContentHeight(this.modal, breakpoint, expand);
});
}
And after all of that i call this method inside of the method that create my modal:
private async presentModal(): Promise<void> {
this.modal = await this.modalCtrl.create({
component: YourModalComponent,
breakpoints: [0.3, 0.65, 1],
initialBreakpoint: 0.3,
backdropBreakpoint: 1,
showBackdrop: false,
canDismiss: false,
keyboardClose: true,
id: 'modalSheet',
}
});
this.initModalListener();
await this.modal.present();
}
This is a first shot so i'm sure that there is plenty of thing to adjust but for now, it work pretty well with Ionic 6.

Hiding DOM elements with a chrome extension without causing a flicker

Preface:
I am aware that there is a duplicate question out there. I am posting it again because it has no answers (and it's from 4 years ago).
General description of what I want:
I want to be able to hide a DOM-element (adding Element.style.display = "none") before the DOM is loaded into the view.
What I've tried:
Other posts point to using a MutationObserver and running it on the document element.
To ensure that we are able to hide an element before the DOM is loaded, we are to run the script containing the MutationObserver as a content_script with "run_at":"document_start".
I did all of this, and I still see a flicker (the elements appear when I load a page and then quickly disappear).
What I'm trying to do:
There's a ul which contains some li with some text on the page I inject my content_script.js into. I populate my popup.html with <text, checkbox> pairs. If the checkbox is checked, the li containing said text is visible, else it is hidden. I want it to persist between refreshes, hence the use of storage.
Things work - but there's a flicker whenever I refresh the page. The elements are there, then they're gone. I don't want them to show up in the first place!
My code:
When I detect that the DOM elements I may remove have loaded, I generate an Object that indicates whether I should hide or keep visible that specific DOM element.
I then set its Element.style.display to none or block accordingly.
/**manifest.json
...
"content_scripts": [
{
"matches": [
"some_website_url"
],
"js": [
"content_script.js"
],
"run_at": "document_start"
}
]
...
*/
///content_script.js
const mutationObserver = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName) {
if (node.querySelector(potentially_hidden_element_selector)) {
chrome.storage.sync.get("courses", ({ courses }) => {
chrome.storage.sync.set({ "courses": generateCourseList(courses) }, () => {
const courseElements = Array.from(node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'))
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement)
const isVisible = courses[courseName]
updateCourseElementInSidebar(courseElement, isVisible)
})
})
})
// We found what we were looking for so stop searching
mutationObserver.disconnect()
}
}
}
}
})
mutationObserver.observe(document, { childList: true, subtree: true })
EDIT 1:
My generateCourseList method depends on the DOM elements I may try to hide - so I can't call the chrome.storage.set method before the DOM has loaded I think.
When I refresh the page, a list of courses eventually populates the DOM.
I then populate the storage's courses object based on these course elements' innerText properties. I set each of these elements' visibility to true or false based on one of two factors: if this course is already defined in the courses object, keep its visibility status, if it isn't, set it to true (visible by default).
I can't make certain DOM elements visible/hidden if I don't have reference to them though. So if I try to call generateCourseList before those specific DOM elements have loaded, I end up trying to retrieve all the course elements (document.querySelectorAll('a[data-parent-key="mycourses"]')) and get returned nothing. I end up setting courses in chrome.storage to nothing because of this chrome.storage.sync.set({ "courses": generateCourseList(courses) }....
EDIT 2:
Here is all of my code. I try to chrome.storage.sync.get as soon as I can, and I try to not depend on the result of chrome.storage.sync.set.
I try to delete the elements as soon as I can, but I'm having difficulty doing so. This is because I have difficulty knowing when the content I want to access (the course elements) have fully loaded. Previously, I was detecting when one course element was visible, and when it was, I assumed all were. This was a mistake. I was able to access the one courselement the moment it popped up, but sometimes only 4 of the 6 course elements were actually loaded. I can't hardcode this number, because it changes from person to person. I can't just tackle them one by one, because then I wouldn't know when to disconnect the MutationObserver. I used the debugger and tried to find what element is loaded soon after all 6 course elements are loaded, and that is the header#page-header.row element. I still get a flicker, though less noticeable than before.
Anything I can do to make it even less noticeable?
function start_mutation_observer() {
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
const generatedCourses = generateCourseList(savedCourses)
const courseElements = getCourseElements()
// Set visibility of course elements
courseElements.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement);
const isShown = generatedCourses[courseName];
setCourseElementVisibility(courseElement, isShown);
});
chrome.storage.sync.set({ 'savedCourses': generatedCourses });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
function getCourseElements() {
const COURSE_ELEMENT_SELECTOR = 'ul > li > a[data-parent-key="mycourses"]'
return Array.from(document.querySelectorAll(COURSE_ELEMENT_SELECTOR))
}
function getCourseElementTextContent(courseElement) {
const COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR = 'a[data-parent-key="mycourses"] > div > div > span.media-body'
return courseElement.querySelector(COURSE_ELEMENT_TEXT_CONTAINER_SELECTOR).textContent
}
function generateCourseList(savedCourses) {
// Turns [[a, b], [b,c]] into {a:b, b:c}
return Object.fromEntries(getCourseElements().map(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const isShown = savedCourses[courseName] ?? true
return [courseName, isShown]
}))
}
function setCourseElementVisibility(courseElement, isShown) {
if (isShown) {
courseElement.style.display = "block"
} else {
courseElement.style.display = "none"
}
}
start_mutation_observer()
EDIT 3:
I think it's as good as can be now. I only refresh the visibility of the course elements that were just loaded into the DOM. There's essentially no flicker now (there is a slight one, but its' the same amount of flickering without my extension).
Here is the code for the MutationObserver
function start_mutation_observer() {
let handledCourseElements = new Set()
chrome.storage.sync.get({ 'savedCourses': {} }, ({ savedCourses }) => {
const observer = new MutationObserver((mutations) => {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
const courseElements = getCourseElements()
const courseElementsAdded = courseElements.length > handledCourseElements.size
// If a courseElement was added, update visibility of those that weren't already processed
if (courseElementsAdded) {
const generatedCourses = generateCourseList(savedCourses)
courseElements
.filter(courseElement => !handledCourseElements.has(courseElement))
.forEach(courseElement => {
const courseName = getCourseElementTextContent(courseElement)
const courseShouldBeVisible = generatedCourses[courseName];
setCourseElementVisibility(courseElement, courseShouldBeVisible);
handledCourseElements.add(courseElement)
})
}
// The page header gets updated AFTER the courseList is updated - so once it's in the page, we know the courseElements are too
if (document.querySelector('header#page-header.row')) {
observer.disconnect()
chrome.storage.sync.set({ 'savedCourses': generateCourseList(savedCourses) });
return
}
}
}
});
observer.observe(document, { childList: true, subtree: true });
// In case the content script has been injected when some of the DOM has already loaded
onMutation([{ addedNodes: [document.documentElement] }]);
});
}
Reading storage is slow and asynchronous, so you need to do it at the beginning:
chrome.storage.sync.get('courses', ({ courses }) => {
chrome.storage.sync.set({ 'courses': generateCourseList(courses) });
const observer = new MutationObserver(onMutation);
observer.observe(document, { childList: true, subtree: true });
onMutation([{addedNodes: [document.documentElement]}]);
function onMutation(mutations) {
for (const { addedNodes } of mutations) {
for (const node of addedNodes) {
if (node.tagName && node.querySelector(potentially_hidden_element_selector)) {
observer.disconnect();
processNode(node, courses);
}
}
}
}
});
function processNode(node, courses) {
const courseElements = Array.from(
node.closest('ul').querySelectorAll('a[data-parent-key="mycourses"]'));
courseElements.forEach(courseElement => {
const courseName = getCourseName(courseElement);
const isVisible = courses[courseName];
updateCourseElementInSidebar(courseElement, isVisible);
});
}

Ion-Content Scroll horizontally with mousewheel

I have a ion content with an horizontal scrollbar
<ion-content [fullscreen]="true" [scrollX]="true" (wheel)="onWheel($event)">
The scrollbar works well but the mouse wheel does not.
I would like to use mouse wheel to scroll horizontally
Here is my try :
onWheel(event: WheelEvent): void {
console.log(event.deltaY);
const element: HTMLElement = event.currentTarget as HTMLElement;
element.scrollLeft += event.deltaY;
event.preventDefault();
}
The console log show +100 or -100, but the scroll does not make it.
The scroll is on a element under the ion-content.
This works :
onWheel(event: WheelEvent): void {
const element: IonContent = event.currentTarget as unknown as IonContent;
element.getScrollElement().then((scroll) => {
scroll.scrollLeft += event.deltaY;
});
event.preventDefault();
}

Detect scrollHeight change with MutationObserver?

How can I detect when scrollHeight changes on a DOM element using MutationObserver? It's not an attribute and it isn't data either.
Background: I need to detect when a scrollbar appears on my content element, the overflow-y of which is set to auto. I figured that the instant the scrollbar appears the value of scrollHeight jumps from 0 to, say, 500, so the idea was to set up a MutationObserver to detect a change in this property.
What I've got so far:
HTML
<div class="body" #body>
CSS
.body {
overflow-y: auto;
}
TypeScript
export class MyWatchedContent implements AfterViewInit, OnDestroy {
#ViewChild('body', { read: ElementRef })
private body: ElementRef;
private observer: MutationObserver;
public ngAfterViewInit() {
this.observer = new MutationObserver(this.observerChanges);
this.observer.observe(this.body.nativeElement, {
attributes: true,
});
}
public ngOnDestroy() {
this.observer.disconnect();
}
private observerChanges(records: MutationRecord[], observer: MutationObserver) {
console.log('##### MUTATION');
records.forEach((_record) => {
console.log(_record);
});
}
}
If I, for example, change the background color in the developer window I can see the observer firing
MUTATION
my-content-watcher.component.ts?d0f4:233 MutationRecord {type: "attributes", target: div.body, addedNodes: NodeList(0), removedNodes: NodeList(0), previousSibling: null…}
If, however, I change the window size to make the scrollbar appear there's no mutation detected. Is this doable with MutationObserver at all and if so, how?
Here's the answer, for anyone still looking for the solution:
As of today, it's not possible to directly monitor scrollHeight changes of an element.
The MutationObserver detects changes in the DOM tree, which could indicate a scrollHeight change, but that's a wild guess.
The ResizeObserver detects changes in the outer height of an element, but not the scrollHeight (i.e. "inner" height).
There is no ScrollHeight-Observer (yet).
BUT the solution is very close:
The Solution
The ResizeObserver detects changes in the outer height of an element...
There's no point in observing the scroll-container because its outer height does not change. The element that changes their out height is any CHILD node of the container!
Once the height of a child node changes, it means, that the scrollHeight of the parent container changed.
Vanilla JS version
const container = document.querySelector('.scrollable-container');
const observer = new ResizeObserver(function() {
console.log('New scrollHeight', container.scrollHeight);
});
// This is the critical part: We observe the size of all children!
for (var i = 0; i < container.children.length; i++) {
observer.observe(container.children[i]);
})
jQuery version
const container = $('.scrollable-container');
const observer = new ResizeObserver(function() {
console.log('New scrollHeight', container[0].scrollHeight);
});
container.children().each(function(index, child) {
observer.observe(child);
});
Further steps
When children are added dynamically, you could add a MutationObserver to add new children to the ResizeObserver once they were added.
You can emulate this behavior by adding an internal wrapping element on the contenteditable element (eg a span) and then add the ResizeObserver listener on the internal element. The internal span has to be display:block otherwise it wont trigger the ResizeObserver.
HTML
<div contenteditable id="input"><span id="content">Some content</span></div>
CSS
#input {
max-height: 100px;
overflow: scroll;
white-space: pre-wrap;
}
#content {
display: block;
white-space: pre-wrap;
}
JS
const content = document.getElementById("content");
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.warn(entry);
}
});
observer.observe(content);

sort multiple items at once with jquery.ui.sortable

did somebody manage to sort multiple items at once with jquery.ui.sortable?
we are working on a photo managing app.
select multiple items
drag them to a new location.
thanx
I had a similar requirement, but the solution in the accepted answer has a bug. It says something like "insertBefore of null", because it removes the nodes.
And also i tried jQuery multisortable, it stacks the selected items on top of each other when dragging, which is not what i want.
So I rolled out my own implementation and hope it will save others some time.
Fiddle Link.
Source code:
$( "#sortable" ).sortable({
// force the cursor position, or the offset might be wrong
cursorAt: {
left: 50,
top: 45
},
helper: function (event, item) {
// make sure at least one item is selected.
if (!item.hasClass("ui-state-active")) {
item.addClass("ui-state-active").siblings().removeClass("ui-state-active");
}
var $helper = $("<li><ul></ul></li>");
var $selected = item.parent().children(".ui-state-active");
var $cloned = $selected.clone();
$helper.find("ul").append($cloned);
// hide it, don't remove!
$selected.hide();
// save the selected items
item.data("multi-sortable", $cloned);
return $helper;
},
stop: function (event, ui) {
// add the cloned ones
var $cloned = ui.item.data("multi-sortable");
ui.item.removeData("multi-sortable");
// append it
ui.item.after($cloned);
// remove the hidden ones
ui.item.siblings(":hidden").remove();
// remove self, it's duplicated
ui.item.remove();
}
});
There's a jQuery UI plugin for that: https://github.com/shvetsgroup/jquery.multisortable
jsFiddle: http://jsfiddle.net/neochief/KWeMM/
$('ul.sortable').multisortable();
... or just define a 'items' option to your multisortable that way (for example) :
$('table tbody').multisortable({
items: 'tr'
});
you can use shvetsgroup/jquery.multisortable
but it will create problem.. because, that js is designed only for tags...
but customize it to use it, its very simple i'll tell you how????
at first download that .js and use it in your program...
step 1. open the js file...now edit the following lines...
$.fn.multiselectable.defaults = {
click: function(event, elem) {},
mousedown: function(event, elem) {},
selectedClass: 'selected',
items: 'li'
};
the above are lines from 107 to 112....
there you can see "items: 'li'
in that use your tag which you are used to enclose those image like if you are using, or or anything you are using like this
$.fn.multiselectable.defaults = {
click: function(event, elem) {},
mousedown: function(event, elem) {},
selectedClass: 'selected',
items: 'div' // or any tag you want...
};
and 249 to 254
selectedClass: 'selected',
placeholder: 'placeholder',
items: 'li'
};
}(jQuery);
change the line " item:'li' " with your tag like this
selectedClass: 'selected',
placeholder: 'placeholder',
items: 'div' // or anything else
};
}(jQuery);
if you are working on textboxes inside those envelopes.. you have to get rid of these lines too
// If no previous selection found, start selecting from first selected item.
prev = prev.length ? prev : $(parent.find('.' + options.selectedClass)[0]).addClass('multiselectable-previous');
var prevIndex = prev.index();
after that comment line...
add a line code that search textbox or check box or any interaction element inside it...
like this..
// If no previous selection found, start selecting from first selected item.
item.children("input").focus(); // customize this code to get your element focus...
prev = prev.length ? prev : $(parent.find('.' + options.selectedClass)[0]).addClass('multiselectable-previous');
var prevIndex = prev.index();
and also to indicate selected tags or elements... use styles like this
div { margin: 2px 0; cursor: pointer; }
div.selected { background-color: orange }
div.child { margin-left: 20px; }
actually i used div.. instead of that you can use any tag you wish...
hope will help u.... if it is not... read again.. and ask again....
wishes