UNPKG

kui-vue

Version:

A lightweight desktop UI component library suitable for Vue.js 2.

489 lines (450 loc) 13.8 kB
import { defineComponent, ref, reactive, onMounted, onBeforeUnmount, watch, nextTick, toRefs, } from "vue"; import Icon from "../icon"; import { getChildren } from "../utils/vnode"; import { withInstall } from "../utils/vue"; import { Refresh, Close, ArrowDown, IconImage, ChevronUp, Loading, AddCircleOutline, RemoveCircleOutline, } from "kui-icons"; const ImagePreview = defineComponent({ name: "ImagePreview", props: { type: String, src: String, origin: String, hasControl: Boolean, value: Boolean, data: { type: Array, default: () => [] }, showPanel: Boolean, }, setup(props, { emit, slots, expose, listeners }) { // console.log(props, listeners, slots); const { value, type, src, origin, showPanel, data } = toRefs(props); const state = reactive({ scale: 1, data, rotate: 0, startPos: { x: 0, y: 0 }, initPos: { x: 0, y: 0 }, left: 0, top: 0, isMouseDown: false, type: type.value, visible: value.value, src: origin.value || src.value, loading: false, error: false, vertical: true, isShowPanel: showPanel.value, panelRight: 0, touch: false, }); const imgRef = ref(null); const panelRef = ref(null); const updatePanelRight = () => { state.panelRight = panelRef.value && state.isShowPanel ? panelRef.value.offsetWidth : 0; }; const setRotate = (left) => { state.rotate = left ? state.rotate - 90 : state.rotate + 90; state.vertical = !state.vertical; resetPosition(); }; const setScale = (zoom) => { state.scale = zoom ? state.scale + 1 : state.scale - 1; state.scale = zoom ? Math.min(state.scale, 5) : Math.max(1, state.scale); resetPosition(); }; const close = () => { state.visible = false; emit("input", false); emit("close"); }; const mousewheel = (e) => { if (!state.visible) return; const { deltaY } = e; setScale(deltaY && deltaY < 0); e.stopPropagation(); e.preventDefault(); }; const mousedown = (e) => { if (!state.visible) return; if (imgRef.value && imgRef.value.contains(e.target)) { if (e.button && e.button != 0) return; let clientX, clientY; if (e.touches && e.touches.length == 1) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } state.isMouseDown = true; state.startPos = { x: clientX, y: clientY }; state.initPos = { x: clientX, y: clientY }; mousemove(e); const [e1, e2] = state.touch ? ["touchmove", "touchend"] : ["mousemove", "mouseup"]; document.addEventListener(e1, mousemove, { passive: false }); document.addEventListener(e2, mouseup, { passive: false }); } }; const resetPosition = () => { if (state.error) return; const { innerHeight, innerWidth } = window; const scale = state.scale; const top = state.top; const left = state.left; const vertical = state.vertical; if (!imgRef.value) return; let offsetWidth = imgRef.value.offsetWidth; let offsetHeight = imgRef.value.offsetHeight; let panelWidth = panelRef.value && state.isShowPanel ? panelRef.value.offsetWidth : 0; let newWidth = offsetWidth + ""; let newHeight = offsetHeight + ""; if (!vertical) { newWidth = offsetHeight + ""; newHeight = offsetWidth + ""; } if (newWidth * scale >= innerWidth - panelWidth) { let maxLeft = (newWidth * scale - (innerWidth - panelWidth)) / 2; if (left >= maxLeft) { state.left = maxLeft; } else if (state.left < -maxLeft) { state.left = -maxLeft; } } else { state.left = 0; } if (newHeight * scale >= innerHeight) { let maxTop = (newHeight * scale - innerHeight) / 2; if (top >= maxTop) { state.top = maxTop; } else if (top < -maxTop) { state.top = -maxTop; } } else { state.top = 0; } }; const mouseup = () => { if (!state.visible) return; state.isMouseDown = false; resetPosition(); const [e1, e2] = state.touch ? ["touchmove", "touchend"] : ["mousemove", "mouseup"]; document.removeEventListener(e1, mousemove); document.removeEventListener(e2, mouseup); }; const mousemove = (e) => { if (!state.visible) return; if (state.isMouseDown) { e.preventDefault(); let clientX, clientY; if (e.touches && e.touches.length == 1) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const { x, y } = state.startPos; state.left += clientX - x; state.top += clientY - y; state.startPos = { x: clientX, y: clientY }; } }; const switchImage = (left) => { state.scale = 1; const data = props.data || []; const index = data.indexOf(state.src); let newIndex = index + 0; newIndex = left ? newIndex - 1 : newIndex + 1; newIndex = Math.max(0, newIndex); newIndex = Math.min(newIndex, data.length - 1); // if (props.global && !slots.panel) { state.src = data[newIndex]; // } if ((left && index == 0) || (!left && index == data.length - 1)) return; emit("switch", newIndex); }; const download = () => { if (!state.error) { const x = new XMLHttpRequest(); x.open("GET", state.src, true); x.responseType = "blob"; x.onload = function () { const url = window.URL.createObjectURL(x.response); const a = document.createElement("a"); a.href = url; a.download = ""; a.click(); }; x.send(); } }; const togglePanel = () => { state.isShowPanel = !state.isShowPanel; emit("togglePanel", state.isShowPanel); nextTick(() => resetPosition()); updatePanelRight(); }; const getPanel = () => { const panel = getChildren(slots.panel?.()); if (panel.length) { return ( <div class={[ "k-image-preview-panel", { "k-image-preview-panel-hidden": !state.isShowPanel }, ]} ref={panelRef} > <span class="k-image-preview-panel-action" onClick={() => togglePanel()} > <Icon type={ChevronUp} /> </span> {panel} </div> ); } return null; }; watch( () => props.src, (src) => { state.src = src; } ); watch( () => props.value, (value) => { state.visible = value; if (value) { nextTick(() => { updatePanelRight(); }); } } ); watch( () => state.src, (src) => { if (state.type == "media" || !src) return; let image = new Image(); let isCompleted = false; const cleanup = () => { if (isCompleted) return; isCompleted = true; image.onload = null; image.onerror = null; image = null; }; state.loading = true; state.error = false; image.onload = () => { state.loading = false; cleanup(); }; image.onerror = () => { state.loading = false; state.error = true; cleanup(); }; image.src = src; } ); watch( () => props.showPanel, (value) => { state.isShowPanel = value; updatePanelRight(); } ); onMounted(() => { if (typeof window !== "undefined") { const touch = !!( "ontouchstart" in window || (window.DocumentTouch && document instanceof window.DocumentTouch) ); state.touch = touch; const event = touch ? "touchstart" : "mousedown"; document.addEventListener(event, mousedown, { passive: false }); document.addEventListener("mousewheel", mousewheel, { passive: false }); document.addEventListener("keydown", escToClose); } }); onBeforeUnmount(() => { document.removeEventListener("mousewheel", mousewheel); document.removeEventListener("keydown", escToClose); }); const show = (options = {}) => { if (options?.props?.src) { state.src = options.props.src; } if (options?.props?.type) { state.type = options.props.type; } state.visible = true; }; const hide = () => { state.visible = false; }; const escToClose = (e) => { if (e.keyCode === 27) { hide(); } }; expose({ show, hide, togglePanel }); return () => { const { scale, rotate, visible, src, left, top, data, loading, panelRight, type, } = state; const imgStyle = { transform: `scale3d(${scale}, ${scale}, 1) rotate(${rotate}deg)`, }; const moveStyle = { transform: `translate3d(${left}px, ${top}px, 0px)`, transition: state.isMouseDown ? "0s" : null, }; const imgProps = { class: "k-image-preview-img", attrs: { src }, style: imgStyle, ref: imgRef, }; const tools = getChildren(slots.tool?.()); return ( <div class="k-image-preview-root"> <transition name="k-image-zoom"> <div class="k-image-preview" v-show={visible}> <div class="k-image-preview-mask" onClick={close}></div> <div class="k-image-preview-wrap" style={{ right: panelRight + "px" }} > <ul class="k-image-preview-control"> <li class="k-image-preview-action" onClick={close}> <Icon type={Close} /> </li> <li class="k-image-preview-action-divider" /> {tools.map((tool) => { return <li class="k-image-preview-action">{tool}</li>; })} <li class="k-image-preview-action" onClick={download}> <Icon type={ArrowDown} /> </li> <li class={[ "k-image-preview-action", { "k-image-preview-action-disabled": scale >= 5 }, ]} onClick={() => setScale(1)} > <Icon type={AddCircleOutline} /> </li> <li class={[ "k-image-preview-action", { "k-image-preview-action-disabled": scale <= 1 }, ]} onClick={() => setScale(0)} > <Icon type={RemoveCircleOutline} /> </li> <li class="k-image-preview-action k-image-preview-action-rotate-right" onClick={() => setRotate(0)} > <Icon type={Refresh} /> </li> <li class="k-image-preview-action k-image-preview-action-rotate-left" onClick={() => setRotate(1)} > <Icon type={Refresh} /> </li> </ul> <div class="k-image-preview-img-wrap" style={moveStyle}> {type == "media" ? ( <video controls {...imgProps} /> ) : !state.error && !state.loading ? ( <img {...imgProps} /> ) : ( <div class="k-image-preview-img-error"> <Icon type={IconImage} /> </div> )} </div> {props.data.length > 1 ? [ <div class={[ "k-image-preview-switch-left", { "k-image-preview-switch-disabled": data.indexOf(src) == 0, }, ]} onClick={() => switchImage(1)} > <Icon type={ChevronUp} /> </div>, <div class={[ "k-image-preview-switch-right", { "k-image-preview-switch-disabled": data.indexOf(src) == data.length - 1, }, ]} onClick={() => switchImage()} > <Icon type={ChevronUp} /> </div>, ] : null} {loading ? ( <div class="k-image-preview-loading"> <Icon type={Loading} spin /> </div> ) : null} </div> {getPanel()} </div> </transition> </div> ); }; }, }); export default withInstall(ImagePreview);