Add ripple support

pull/358/head
cagataycivici 2020-06-25 11:26:13 +03:00
parent 17411a9659
commit 1ecdcf58b4
14 changed files with 231 additions and 27 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="p-accordion p-component"> <div class="p-accordion p-component">
<slot></slot> <slot></slot>
<div v-for="(tab, i) of tabs" :key="tab.header || i" class="p-accordion-tab"> <div v-for="(tab, i) of tabs" :key="tab.header || i" :class="['p-accordion-tab', {'p-accordion-tab-active': tab.d_active}]">
<div :class="['p-accordion-header', {'p-highlight': tab.d_active, 'p-disabled': tab.disabled}]"> <div :class="['p-accordion-header', {'p-highlight': tab.d_active, 'p-disabled': tab.disabled}]">
<a role="tab" class="p-accordion-header-link" @click="onTabClick($event, tab)" @keydown="onTabKeydown($event, tab)" :tabindex="tab.disabled ? null : '0'" <a role="tab" class="p-accordion-header-link" @click="onTabClick($event, tab)" @keydown="onTabKeydown($event, tab)" :tabindex="tab.disabled ? null : '0'"
:aria-expanded="tab.d_active" :id="ariaId + i + '_header'" :aria-controls="ariaId + i + '_content'"> :aria-expanded="tab.d_active" :id="ariaId + i + '_header'" :aria-controls="ariaId + i + '_content'">

View File

@ -16,7 +16,7 @@
<transition name="p-input-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave"> <transition name="p-input-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave">
<div ref="overlay" class="p-autocomplete-panel p-component" :style="{'max-height': scrollHeight}" v-if="overlayVisible"> <div ref="overlay" class="p-autocomplete-panel p-component" :style="{'max-height': scrollHeight}" v-if="overlayVisible">
<ul :id="listId" class="p-autocomplete-items" role="listbox"> <ul :id="listId" class="p-autocomplete-items" role="listbox">
<li v-for="(item, i) of suggestions" class="p-autocomplete-item" :key="i" @click="selectItem($event, item)" role="option"> <li v-for="(item, i) of suggestions" class="p-autocomplete-item" :key="i" @click="selectItem($event, item)" role="option" v-ripple>
<slot name="item" :item="item" :index="i"> <slot name="item" :item="item" :index="i">
{{getItemContent(item)}} {{getItemContent(item)}}
</slot> </slot>
@ -32,6 +32,7 @@ import ObjectUtils from '../utils/ObjectUtils';
import DomHandler from '../utils/DomHandler'; import DomHandler from '../utils/DomHandler';
import Button from '../button/Button'; import Button from '../button/Button';
import UniqueComponentId from '../utils/UniqueComponentId'; import UniqueComponentId from '../utils/UniqueComponentId';
import Ripple from '../ripple/Ripple';
export default { export default {
inheritAttrs: false, inheritAttrs: false,
@ -424,6 +425,9 @@ export default {
}, },
components: { components: {
'Button': Button 'Button': Button
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -474,6 +478,8 @@ export default {
.p-autocomplete-item { .p-autocomplete-item {
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
position: relative;
overflow: hidden;
} }
.p-autocomplete-multiple-container { .p-autocomplete-multiple-container {

View File

@ -6,6 +6,8 @@
align-items: center; align-items: center;
vertical-align: bottom; vertical-align: bottom;
text-align: center; text-align: center;
overflow: hidden;
position: relative;
} }
.p-button-text { .p-button-text {

View File

@ -1,5 +1,5 @@
<template> <template>
<button :class="buttonClass" v-on="$listeners" type="button"> <button :class="buttonClass" v-on="$listeners" type="button" v-ripple>
<span v-if="icon" :class="iconClass"></span> <span v-if="icon" :class="iconClass"></span>
<span class="p-button-text">{{label||'&nbsp;'}}</span> <span class="p-button-text">{{label||'&nbsp;'}}</span>
<span class="p-badge" v-if="badge" :class="badgeClass">{{badge}}</span> <span class="p-badge" v-if="badge" :class="badgeClass">{{badge}}</span>
@ -7,6 +7,8 @@
</template> </template>
<script> <script>
import Ripple from '../ripple/Ripple';
export default { export default {
props: { props: {
label: { label: {
@ -48,6 +50,9 @@ export default {
} }
] ]
} }
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="p-checkbox p-component" @click="onClick($event)"> <div :class="containerClass" @click="onClick($event)">
<div class="p-hidden-accessible"> <div class="p-hidden-accessible">
<input ref="input" type="checkbox" :checked="checked" :value="value" v-bind="$attrs" @focus="onFocus($event)" @blur="onBlur($event)"> <input ref="input" type="checkbox" :checked="checked" :value="value" v-bind="$attrs" @focus="onFocus($event)" @blur="onBlur($event)">
</div> </div>
@ -61,6 +61,9 @@ export default {
computed: { computed: {
checked() { checked() {
return this.binary ? this.modelValue : ObjectUtils.contains(this.value, this.modelValue); return this.binary ? this.modelValue : ObjectUtils.contains(this.value, this.modelValue);
},
containerClass() {
return ['p-checkbox p-component', {'p-checkbox-checked': this.checked, 'p-checkbox-disabled': this.$attrs.disabled, 'p-checkbox-focused': this.focused}];
} }
} }
} }

View File

@ -151,4 +151,30 @@ button {
position: absolute; position: absolute;
width: 1px; width: 1px;
word-wrap: normal !important; word-wrap: normal !important;
} }
.p-ink {
display: block;
position: absolute;
background: rgba(255, 255, 255, 0.5);
border-radius: 100%;
transform: scale(0);
}
.p-ink-active {
animation: ripple 0.4s linear;
}
@-webkit-keyframes ripple {
100% {
opacity: 0;
-webkit-transform: scale(2.5);
}
}
@keyframes ripple {
100% {
opacity: 0;
transform: scale(2.5);
}
}

View File

@ -25,7 +25,7 @@
</div> </div>
<div ref="itemsWrapper" class="p-dropdown-items-wrapper" :style="{'max-height': scrollHeight}"> <div ref="itemsWrapper" class="p-dropdown-items-wrapper" :style="{'max-height': scrollHeight}">
<ul class="p-dropdown-items" role="listbox"> <ul class="p-dropdown-items" role="listbox">
<li v-for="(option, i) of visibleOptions" :class="['p-dropdown-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" <li v-for="(option, i) of visibleOptions" :class="['p-dropdown-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" v-ripple
:aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" role="option" :aria-selected="isSelected(option)"> :aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" role="option" :aria-selected="isSelected(option)">
<slot name="option" :option="option" :index="i"> <slot name="option" :option="option" :index="i">
{{getOptionLabel(option)}} {{getOptionLabel(option)}}
@ -41,6 +41,7 @@
<script> <script>
import ObjectUtils from '../utils/ObjectUtils'; import ObjectUtils from '../utils/ObjectUtils';
import DomHandler from '../utils/DomHandler'; import DomHandler from '../utils/DomHandler';
import Ripple from '../ripple/Ripple';
export default { export default {
props: { props: {
@ -451,6 +452,9 @@ export default {
equalityKey() { equalityKey() {
return this.optionValue ? null : this.dataKey; return this.optionValue ? null : this.dataKey;
} }
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -510,6 +514,8 @@ input.p-dropdown-label {
cursor: pointer; cursor: pointer;
font-weight: normal; font-weight: normal;
white-space: nowrap; white-space: nowrap;
position: relative;
overflow: hidden;
} }
.p-dropdown-items { .p-dropdown-items {

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="p-listbox-list-wrapper" :style="listStyle"> <div class="p-listbox-list-wrapper" :style="listStyle">
<ul class="p-listbox-list" role="listbox" aria-multiselectable="multiple"> <ul class="p-listbox-list" role="listbox" aria-multiselectable="multiple">
<li v-for="(option, i) of visibleOptions" :tabindex="isOptionDisabled(option) ? null : '0'" :class="['p-listbox-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" <li v-for="(option, i) of visibleOptions" :tabindex="isOptionDisabled(option) ? null : '0'" :class="['p-listbox-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" v-ripple
:aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @touchend="onOptionTouchEnd()" @keydown="onOptionKeyDown($event, option)" role="option" :aria-selected="isSelected(option)"> :aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @touchend="onOptionTouchEnd()" @keydown="onOptionKeyDown($event, option)" role="option" :aria-selected="isSelected(option)">
<slot name="option" :option="option" :index="i"> <slot name="option" :option="option" :index="i">
{{getOptionLabel(option)}} {{getOptionLabel(option)}}
@ -22,6 +22,7 @@
<script> <script>
import ObjectUtils from '../utils/ObjectUtils'; import ObjectUtils from '../utils/ObjectUtils';
import DomHandler from '../utils/DomHandler'; import DomHandler from '../utils/DomHandler';
import Ripple from '../ripple/Ripple';
export default { export default {
props: { props: {
@ -227,6 +228,9 @@ export default {
equalityKey() { equalityKey() {
return this.optionValue ? null : this.dataKey; return this.optionValue ? null : this.dataKey;
} }
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -244,6 +248,8 @@ export default {
.p-listbox-item { .p-listbox-item {
cursor: pointer; cursor: pointer;
position: relative;
overflow: hidden;
} }
.p-listbox-filter-container { .p-listbox-filter-container {

View File

@ -36,7 +36,7 @@
<div ref="itemsWrapper" class="p-multiselect-items-wrapper" :style="{'max-height': scrollHeight}"> <div ref="itemsWrapper" class="p-multiselect-items-wrapper" :style="{'max-height': scrollHeight}">
<ul class="p-multiselect-items p-component" role="listbox" aria-multiselectable="true"> <ul class="p-multiselect-items p-component" role="listbox" aria-multiselectable="true">
<li v-for="(option, i) of visibleOptions" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)" <li v-for="(option, i) of visibleOptions" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)"
:aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'"> :aria-label="getOptionLabel(option)" :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" v-ripple>
<div class="p-checkbox p-component"> <div class="p-checkbox p-component">
<div :class="['p-checkbox-box p-component', {'p-highlight': isSelected(option)}]"> <div :class="['p-checkbox-box p-component', {'p-highlight': isSelected(option)}]">
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span> <span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
@ -56,6 +56,7 @@
<script> <script>
import ObjectUtils from '../utils/ObjectUtils'; import ObjectUtils from '../utils/ObjectUtils';
import DomHandler from '../utils/DomHandler'; import DomHandler from '../utils/DomHandler';
import Ripple from '../ripple/Ripple';
export default { export default {
props: { props: {
@ -406,6 +407,9 @@ export default {
equalityKey() { equalityKey() {
return this.optionValue ? null : this.dataKey; return this.optionValue ? null : this.dataKey;
} }
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -466,6 +470,8 @@ export default {
align-items: center; align-items: center;
font-weight: normal; font-weight: normal;
white-space: nowrap; white-space: nowrap;
position: relative;
overflow: hidden;
} }
.p-multiselect-header { .p-multiselect-header {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="p-radiobutton p-component" @click="onClick($event)"> <div :class="containerClass" @click="onClick($event)">
<div class="p-hidden-accessible"> <div class="p-hidden-accessible">
<input ref="input" type="radio" :checked="checked" :value="value" v-bind="$attrs" @focus="onFocus($event)" @blur="onBlur($event)"> <input ref="input" type="radio" :checked="checked" :value="value" v-bind="$attrs" @focus="onFocus($event)" @blur="onBlur($event)">
</div> </div>
@ -51,6 +51,9 @@ export default {
computed: { computed: {
checked() { checked() {
return this.modelValue != null && ObjectUtils.equals(this.modelValue, this.value); return this.modelValue != null && ObjectUtils.equals(this.modelValue, this.value);
},
containerClass() {
return ['p-radiobutton p-component', {'p-radiobutton-checked': this.checked, 'p-radiobutton-disabled': this.$attrs.disabled, 'p-radiobutton-focused': this.focused}];
} }
} }
} }

View File

@ -0,0 +1,87 @@
import DomHandler from '../utils/DomHandler';
function bindEvents(el) {
el.addEventListener('mousedown', onMouseDown);
el.addEventListener('mouseleave', onMouseLeave);
}
function unbindEvents(el) {
el.removeEventListener('mousedown', onMouseDown);
el.removeEventListener('mouseleave', onMouseLeave);
}
function create(el) {
let ink = document.createElement('span');
ink.className = 'p-ink';
el.appendChild(ink);
ink.addEventListener('animationend', onAnimationEnd);
}
function remove(el) {
let ink = getInk(el);
if (ink) {
ink.removeEventListener('animationend', onAnimationEnd);
ink.remove();
}
}
function onMouseDown(event) {
let target = event.currentTarget;
let ink = getInk(target);
if (ink) {
DomHandler.removeClass(ink, 'p-ink-active');
if (!DomHandler.getHeight(ink) && !DomHandler.getWidth(ink)) {
let d = Math.max(DomHandler.getOuterWidth(target), DomHandler.getOuterHeight(target));
ink.style.height = d + 'px';
ink.style.width = d + 'px';
}
let offset = DomHandler.getOffset(target);
let x = event.pageX - offset.left + document.body.scrollTop - DomHandler.getWidth(ink) / 2;
let y = event.pageY - offset.top + document.body.scrollLeft - DomHandler.getHeight(ink) / 2;
ink.style.top = y + 'px';
ink.style.left = x + 'px';
DomHandler.addClass(ink, 'p-ink-active');
}
}
function onMouseLeave(event) {
resetInk(event);
}
function resetInk(event) {
let target = event.target;
let ink = getInk(target);
if (ink) {
DomHandler.removeClass(ink, 'p-ink-active');
}
}
function onAnimationEnd(event) {
DomHandler.removeClass(event.currentTarget, 'p-ink-active');
}
function getInk(el) {
for (let i = 0; i < el.children.length; i++) {
if (el.children[i].className.indexOf('p-ink') !== -1) {
return el.children[i];
}
}
return null;
}
const Ripple = {
inserted(el) {
create(el);
bindEvents(el);
},
unbind(el) {
remove(el);
unbindEvents(el);
}
};
export default Ripple;

View File

@ -1,23 +1,27 @@
<template> <template>
<div class="p-tabmenu p-component"> <div class="p-tabmenu p-component">
<ul class="p-tabmenu-nav p-reset" role="tablist"> <ul ref="nav" class="p-tabmenu-nav p-reset" role="tablist">
<template v-for="(item,i) of model"> <template v-for="(item,i) of model">
<li :key="item.label + '_' + i" :class="getItemClass(item)" :style="item.style" v-if="visible(item)" role="tab" :aria-selected="isActive(item)" :aria-expanded="isActive(item)"> <li :key="item.label + '_' + i" :class="getItemClass(item)" :style="item.style" v-if="visible(item)" role="tab" :aria-selected="isActive(item)" :aria-expanded="isActive(item)">
<router-link v-if="item.to && !item.disabled" :to="item.to" class="p-menuitem-link" @click.native="onItemClick($event, item)" role="presentation"> <router-link v-if="item.to && !item.disabled" :to="item.to" class="p-menuitem-link" @click.native="onItemClick($event, item)" role="presentation" v-ripple>
<span :class="getItemIcon(item)" v-if="item.icon"></span> <span :class="getItemIcon(item)" v-if="item.icon"></span>
<span class="p-menuitem-text">{{item.label}}</span> <span class="p-menuitem-text">{{item.label}}</span>
</router-link> </router-link>
<a v-else :href="item.url" class="p-menuitem-link" :target="item.target" @click="onItemClick($event, item)" role="presentation" :tabindex="item.disabled ? null : '0'"> <a v-else :href="item.url" class="p-menuitem-link" :target="item.target" @click="onItemClick($event, item)" role="presentation" :tabindex="item.disabled ? null : '0'" v-ripple>
<span :class="getItemIcon(item)" v-if="item.icon"></span> <span :class="getItemIcon(item)" v-if="item.icon"></span>
<span class="p-menuitem-text">{{item.label}}</span> <span class="p-menuitem-text">{{item.label}}</span>
</a> </a>
</li> </li>
</template> </template>
<li ref="inkbar" class="p-tabmenu-ink-bar"></li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script>
import DomHandler from '../utils/DomHandler';
import Ripple from '../ripple/Ripple';
export default { export default {
props: { props: {
model: { model: {
@ -25,6 +29,12 @@ export default {
default: null default: null
} }
}, },
mounted() {
this.updateInkBar();
},
updated() {
this.updateInkBar();
},
methods: { methods: {
onItemClick(event, item) { onItemClick(event, item) {
if (item.disabled) { if (item.disabled) {
@ -53,12 +63,32 @@ export default {
}, },
visible(item) { visible(item) {
return (typeof item.visible === 'function' ? item.visible() : item.visible !== false); return (typeof item.visible === 'function' ? item.visible() : item.visible !== false);
},
findActiveTabIndex() {
if (this.model) {
for (let i = 0; i < this.model.length; i++) {
let item = this.model[i];
if (this.isActive(this.model[i])) {
return i;
}
}
}
return null;
},
updateInkBar() {
let tabHeader = this.$refs.nav.children[this.findActiveTabIndex()];
this.$refs.inkbar.style.width = DomHandler.getWidth(tabHeader) + 'px';
this.$refs.inkbar.style.left = tabHeader.offsetLeft + 'px';
} }
}, },
computed: { computed: {
activeRoute() { activeRoute() {
return this.$route.path; return this.$route.path;
} }
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -79,6 +109,8 @@ export default {
align-items: center; align-items: center;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
text-decoration: none;
overflow: hidden;
} }
.p-tabmenu-nav a:focus { .p-tabmenu-nav a:focus {
@ -88,4 +120,8 @@ export default {
.p-tabmenu-nav .p-menuitem-text { .p-tabmenu-nav .p-menuitem-text {
line-height: 1; line-height: 1;
} }
.p-tabmenu-ink-bar {
display: none;
}
</style> </style>

View File

@ -1,12 +1,13 @@
<template> <template>
<div class="p-tabview p-component"> <div class="p-tabview p-component">
<ul class="p-tabview-nav" role="tablist"> <ul ref="nav" class="p-tabview-nav" role="tablist">
<li role="presentation" v-for="(tab, i) of tabs" :key="tab.header || i" :class="[{'p-highlight': (tab.d_active), 'p-disabled': tab.disabled}]"> <li role="presentation" v-for="(tab, i) of tabs" :key="tab.header || i" :class="[{'p-highlight': (tab.d_active), 'p-disabled': tab.disabled}]">
<a role="tab" class="p-tabview-nav-link" @click="onTabClick($event, tab)" @keydown="onTabKeydown($event, tab)" :tabindex="tab.disabled ? null : '0'" :aria-selected="tab.d_active"> <a role="tab" class="p-tabview-nav-link" @click="onTabClick($event, tab)" @keydown="onTabKeydown($event, tab)" :tabindex="tab.disabled ? null : '0'" :aria-selected="tab.d_active" v-ripple>
<span class="p-tabview-title" v-if="tab.header">{{tab.header}}</span> <span class="p-tabview-title" v-if="tab.header">{{tab.header}}</span>
<TabPanelHeaderSlot :tab="tab" v-if="tab.$scopedSlots.header" /> <TabPanelHeaderSlot :tab="tab" v-if="tab.$scopedSlots.header" />
</a> </a>
</li> </li>
<li ref="inkbar" class="p-tabview-ink-bar"></li>
</ul> </ul>
<div class="p-tabview-panels"> <div class="p-tabview-panels">
<slot></slot> <slot></slot>
@ -15,6 +16,9 @@
</template> </template>
<script> <script>
import DomHandler from '../utils/DomHandler';
import Ripple from '../ripple/Ripple';
const TabPanelHeaderSlot = { const TabPanelHeaderSlot = {
functional: true, functional: true,
props: { props: {
@ -37,6 +41,13 @@ export default {
mounted() { mounted() {
this.d_children = this.$children; this.d_children = this.$children;
}, },
updated() {
let activeTab = this.tabs[this.findActiveTabIndex()];
if (!activeTab && this.tabs.length) {
this.tabs[0].d_active = true;
}
this.updateInkBar();
},
methods: { methods: {
onTabClick(event, tab) { onTabClick(event, tab) {
if (!tab.disabled && !tab.d_active) { if (!tab.disabled && !tab.d_active) {
@ -54,29 +65,28 @@ export default {
this.tabs[i].d_active = active; this.tabs[i].d_active = active;
this.tabs[i].$emit('update:active', active); this.tabs[i].$emit('update:active', active);
} }
this.updateInkBar();
}, },
onTabKeydown(event, tab) { onTabKeydown(event, tab) {
if (event.which === 13) { if (event.which === 13) {
this.onTabClick(event, tab); this.onTabClick(event, tab);
} }
}, },
findActiveTab() { findActiveTabIndex() {
let activeTab;
for (let i = 0; i < this.tabs.length; i++) { for (let i = 0; i < this.tabs.length; i++) {
let tab = this.tabs[i]; let tab = this.tabs[i];
if (tab.d_active) { if (tab.d_active) {
activeTab = tab; return i;
break;
} }
} }
return activeTab; return null;
} },
}, updateInkBar() {
updated() { let tabHeader = this.$refs.nav.children[this.findActiveTabIndex()];
let activeTab = this.findActiveTab(); this.$refs.inkbar.style.width = DomHandler.getWidth(tabHeader) + 'px';
if (!activeTab && this.tabs.length) { this.$refs.inkbar.style.left = tabHeader.offsetLeft + 'px';
this.tabs[0].d_active = true;
} }
}, },
computed: { computed: {
@ -86,6 +96,9 @@ export default {
}, },
components: { components: {
'TabPanelHeaderSlot': TabPanelHeaderSlot 'TabPanelHeaderSlot': TabPanelHeaderSlot
},
directives: {
'ripple': Ripple
} }
} }
</script> </script>
@ -106,6 +119,11 @@ export default {
align-items: center; align-items: center;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
overflow: hidden;
}
.p-tabview-ink-bar {
display: none;
} }
.p-tabview-nav-link:focus { .p-tabview-nav-link:focus {

View File

@ -88,8 +88,8 @@ export default class DomHandler {
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
return { return {
top: rect.top + document.body.scrollTop, top: rect.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0),
left: rect.left + document.body.scrollLeft left: rect.left + (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0),
}; };
} }