isu-elements
Version:
Polymer components for building web apps.
639 lines (582 loc) • 17.5 kB
JavaScript
import { html, PolymerElement } from '@polymer/polymer'
import '@webcomponents/shadycss/entrypoints/apply-shim.js'
import { BaseBehavior } from './behaviors/base-behavior.js'
import { TipBehavior } from './behaviors/tip-behavior'
import { AjaxBehavior } from './behaviors/ajax-behavior'
import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class'
import '@polymer/paper-dialog'
import './behaviors/isu-elements-shared-styles.js'
import './isu-button.js'
import './isu-dialog'
import './isu-tip'
/**
* `isu-image-upload`
*
* Example:
* ```html
* <isu-image-upload label="上河图" value="{{file}}"></isu-image-upload>
* <isu-image-upload size-limit="1.4M" value="{{file}}"></isu-image-upload>
* <isu-image-upload label="上河图" type="view" src="https://d1.awsstatic.com/product-marketing/Elastic%20Beanstalk/ElasticBeanstalk_Benefit_Productivity.5cd0e6aedfa2e3b2c05ed7f5faeb0fd215c9742b.png"></isu-image-upload>
*
* ```
* ## Styling
*
* The following custom properties and mixins are available for styling:
*
* |Custom property | Description | Default|
* |----------------|-------------|----------|
* |`--isu-label` | Mixin applied to the label of image uploader | {}
* |`--isu-image-upload-width` | The width of image uploader | 140px
* |`--isu-image-upload-height` | The height of image uploader | 180px
* |`--isu-image-upload-buttons` | Mixin applied to tool buttons of the uploader if type is edit | {}
* |`--isu-image-view-button` | Mixin applied to tool buttons of the uploader if type is view | {}
*
*
* @customElement
* @polymer
* @demo demo/isu-image-upload/index.html
*/
class IsuImageUpload extends mixinBehaviors([BaseBehavior, TipBehavior, AjaxBehavior], PolymerElement) {
static get template () {
return html`
<style include="isu-elements-shared-styles">
:host {
display: inline-block;
font-family: var(--isu-ui-font-family), sans-serif;
font-size: var(--isu-ui-font-size);
}
#main-container {
display: flex;
height: inherit;
}
#inner-container {
display: flex;
flex-flow: column nowrap;
border: 1px dashed #ccc;
font-size: inherit;
width: var(--isu-image-upload-width, 140px);
height: var(--isu-image-upload-height, 180px);
background: #fafafa;
position: relative;
border-radius: 5px;
}
#img__container {
flex: 1;
cursor: zoom-in;
display: flex;
}
.toolbar {
display: flex;
background: #f0f0f0;
height: 36px;
justify-content: space-evenly;
align-items: center;
padding: 0 2px;
}
.toolbar isu-button {
height: 26px;
width: 42px;
--isu-button: {
background-color: #5cb85c;
@apply --isu-image-upload-buttons;
};
}
.toolbar isu-button.isu-button-view {
height: 26px;
width: 72px;
--isu-button: {
background-color: #5cb85c;
@apply --isu-image-view-button;
}
}
#viewer-dialog {
display: flex;
overflow: hidden;
width: 100%;
height: 100%;
padding: 0;
justify-content: center;
}
#viewer-img {
cursor: zoom-out;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #717171;
user-select: none;
}
#viewer-img img{
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
}
#file-chooser {
display: none;
}
#paste-panel {
flex: 1;
outline: 1px dashed #aeaeae;
outline-offset: -10px;
text-align: center;
padding: 20px;
white-space: normal;
display: flex;
justify-content: center;
align-items: center;
}
#paste-panel[hidden] {
display: none;
}
:host([data-has-src]) #paste-panel,
:host(:not([data-has-src])) #cancel-btn {
display: none;
}
:host(:not([data-has-src])) #img__container {
cursor: default;
}
:host([size=small]) #inner-container {
width: var(--isu-image-upload-width, 100px);
height: var(--isu-image-upload-height, 128px);
}
:host([size=large]) #inner-container {
width: var(--isu-image-upload-width, 170px);
height: var(--isu-image-upload-height, 218px);
}
.mirrorRotateLevel {
transform: rotateY(180deg); /* 水平镜像翻转 */
}
.icons {
position: absolute;
bottom: 30px;
display: flex;
width: 210px;
height: 45px;
border-radius: 25px;
justify-content: center;
align-items: center;
background-color: #555555;
opacity: 0.5;
z-index: 2;
}
.icons iron-icon {
color: white;
width: 25%;
}
</style>
<div id="main-container" class$="[[fontSize]]">
<template is="dom-if" if="[[ toBoolean(label) ]]">
<div class="isu-label-div"><span class$="isu-label [[fontSize]]">[[label]]</span><span class="isu-label-after-extension"></span></div>
</template>
<div id="inner-container">
<div id="img__container" on-click="openViewZoom">
<div id="paste-panel">拖拽或者粘贴图片到这里</div>
</div>
<div class="toolbar">
<template is="dom-if" if="[[__isEdit(type)]]">
<isu-button class$="[[fontSize]]" title="点击选择文件" on-click="_triggerChooseFile">选择</isu-button>
<isu-button class$="[[fontSize]]" id="cancel-btn" type="warning" on-click="cancelSelection">取消</isu-button>
</template>
<input type="file" on-change="_chooseFile" id="file-chooser" accept$="[[accept]]">
<template is="dom-if" if="[[!__isEdit(type)]]">
<isu-button class$="isu-button-view [[fontSize]]" on-click="openViewZoom">查看大图</isu-button>
</template>
</div>
<div class="mask" part="mask"></div>
</div>
</div>
<paper-dialog id="viewer-dialog" on-click="closeViewZoom" opened="{{_isOpened}}">
<div class="icons">
<iron-icon icon="icons:zoom-out" data-args="zoomOut" on-click="handleActions"></iron-icon>
<iron-icon icon="icons:zoom-in" data-args="zoomIn" on-click="handleActions"></iron-icon>
<iron-icon icon$="[[modeIcon]]" on-click="toggleMode"></iron-icon>
<iron-icon icon="icons:refresh" data-args="anticlocelise" on-click="handleActions"></iron-icon>
<iron-icon class="mirrorRotateLevel" icon="icons:refresh" data-args="clocelise" on-click="handleActions"></iron-icon>
</div>
<div id="viewer-img"><img src$="[[src]]" style$="[[imgStyle]]" on-click="imgHandle" on-mousedown="handleMouseDown"></div>
</paper-dialog>
`
}
static get properties () {
return {
/**
* The remote uri of image.
*/
src: {
type: String
},
/**
* The file object of the image. It will be `undefined` when image is from remote server.
*/
value: {
type: Object,
notify: true
},
/**
* The label of the uploader.
*/
label: {
type: String
},
/**
* Set to true, if the select is required.
* @type {boolean}
* @default false
*/
required: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
* Set to true, if the select is readonly.
* @type {boolean}
* @default false
*/
readonly: {
type: Boolean,
value: false,
reflectToAttribute: true
},
/**
* The max size/length of image allowed to upload.
* Support pattern: /^((?:\d*\.)?\d+)([GgMmKk][Bb]?$)/
* i.e 1M, 1Mb, 2Kb
* @type string
*/
sizeLimit: {
type: String
},
__byteSize: {
type: Number,
computed: '__parseSizeLimit(sizeLimit)'
},
/**
* Bound to input's `accept` attribute.
* @default 'image/gif, image/jpeg, image/png'
*/
accept: {
type: String,
value: 'image/gif, image/jpeg, image/png'
},
/**
* 模式:edit/view
* */
type: {
type: String,
value: 'edit'
},
/**
* Url for uploading the image,if it exit, image will be uploaded directly.
* */
uploadImgUrl: {
type: String,
value: ''
},
/**
* Custom your uploadFile`s name
* */
uploadFileName: {
type: String,
value: 'imageFile'
},
/**
* The callback function after upload the image
* */
uploadCallback: {
type: Function
},
cancelCallback: {
type: Function
},
/**
* Parse the format of the return data when the uploadImgUrl is not empty, eg: text|json|blob|formData|arrayBuffer
* */
handleAs: {
type: String,
value: 'json'
},
transform: {
type: Object,
value () {
return {
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
mode: '',
enableTransition: false
}
}
},
imgStyle: {
type: String
},
modeIcon: {
type: String,
value: 'icons:fullscreen'
},
_isOpened: {
type: Boolean
},
data: {
type: Object,
notify: true
}
}
}
static get is () {
return 'isu-image-upload'
}
static get observers () {
return [
'__srcChanged(src)', '_transformChanged(transform.*)', '__isOpenedChanged(_isOpened)'
]
}
connectedCallback () {
super.connectedCallback()
const ele = this.$['paste-panel']
const dragHandler = (e) => {
e.preventDefault()
e.stopPropagation()
if (this.readonly) return
if (e.type === 'drop') {
this.__readDataTransfer(e.dataTransfer)
} else if (e.type === 'paste') {
this.__readDataTransfer(e.clipboardData)
}
}
ele.addEventListener('dragenter', dragHandler, false)
ele.addEventListener('dragleave', dragHandler, false)
ele.addEventListener('dragover', dragHandler, false)
ele.addEventListener('drop', dragHandler, false)
ele.addEventListener('paste', dragHandler, false)
}
__isOpenedChanged (_isOpened) {
if (_isOpened === false) {
document.body.style['overflow-y'] = 'auto'
}
}
__isEdit (type) {
return type === 'edit'
}
__srcChanged (src) {
const style = this.$.img__container.style
// const viewerStyle = this.$['viewer-img'].style
if (src) {
this.setAttribute('data-has-src', '')
style.background = `url(${src}) no-repeat center`
style.backgroundSize = 'contain'
} else {
this.removeAttribute('data-has-src')
style.background = 'none'
}
}
__parseSizeLimit (sizeLimit) {
const reg = /^((?:\d*\.)?\d+)([GgMmKk][Bb]?$)/g
if (!reg.test(sizeLimit)) return 0
const bits = sizeLimit.replace(reg, (match, size, unit) => {
switch (unit.toUpperCase()) {
case 'GB':
case 'G':
return size * Math.pow(1024, 3)
case 'MB':
case 'M':
return size * Math.pow(1024, 2)
case 'KB':
case 'K':
return size * 1024
}
})
return bits | 0
}
_triggerChooseFile () {
const fileChooser = this.$['file-chooser']
fileChooser && fileChooser.click()
}
_chooseFile (e) {
const file = e.target.files[0]
file && this.__loadFileData(file)
}
__readDataTransfer (dataTransfer) {
const source = [].find.call(dataTransfer.items, item => item.kind === 'file')
source && this.__loadFileData(source.getAsFile())
}
async __loadFileData (blob) {
if (this.__byteSize > 0 && blob.size > this.__byteSize) {
this.isuTip.error(`上传图片不能超过${this.sizeLimit}`, 3000)
return
}
const reader = new FileReader()
reader.onload = (e) => {
this.src = e.target.result
this.value = blob
}
reader.readAsDataURL(blob)
/* 如果有上传文件的url,则直接上传到对应的服务器 */
if (this.uploadImgUrl) {
const formData = new FormData()
formData.append(this.uploadFileName, blob)
const data = await this.post({ url: this.uploadImgUrl, data: formData, handleAs: this.handleAs })
this.uploadCallback && this.isFunction(this.uploadCallback) && this.uploadCallback.call(this.domHost, data, this.uploadFileName, this)
}
}
/**
* Cancel selection of the image.It will clear the `src` and `value`.
* */
cancelSelection () {
this.src = null
this.value = null
this.$['file-chooser'].value = ''
this.cancelCallback && this.isFunction(this.cancelCallback) && this.cancelCallback.call(this.domHost, data, this, this.uploadFileName)
}
/**
* Open the view zoom
*/
openViewZoom () {
if (this.src) {
this.$['viewer-dialog'].open()
this.deviceSupportInstall()
}
}
/**
* Close the view zoom.
*/
closeViewZoom () {
this.$['viewer-dialog'].close()
this.deviceSupportUninstall()
}
_transformChanged () {
const { scale, deg, offsetX, offsetY, enableTransition, mode } = this.transform
const imgStyle = `
transform: scale(${scale}) rotate(${deg}deg);
transition: ${enableTransition ? 'transform .3s' : ''};
margin-left: ${offsetX}px;
margin-top: ${offsetY}px;
height: ${mode === 'fullscreen' ? '100%' : 'auto'}
`
this.set('imgStyle', imgStyle)
}
handleActions (e) {
e.stopPropagation()
const action = e.target.dataset.args
this._handleActions(action)
}
_handleActions (action, options) {
const { rotate, deg, enableTransition } = {
rotate: 0.2,
deg: 90,
enableTransition: true,
...options
}
switch (action) {
case 'zoomOut':
if (this.transform.scale > 0.2) {
this.set('transform.scale', parseFloat(+this.transform.scale - rotate).toFixed(3))
}
break
case 'zoomIn':
this.set('transform.scale', parseFloat(+this.transform.scale + rotate).toFixed(3))
break
case 'anticlocelise':
this.set('transform.deg', +this.transform.deg + deg)
break
case 'clocelise':
this.set('transform.deg', +this.transform.deg - deg)
break
}
this.set('transform.enableTransition', enableTransition)
}
toggleMode (e) {
e.stopPropagation()
const mode = this.transform.mode === 'fullscreen' ? '' : 'fullscreen'
this.reset()
this.set('transform.mode', mode)
this.set('modeIcon', mode === 'fullscreen' ? 'icons:reply' : 'icons:fullscreen')
}
imgHandle (e) {
e.stopPropagation()
}
reset () {
this.transform = {
scale: 1,
deg: 0,
offsetX: 0,
offsetY: 0,
enableTransition: false
}
}
deviceSupportInstall () {
this._keyDownHandler = e => {
e.stopPropagation()
const keyCode = e.key
switch (keyCode) {
// ESC
case 'Escape':
this.closeViewZoom()
break
// UP_ARROW
case 'ArrowUp':
this._handleActions('zoomIn')
break
// DOWN_ARROW
case 'ArrowDown':
this._handleActions('zoomOut')
}
}
this._mouseWheel = e => {
e.stopPropagation()
const delta = e.wheelDelta ? e.wheelDelta : -e.detail
if (delta > 0) {
this._handleActions('zoomIn', {
rotate: 0.015,
enableTransition: false
})
} else {
this._handleActions('zoomOut', {
rotate: 0.015,
enableTransition: false
})
}
}
const dialogEle = this.$['viewer-dialog']
dialogEle.addEventListener('keydown', this._keyDownHandler, false)
dialogEle.addEventListener('mousewheel', this._mouseWheel, false)
document.body.style['overflow-y'] = 'hidden'
}
deviceSupportUninstall () {
this.removeEventListener('keydown', this._keyDownHandler, false)
this.removeEventListener('mousewheel', this._mouseWheel, false)
this._keyDownHandler = null
this._mouseWheel = null
document.body.style['overflow-y'] = 'auto'
}
handleMouseDown (e) {
if (e.button !== 0) return
const { offsetX, offsetY } = this.transform
const startX = e.pageX
const startY = e.pageY
const _dragHandler = ev => {
this.set('transform.offsetX', offsetX + ev.pageX - startX)
this.set('transform.offsetY', offsetY + ev.pageY - startY)
}
const imgEle = this.$['viewer-img']
imgEle.addEventListener('mousemove', _dragHandler)
imgEle.addEventListener('mouseup', () => {
imgEle.removeEventListener('mousemove', _dragHandler)
})
e.preventDefault()
}
/**
* Validate, true if the select is set to be required and this.value is a truth-value or else false.
* @return {boolean}
*/
validate () {
return this.required ? !!this.value : true
}
}
window.customElements.define(IsuImageUpload.is, IsuImageUpload)