0 },
get firstVideoIndex() { return this.hasVideos ? this.images.length : -1 },
get total() { return this.images.length + this.productVideos.length },
modalOpen: false,
modalActive: 0,
// Drag/swipe state
isDragging: false,
wasDragging: false,
startX: 0,
currentX: 0,
dragOffset: 0,
containerWidth: 0,
startY: 0,
directionLocked: null,
isRtl: false,
imageLoading: false,
// Zoom state
zoomScale: 1,
zoomPanX: 0,
zoomPanY: 0,
zoomPinching: false,
zoomPanning: false,
zoomInitialDistance: 0,
zoomInitialScale: 1,
zoomPanStartX: 0,
zoomPanStartY: 0,
zoomPanOffsetX: 0,
zoomPanOffsetY: 0,
zoomLastTap: 0,
zoomLastTouchX: 0,
zoomLastTouchY: 0,
zoomAnimating: false,
lastInputWasTouch: false,
init() {
this.isRtl = document.documentElement.dir === 'rtl';
this.$nextTick(() => {
this.containerWidth = this.$refs.carouselContainer?.offsetWidth || 0;
// Handle already-cached images where x-on:load won't fire
this.$el.querySelectorAll('img.lqip-placeholder').forEach(lqip => {
const realImg = lqip.nextElementSibling;
if (realImg && realImg.complete && realImg.naturalWidth > 0) {
realImg.dataset.lqipDone = '1';
lqip.style.display = 'none';
}
});
});
window.addEventListener('resize', () => {
this.containerWidth = this.$refs.carouselContainer?.offsetWidth || 0;
});
// Preload first image of each variant for instant switching
Object.values(this.variationImagesMap).forEach(variant => {
if (variant.images && variant.images[0]) { new Image().src = variant.images[0]; }
});
// Only the active slide may play. Clearing startedVideo also
// drops the bound src, so swiping away stops the download and
// not just the playback.
this.$watch('active', () => {
this.startedVideo = null;
this.$el.querySelectorAll('video[data-product-video]').forEach(v => v.pause());
});
},
handleDragStart(e) {
if (this.total <= 1) return;
this.isDragging = true;
this.directionLocked = null;
this.startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
this.startY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
this.currentX = this.startX;
this.containerWidth = this.$refs.carouselContainer?.offsetWidth || 0;
},
handleDragMove(e) {
if (!this.isDragging) return;
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
if (this.directionLocked === null) {
const deltaX = Math.abs(clientX - this.startX);
const deltaY = Math.abs(clientY - this.startY);
if (deltaX < 5 && deltaY < 5) return;
if (deltaY > deltaX) {
this.directionLocked = 'vertical';
this.isDragging = false;
this.dragOffset = 0;
return;
}
this.directionLocked = 'horizontal';
}
if (this.directionLocked === 'vertical') return;
e.preventDefault();
this.currentX = clientX;
let diff = this.currentX - this.startX;
// Limit drag at edges
const atFirst = this.active === 0;
const atLast = this.active === this.total - 1;
const isAtEdge = this.isRtl ?
(atFirst && diff < 0) || (atLast && diff > 0) :
(atFirst && diff > 0) || (atLast && diff < 0);
if (isAtEdge) {
diff = diff * 0.3; // Resistance at edges
}
this.dragOffset = diff;
},
handleDragEnd() {
this.directionLocked = null;
if (!this.isDragging) return;
this.isDragging = false;
// Mark that we just finished dragging to prevent click from opening modal
if (Math.abs(this.dragOffset) > 5) {
this.wasDragging = true;
setTimeout(() => { this.wasDragging = false; }, 100);
}
const threshold = this.containerWidth * 0.2;
if (Math.abs(this.dragOffset) > threshold) {
if (this.isRtl) {
// RTL: drag left (negative) = prev, drag right (positive) = next
if (this.dragOffset < 0) {
this.prev();
} else {
this.next();
}
} else {
// LTR: drag right (positive) = prev, drag left (negative) = next
if (this.dragOffset > 0) {
this.prev();
} else {
this.next();
}
}
}
this.dragOffset = 0;
},
getSlideTransform() {
// RTL: images flow right-to-left, so we use positive offset
// LTR: images flow left-to-right, so we use negative offset
const baseOffset = this.isRtl ? this.active * 100 : -this.active * 100;
const dragPercent = this.containerWidth > 0 ? (this.dragOffset / this.containerWidth) * 100 : 0;
return `translateX(${baseOffset + dragPercent}%)`;
},
next() {
if (this.active < this.total - 1) {
this.active = this.active + 1;
}
},
prev() {
if (this.active > 0) {
this.active = this.active - 1;
}
},
goTo(idx) { this.active = idx },
openModal(idx) {
if (this.isDragging || this.wasDragging) return;
// The lightbox is images only. Its surface is touch-action:none
// with modalDragStart calling preventDefault() on every touch,
// which would swallow the native video control bar - and
// native fullscreen is one tap away from the inline player
// anyway. Video slides simply never open it.
if (this.hasVideos && idx >= this.firstVideoIndex) return;
this.modalActive = idx;
this.modalOpen = true;
document.body.style.overflow = 'hidden';
},
closeModal() {
this.resetZoom();
this.modalOpen = false;
document.body.style.overflow = 'auto';
},
// Wrap on the image count, not `total`: the modal never shows a
// video, so paging must not land on a video index.
get modalTotal() { return Math.max(1, this.images.length) },
modalNext() { this.resetZoom(); this.modalActive = (this.modalActive + 1) % this.modalTotal },
modalPrev() { this.resetZoom(); this.modalActive = (this.modalActive - 1 + this.modalTotal) % this.modalTotal },
modalGoTo(idx) { this.resetZoom(); this.modalActive = idx },
// Modal drag/swipe
modalDragging: false,
modalWasDragging: false,
modalStartX: 0,
modalDragOffset: 0,
// Zoom helpers
resetZoom() {
this.zoomScale = 1;
this.zoomPanX = 0;
this.zoomPanY = 0;
this.zoomPinching = false;
this.zoomPanning = false;
},
getTouchDistance(touches) {
return Math.hypot(
touches[0].clientX - touches[1].clientX,
touches[0].clientY - touches[1].clientY
);
},
clampPan() {
const maxPanX = Math.max(0, (this.zoomScale - 1) * window.innerWidth / 2);
const maxPanY = Math.max(0, (this.zoomScale - 1) * window.innerHeight / 2);
this.zoomPanX = Math.max(-maxPanX, Math.min(maxPanX, this.zoomPanX));
this.zoomPanY = Math.max(-maxPanY, Math.min(maxPanY, this.zoomPanY));
},
modalDragStart(e) {
const isTouch = e.type.includes('touch');
this.lastInputWasTouch = isTouch;
// Store touch position for double-tap zoom center
if (isTouch && e.touches[0]) {
this.zoomLastTouchX = e.touches[0].clientX;
this.zoomLastTouchY = e.touches[0].clientY;
}
// Two fingers: start pinch-to-zoom
if (isTouch && e.touches.length === 2) {
this.zoomPinching = true;
this.zoomInitialDistance = this.getTouchDistance(e.touches);
this.zoomInitialScale = this.zoomScale;
this.modalDragging = false;
this.zoomPanning = false;
e.preventDefault();
return;
}
// Single finger/mouse while zoomed: start panning
if (this.zoomScale > 1) {
this.zoomPanning = true;
this.zoomPanStartX = isTouch ? e.touches[0].clientX : e.clientX;
this.zoomPanStartY = isTouch ? e.touches[0].clientY : e.clientY;
this.zoomPanOffsetX = this.zoomPanX;
this.zoomPanOffsetY = this.zoomPanY;
e.preventDefault();
return;
}
// Single finger not zoomed or mouse: existing swipe
// Prevent browser double-tap-to-zoom so our custom handler works
if (isTouch) {
e.preventDefault();
}
if (this.total <= 1) return;
this.modalDragging = true;
this.modalStartX = isTouch ? e.touches[0].clientX : e.clientX;
this.modalDragOffset = 0;
},
modalDragMove(e) {
const isTouch = e.type.includes('touch');
// Pinch-to-zoom
if (this.zoomPinching && isTouch && e.touches.length >= 2) {
e.preventDefault();
const newDist = this.getTouchDistance(e.touches);
let newScale = this.zoomInitialScale * (newDist / this.zoomInitialDistance);
this.zoomScale = Math.max(1, Math.min(4, newScale));
this.clampPan();
return;
}
// Panning while zoomed
if (this.zoomPanning) {
e.preventDefault();
const px = isTouch ? e.touches[0].clientX : e.clientX;
const py = isTouch ? e.touches[0].clientY : e.clientY;
const dx = px - this.zoomPanStartX;
const dy = py - this.zoomPanStartY;
this.zoomPanX = this.zoomPanOffsetX + dx;
this.zoomPanY = this.zoomPanOffsetY + dy;
this.clampPan();
return;
}
// Existing swipe logic
if (!this.modalDragging) return;
e.preventDefault();
const x = isTouch ? e.touches[0].clientX : e.clientX;
this.modalDragOffset = x - this.modalStartX;
},
modalDragEnd(e) {
const isTouch = e && e.type && e.type.includes('touch');
// End pinch
if (this.zoomPinching) {
this.zoomPinching = false;
// Snap to 1 if nearly unzoomed
if (this.zoomScale < 1.1) {
this.resetZoom();
}
this.modalWasDragging = true;
setTimeout(() => { this.modalWasDragging = false; }, 100);
// If one finger remains after pinch, start panning
if (isTouch && e.touches && e.touches.length === 1 && this.zoomScale > 1) {
this.zoomPanning = true;
this.zoomPanStartX = e.touches[0].clientX;
this.zoomPanStartY = e.touches[0].clientY;
this.zoomPanOffsetX = this.zoomPanX;
this.zoomPanOffsetY = this.zoomPanY;
}
return;
}
// End pan
if (this.zoomPanning) {
this.zoomPanning = false;
this.modalWasDragging = true;
setTimeout(() => { this.modalWasDragging = false; }, 100);
// Double-tap to zoom out while panned
if (isTouch) {
const now = Date.now();
if (now - this.zoomLastTap < 300) {
this.zoomAnimating = true;
setTimeout(() => { this.zoomAnimating = false; }, 300);
this.resetZoom();
this.zoomLastTap = 0;
} else {
this.zoomLastTap = now;
}
}
return;
}
// Existing swipe end logic
const wasDragging = this.modalDragging;
if (this.modalDragging) {
this.modalDragging = false;
const absDrag = Math.abs(this.modalDragOffset);
if (absDrag > 5) {
this.modalWasDragging = true;
setTimeout(() => { this.modalWasDragging = false; }, 100);
}
const threshold = 80;
if (absDrag > threshold) {
if (this.isRtl) {
this.modalDragOffset < 0 ? this.modalPrev() : this.modalNext();
} else {
this.modalDragOffset > 0 ? this.modalPrev() : this.modalNext();
}
this.modalDragOffset = 0;
this.zoomLastTap = 0;
return;
}
this.modalDragOffset = 0;
}
// Double-tap detection (works for both zoomed and not zoomed)
if (isTouch) {
const now = Date.now();
if (now - this.zoomLastTap < 300) {
this.zoomAnimating = true;
setTimeout(() => { this.zoomAnimating = false; }, 300);
this.modalWasDragging = true;
setTimeout(() => { this.modalWasDragging = false; }, 200);
if (this.zoomScale > 1) {
this.resetZoom();
} else {
const cx = this.zoomLastTouchX - window.innerWidth / 2;
const cy = this.zoomLastTouchY - window.innerHeight / 2;
this.zoomScale = 2.5;
this.zoomPanX = -cx * (this.zoomScale - 1) / this.zoomScale;
this.zoomPanY = -cy * (this.zoomScale - 1) / this.zoomScale;
this.clampPan();
}
this.zoomLastTap = 0;
return;
}
this.zoomLastTap = now;
// Single tap on backdrop: close modal (touch equivalent of @click.self)
if (e.target === e.currentTarget) {
this.closeModal();
}
}
},
switchToVariation(attrValueId) {
const varImages = this.variationImagesMap[attrValueId];
let newImages, newLargeImages, newLqipImages;
if (varImages && varImages.images && varImages.images.length > 0) {
newImages = varImages.images;
newLargeImages = varImages.largeImages || varImages.images;
newLqipImages = varImages.lqipImages || [];
} else {
newImages = this.defaultImages;
newLargeImages = this.defaultLargeImages;
newLqipImages = this.defaultLqipImages;
}
// Show loading state while first image loads
this.imageLoading = true;
const firstImg = new Image();
firstImg.onload = () => {
this.localImages = newImages;
this.localLargeImages = newLargeImages;
this.localLqipImages = newLqipImages;
this.active = 0;
this.modalActive = 0;
this.imageLoading = false;
};
firstImg.onerror = () => {
this.localImages = newImages;
this.localLargeImages = newLargeImages;
this.localLqipImages = newLqipImages;
this.active = 0;
this.modalActive = 0;
this.imageLoading = false;
};
firstImg.src = newImages[0];
// If already cached, onload fires synchronously or near-instantly
}
}" x-on:keydown.escape.window="closeModal()"
x-on:keydown.arrow-left.window="modalOpen && modalPrev()"
x-on:keydown.arrow-right.window="modalOpen && modalNext()"
x-on:switch-variation-images.window="switchToVariation($event.detail.attrValueId ?? $event.detail[0]?.attrValueId)">
/
0 ? -0.25 : 0.25;
const oldScale = zoomScale;
zoomScale = Math.max(1, Math.min(4, zoomScale + delta));
if (oldScale === 1 && zoomScale > 1) {
const r = $el.getBoundingClientRect();
const cx = $event.clientX - (r.left + r.width/2);
const cy = $event.clientY - (r.top + r.height/2);
zoomPanX = -cx * (zoomScale-1)/zoomScale;
zoomPanY = -cy * (zoomScale-1)/zoomScale;
} else if (zoomScale > 1) {
zoomPanX = zoomPanX * (zoomScale / oldScale);
zoomPanY = zoomPanY * (zoomScale / oldScale);
}
if (zoomScale <= 1) { resetZoom(); } else { clampPan(); }
"
class="relative w-full h-full flex items-center justify-center p-4 [touch-action:none]"
:class="zoomScale > 1 ? (zoomPanning ? 'cursor-grabbing' : 'cursor-grab') : (modalTotal > 1 ? (modalDragging ? 'cursor-grabbing' : 'cursor-grab') : '')"
@mousedown="modalDragStart($event)"
@mousemove="modalDragMove($event)"
@mouseup="modalDragEnd($event)"
@mouseleave="modalDragEnd($event)"
@touchstart="modalDragStart($event)"
@touchmove="modalDragMove($event)"
@touchend="modalDragEnd($event)"
:style="modalDragOffset ? `transform: translateX(${modalDragOffset}px); transition: none;` : 'transition: transform 0.2s ease-out;'">
1) { resetZoom(); } else { const r = $el.getBoundingClientRect(); const cx = $event.clientX - (r.left + r.width/2); const cy = $event.clientY - (r.top + r.height/2); zoomScale = 2.5; zoomPanX = -cx * (zoomScale-1)/zoomScale; zoomPanY = -cy * (zoomScale-1)/zoomScale; clampPan(); } setTimeout(() => { zoomAnimating = false; }, 300);"
draggable="false"
:style="modalActive === largeIdx
? 'position:relative;transform: scale(' + zoomScale + ') translate(' + (zoomPanX / zoomScale) + 'px, ' + (zoomPanY / zoomScale) + 'px); transform-origin: center center; will-change: transform; transition: ' + (zoomAnimating ? 'transform 0.3s ease-out' : (zoomScale > 1 ? 'none' : 'transform 0.2s ease-out')) + ';'
: ''" />
جلبة التوصيل المستقيمة "HYDRO uPVC Straight Coupling / Socket"،
ادوات ومعدات زراعية
AED 20
جلبة التوصيل المستقيمة المتميزة من ماركة هايدرو العالمية "HYDRO uPVC Straight Coupling / Socket"، المصنوعة من البولي فينيل كلورايد غير الملدن عالي الجودة، هي القطعة الهندسية الأساسية والمعتمدة لربط وتمديد خطوط الأنابيب الملساء في مسار مستقيم واحد داخل شبكات الري الحديثة، خطوط الأنابيب الزراعية، وأنظمة توزيع المياه. يتميز بتصميمه المتين ومقاومته الفائقة للضغوط العالية والمواد الكيميائية، مما يضمن دمجاً مثالياً وبدون أي تسريب.
إليك المواصفات والدليل الإرشادي الكامل للمنتج :
✨ مميزات المنتج
- ربط مستقيم ومثالي الكفاءة: يمنحك اتصالاً مستقيماً ومحكماً بنسبة 100% لتمديد طول الأنابيب أو لربط الأجزاء المقطوعة أثناء أعمال الصيانة والتعديل على شبكات الري بسلاسة تامة.
- حصانة كاملة ضد التآكل والصدأ: خامات uPVC المعتمدة من HYDRO تمنح الجلبة مقاومة مطلقة للأكسدة، الأملاح، وتأثير الأسمدة الكيماوية والمبيدات المذابة في مياه الري.
- حافة مصد داخلية (Internal Stop Ridge): مزودة بمصد داخلي بارز في المنتصف تماماً يضمن دخول الأنبوب من كلا الطرفين لمسافة متساوية ومحددة هندسياً، مما يحقق توزيعاً متوازناً للضغط ويمنع أي خطأ في التركيب.
- جدران داخلية ناعمة: تمتاز الأسطح الداخلية بنعومة فائقة تقلل من الاحتكاك، مما يمنع ترسب الأملاح أو التكلسات ويحافظ على ثبات تدفق وضغط المياه عبر خط الري.
🧪 المواصفات الفنية والفيزيائية
- العلامة التجارية (Brand): هايدرو (HYDRO) الرائدة في مستلزمات وشبكات الري المتطورة.
- نوع المنتج: جلبة لحام / وصلة مستقيمة ملساء (uPVC Straight Coupling / Socket).
- المادة والتركيب (Material): بولي فينيل كلورايد غير ملدن (uPVC) نقي عالي الكثافة ومقاوم للأشعة فوق البنفسجية (UV).
- نوع التوصيل (Connection Type): غراء / لحام بارد (Solvent Cement) من الطرفين لتوفير ربط دائم، متين، ومقاوم تماماً للتسريب تحت مختلف الظروف.
- الاستدامة: عمر افتراضي طويل وخامات شديدة التحمل للاستخدام في المزارع المكشوفة والبيئات الصحراوية الحارة.
📅 دليل الاستخدام والتطبيق السريع
- طرق التركيب والتطبيق:
- التنظيف: قم بتنظيف الأسطح الخارجية للأنابيب والسطح الداخلي للجلبة جيداً من الأتربة والرطوبة والشحوم باستخدام منظف uPVC مخصص.
- وضع الغراء: ضع طبقة متساوية من غراء الأنابيب uPVC عالي الجودة على أطراف الأنابيب وفي الداخل للجلبة.
- الكبس والتثبيت: ادفع الأنبوب الأول داخل الجلبة بسرعة حتى يصل إلى المصد الداخلي مع تدويره ربع لفة، ثم كرر العملية للأنبوب المقابل في الطرف الآخر، وثبتهما يدوياً لمدة 10-15 ثانية حتى يتماسك اللحام البارد.
- الاستخدامات المستهدفة: تمديد خطوط مياه الري بالتنقيط والرش الرئيسية والفرعية، صيانة خطوط الأنابيب المكسورة، تمديدات الصوب الزراعية، وأنظمة الفلترة والتحكم المركزية.
⚠️ إرشادات التخزين والسلامة
- التخزين: يُحفظ في مكان جاف وبعيداً عن الأتربة قبل التركيب لضمان نظافة الأسطح الداخلية قبل عملية اللصق والتركيب لضمان أعلى قوة دمج ممكنة.
- توصية HYDRO التشغيلية: اترك الوصلات المغراة تجف تماماً للمدة الموصى بها على عبوة الغراء (عادة 24 ساعة) قبل تشغيل المياه وضخ الضغط العالي في الشبكة للتأكد من جفاف اللحام الكيميائي تماماً.
| التصنيف | ادوات ومعدات زراعية |
|---|
لا توجد تقييمات بعد. كن أول من يقيم هذا المنتج!
تسجيل الدخول لكتابة تقييم
منتجات مرتبطة
AED 20