You've already forked pure-component
添加了编译流程;
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
25
gulpfile.js
Normal file
25
gulpfile.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const gulp = require('gulp')
|
||||
const chinese2unicode = require('gulp-chinese2unicode')
|
||||
const gulpTerser = require('gulp-terser')
|
||||
|
||||
// 构建
|
||||
gulp.task('minify', async () => {
|
||||
console.log('开始构建...')
|
||||
gulp
|
||||
.src('index.js')
|
||||
.pipe(
|
||||
gulpTerser({
|
||||
mangle: {
|
||||
properties: true,
|
||||
},
|
||||
format: {
|
||||
ascii_only: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
.pipe(chinese2unicode())
|
||||
.on('data', function () {
|
||||
console.log('构建完成!')
|
||||
})
|
||||
.pipe(gulp.dest('./src/'))
|
||||
})
|
378
index.js
378
index.js
@@ -3,113 +3,139 @@ class API {
|
||||
constructor(options = {}) {
|
||||
// 接口基础地址
|
||||
this.baseUrl = ''
|
||||
// 请求对象
|
||||
this.xhr = new XMLHttpRequest()
|
||||
// 合并配置
|
||||
Object.assign(this, options)
|
||||
}
|
||||
|
||||
// 创建新的 XMLHttpRequest 实例
|
||||
createXHR() {
|
||||
return new XMLHttpRequest()
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
setHeaders(headers = {}) {
|
||||
const { xhr } = this
|
||||
setHeaders(xhr, headers = {}) {
|
||||
for (let key in headers) {
|
||||
xhr.setRequestHeader(key, headers[key])
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
formatData(data) {
|
||||
let parseData = null
|
||||
if (typeof data !== 'string') return data
|
||||
|
||||
try {
|
||||
parseData = JSON.parse(data)
|
||||
} catch {
|
||||
parseData = data
|
||||
return JSON.parse(data)
|
||||
} catch (e) {
|
||||
return data
|
||||
}
|
||||
return parseData
|
||||
}
|
||||
|
||||
// 中断请求
|
||||
abort(callback = null) {
|
||||
this.xhr.abort()
|
||||
callback && callback()
|
||||
abort(xhr, callback = null) {
|
||||
if (xhr) {
|
||||
xhr.abort()
|
||||
callback && callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理响应
|
||||
handleResponse(xhr, callback, success, fail, error) {
|
||||
try {
|
||||
const response = this.formatData(xhr.responseText)
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
callback && callback(response)
|
||||
success && success(response)
|
||||
} else {
|
||||
const err = new Error(`HTTP Error ${xhr.status}`)
|
||||
error && error(err)
|
||||
fail && fail(err)
|
||||
}
|
||||
} catch (e) {
|
||||
error && error(e)
|
||||
fail && fail(e)
|
||||
}
|
||||
}
|
||||
|
||||
// get请求
|
||||
get(options = {}) {
|
||||
const { baseUrl, xhr, formatData } = this
|
||||
// 默认配置
|
||||
const defaultOption = {
|
||||
// 请求地址
|
||||
url: '',
|
||||
// 请求头
|
||||
headers: null,
|
||||
// 成功回调
|
||||
callback: null,
|
||||
success: null,
|
||||
// 失败回调
|
||||
fail: null,
|
||||
error: null,
|
||||
// 是否异步
|
||||
async: true,
|
||||
}
|
||||
Object.assign(defaultOption, options)
|
||||
const { url, callback, success, fail, error, async, headers } = defaultOption
|
||||
|
||||
xhr.open('GET', `${baseUrl}${url}`, async)
|
||||
headers && this.setHeaders(headers)
|
||||
const config = Object.assign({}, defaultOption, options)
|
||||
const { url, callback, success, fail, error, async, headers } = config
|
||||
const xhr = this.createXHR()
|
||||
|
||||
xhr.addEventListener('error', function (err) {
|
||||
xhr.open('GET', `${this.baseUrl}${url}`, async)
|
||||
|
||||
if (headers) {
|
||||
this.setHeaders(xhr, headers)
|
||||
}
|
||||
|
||||
xhr.addEventListener('error', err => {
|
||||
error && error(err)
|
||||
fail && fail(err)
|
||||
})
|
||||
xhr.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
callback && callback(formatData(this.responseText))
|
||||
success && success(formatData(this.responseText))
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
this.handleResponse(xhr, callback, success, fail, error)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
return xhr
|
||||
}
|
||||
|
||||
// post请求
|
||||
post(options = {}) {
|
||||
const { baseUrl, xhr, formatData } = this
|
||||
// 默认配置
|
||||
const defaultOption = {
|
||||
// 请求地址
|
||||
url: '',
|
||||
// 请求数据
|
||||
data: null,
|
||||
// 请求头
|
||||
headers: null,
|
||||
// 进度回调
|
||||
progress: null,
|
||||
// 成功回调
|
||||
callback: null,
|
||||
success: null,
|
||||
// 失败回调
|
||||
fail: null,
|
||||
error: null,
|
||||
// 是否异步
|
||||
async: true,
|
||||
}
|
||||
Object.assign(defaultOption, options)
|
||||
const { url, data, progress, callback, success, error, fail, async, headers } = defaultOption
|
||||
|
||||
xhr.open('POST', `${baseUrl}${url}`, async)
|
||||
headers && this.setHeaders(headers)
|
||||
const config = Object.assign({}, defaultOption, options)
|
||||
const { url, data, progress, callback, success, error, fail, async, headers } = config
|
||||
const xhr = this.createXHR()
|
||||
|
||||
xhr.addEventListener('progress', function (evt) {
|
||||
const { response } = evt.target
|
||||
progress && progress(response)
|
||||
})
|
||||
xhr.open('POST', `${this.baseUrl}${url}`, async)
|
||||
|
||||
xhr.addEventListener('error', function (err) {
|
||||
if (headers) {
|
||||
this.setHeaders(xhr, headers)
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
xhr.upload.addEventListener('progress', evt => {
|
||||
progress(evt)
|
||||
})
|
||||
}
|
||||
|
||||
xhr.addEventListener('error', err => {
|
||||
error && error(err)
|
||||
fail && fail(err)
|
||||
})
|
||||
xhr.onreadystatechange = function () {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
callback && callback(formatData(this.responseText))
|
||||
success && success(formatData(this.responseText))
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
this.handleResponse(xhr, callback, success, fail, error)
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send(data)
|
||||
return xhr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,16 +143,21 @@ class API {
|
||||
class UI {
|
||||
constructor(registry = null) {
|
||||
const that = this
|
||||
// 属性转换方法
|
||||
const parseBoolean = value => {
|
||||
return value !== null && value !== 'false'
|
||||
}
|
||||
// 搜索框
|
||||
this.Search = class Input extends HTMLElement {
|
||||
// 可用属性
|
||||
static observedAttributes = ['value', 'placeholder', 'disabled', 'max', 'wrap']
|
||||
static get observedAttributes() {
|
||||
return ['value', 'disabled']
|
||||
return ['value', 'placeholder', 'disabled', 'max', 'wrap']
|
||||
}
|
||||
// 构造函数
|
||||
constructor() {
|
||||
super()
|
||||
this.attachShadow({ mode: 'open' })
|
||||
// 输入框
|
||||
this.input = null
|
||||
// 输入框值
|
||||
@@ -147,38 +178,37 @@ class UI {
|
||||
}
|
||||
// 属性变化
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this[name] = newValue
|
||||
if (name == 'disabled') {
|
||||
this.input.querySelector('.search-input').setAttribute('disabled', eval(this.disabled))
|
||||
this.input.querySelector('.search-input').setAttribute('contenteditable', !eval(this.disabled))
|
||||
}
|
||||
if (this[name] === newValue) return
|
||||
|
||||
if (name == 'value') {
|
||||
this.input.querySelector('.search-input').value = newValue
|
||||
this[name] = name === 'disabled' || name === 'wrap' ? parseBoolean(newValue) : newValue
|
||||
|
||||
if (!this.input) return
|
||||
|
||||
switch (name) {
|
||||
case 'disabled':
|
||||
const inputEl = this.shadowRoot.querySelector('.search-input')
|
||||
if (inputEl) {
|
||||
inputEl.toggleAttribute('disabled', this.disabled)
|
||||
inputEl.contentEditable = !this.disabled
|
||||
const clearBtn = this.shadowRoot.querySelector('.search-clear')
|
||||
if (clearBtn) {
|
||||
clearBtn.classList.toggle('visible')
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'value':
|
||||
const searchInput = this.shadowRoot.querySelector('.search-input')
|
||||
if (searchInput) {
|
||||
searchInput.textContent = newValue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 初始化
|
||||
init() {
|
||||
const { value, placeholder, disabled, wrap } = this
|
||||
const shadow = this.attachShadow({ mode: 'open' })
|
||||
const template = document.createElement('template')
|
||||
const style = document.createElement('style')
|
||||
const inputBox = document.createElement('div')
|
||||
inputBox.className = inputBox.part = 'search'
|
||||
const input = document.createElement('div')
|
||||
input.className = input.part = 'search-input'
|
||||
input.setAttribute('placeholder', placeholder)
|
||||
input.setAttribute('disabled', disabled)
|
||||
input.contentEditable = true
|
||||
const prefix = document.createElement('slot')
|
||||
prefix.name = 'prefix'
|
||||
const append = document.createElement('slot')
|
||||
append.name = 'append'
|
||||
const clear = document.createElement('button')
|
||||
clear.className = clear.part = 'search-clear'
|
||||
clear.innerHTML = that.Icon.clear
|
||||
|
||||
style.innerHTML = `
|
||||
const shadow = this.shadowRoot
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.search {
|
||||
--background-color: #fff;
|
||||
--border-color: #ccc;
|
||||
@@ -190,57 +220,62 @@ class UI {
|
||||
--min-width: 150px;
|
||||
--gap: .5em;
|
||||
--scrollbar-color: var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1em;
|
||||
position: relative;
|
||||
background: var(--background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: var(--min-width);
|
||||
gap: var(--gap);
|
||||
cursor: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1em;
|
||||
position: relative;
|
||||
background: var(--background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--padding);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: var(--min-width);
|
||||
gap: var(--gap);
|
||||
cursor: text;
|
||||
}
|
||||
.search-input {
|
||||
width: 100%;
|
||||
font-size: 1em;
|
||||
outline: none;
|
||||
transition: all .2s;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
font-size: 1em;
|
||||
outline: none;
|
||||
transition: all .2s;
|
||||
resize: none;
|
||||
min-height: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.search-input::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: .1em;
|
||||
width: 0;
|
||||
height: .1em;
|
||||
}
|
||||
.search-input::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-color);
|
||||
background: var(--scrollbar-color);
|
||||
}
|
||||
.search-input:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: var(--placeholder-color);
|
||||
content: attr(placeholder);
|
||||
color: var(--placeholder-color);
|
||||
}
|
||||
.search-input:focus::before {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
.search:focus-within {
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.search-input:focus-within + .search-clear {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.search-clear {
|
||||
cursor: pointer;
|
||||
color: var(--clear-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: var(--clear-color);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transition: all .3s;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-clear.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.search-clear svg {
|
||||
fill: var(--clear-color);
|
||||
@@ -249,80 +284,99 @@ class UI {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
</style>
|
||||
<div class="search" part="search">
|
||||
<slot name="prefix"></slot>
|
||||
<div class="search-input" part="search-input"
|
||||
placeholder="${this.placeholder}"
|
||||
${this.disabled ? 'disabled' : ''}
|
||||
contenteditable="${!this.disabled}">
|
||||
${this.value}
|
||||
</div>
|
||||
<button class="search-clear" part="search-clear">
|
||||
${that.Icon.clear}
|
||||
</button>
|
||||
<slot name="append"></slot>
|
||||
</div>
|
||||
`
|
||||
|
||||
inputBox.appendChild(prefix)
|
||||
inputBox.appendChild(input)
|
||||
inputBox.appendChild(clear)
|
||||
inputBox.appendChild(append)
|
||||
this.input = shadow.querySelector('.search-input')
|
||||
this.input.textContent = ''
|
||||
this.value = ''
|
||||
// 初始清除按钮状态
|
||||
this.updateClearButton()
|
||||
|
||||
if (wrap || wrap === '') {
|
||||
input.style.whiteSpace = 'pre-wrap'
|
||||
input.style.overflowY = 'auto'
|
||||
// 换行处理
|
||||
if (this.wrap) {
|
||||
this.input.style.whiteSpace = 'pre-wrap'
|
||||
this.input.style.overflowY = 'auto'
|
||||
} else {
|
||||
input.style.whiteSpace = 'nowrap'
|
||||
input.style.overflowX = 'auto'
|
||||
this.input.style.whiteSpace = 'nowrap'
|
||||
this.input.style.overflowX = 'auto'
|
||||
}
|
||||
|
||||
input.innerText = value.trim()
|
||||
template.content.appendChild(style)
|
||||
template.content.appendChild(inputBox)
|
||||
shadow.appendChild(template.content.cloneNode(true))
|
||||
this.input = shadow
|
||||
}
|
||||
// 清空输入框
|
||||
updateClearButton() {
|
||||
const clearBtn = this.shadowRoot.querySelector('.search-clear')
|
||||
if (clearBtn) {
|
||||
clearBtn.classList.toggle('visible', this.value.length > 0)
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
this.input.querySelector('.search-input').innerText = this.value = ''
|
||||
this.input.textContent = ''
|
||||
this.value = ''
|
||||
this.updateClearButton()
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: this.value }))
|
||||
}
|
||||
// 绑定事件
|
||||
bindEvent() {
|
||||
const { input, max } = this
|
||||
// 清空输入框
|
||||
input.querySelector('.search-clear').addEventListener('click', e => {
|
||||
const clearBtn = this.shadowRoot.querySelector('.search-clear')
|
||||
const inputEl = this.input
|
||||
|
||||
clearBtn.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
input.querySelector('.search-input').innerText = this.value = ''
|
||||
this.clear()
|
||||
})
|
||||
|
||||
// 输入框输入事件
|
||||
input.querySelector('.search-input').addEventListener('input', e => {
|
||||
e.preventDefault()
|
||||
// 过滤换行符
|
||||
this.value = e.target.innerText.replace(/[\n]/g, '')
|
||||
inputEl.addEventListener('input', e => {
|
||||
let newValue = inputEl.textContent.replace(/[\n]/g, '')
|
||||
|
||||
if (this.value.length < max) {
|
||||
e.target.innerText = this.value
|
||||
} else {
|
||||
e.target.innerText = this.value = this.value.slice(0, max)
|
||||
if (newValue.length > this.max) {
|
||||
newValue = newValue.substring(0, this.max)
|
||||
inputEl.textContent = newValue
|
||||
}
|
||||
|
||||
// 移动光标到最后
|
||||
const range = document.createRange()
|
||||
const selection = window.getSelection()
|
||||
range.selectNodeContents(e.target)
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
this.value = newValue
|
||||
this.updateClearButton()
|
||||
|
||||
// 移动光标到末尾
|
||||
const range = document.createRange()
|
||||
const sel = window.getSelection()
|
||||
range.selectNodeContents(inputEl)
|
||||
range.collapse(false)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
|
||||
selection.addRange(range)
|
||||
e.target.focus()
|
||||
this.dispatchEvent(new CustomEvent('change', { detail: this.value }))
|
||||
})
|
||||
|
||||
// 输入框回车事件
|
||||
input.querySelector('.search-input').addEventListener('keydown', e => {
|
||||
inputEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
this.dispatchEvent(new CustomEvent('enter', { detail: this.value }))
|
||||
}
|
||||
})
|
||||
|
||||
// 输入框失焦事件
|
||||
input.querySelector('.search-input').addEventListener('blur', () => this.dispatchEvent(new CustomEvent('blur')))
|
||||
inputEl.addEventListener('blur', () => {
|
||||
this.dispatchEvent(new CustomEvent('blur'))
|
||||
})
|
||||
|
||||
// 输入框聚焦事件
|
||||
input.querySelector('.search-input').addEventListener('focus', () => this.dispatchEvent(new CustomEvent('focus')))
|
||||
inputEl.addEventListener('focus', () => {
|
||||
this.dispatchEvent(new CustomEvent('focus'))
|
||||
})
|
||||
}
|
||||
}
|
||||
// 下拉框
|
||||
@@ -330,7 +384,7 @@ class UI {
|
||||
// 可用属性
|
||||
static observedAttributes = ['placeholder', 'list', 'value', 'current', 'disabled', 'focus']
|
||||
static get observedAttributes() {
|
||||
return ['value', 'current', 'disabled', 'focus']
|
||||
return ['placeholder', 'list', 'value', 'current', 'disabled', 'focus']
|
||||
}
|
||||
// 构造函数
|
||||
constructor() {
|
||||
|
2548
package-lock.json
generated
Normal file
2548
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "pure-component",
|
||||
"version": "1.0.0",
|
||||
"description": "一个功能强大的UI组件库和一个轻量级的API请求工具,可以帮助开发者快速构建现代化的Web应用界面。",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "gulp minify"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"gulp": "^5.0.1",
|
||||
"gulp-chinese2unicode": "^1.0.1",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"terser": "^5.43.1"
|
||||
}
|
||||
}
|
1
src/index.js
Normal file
1
src/index.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user