├── packages ├── jeact │ ├── index.ts │ ├── index.js │ ├── src │ │ ├── index.ts │ │ ├── utils.ts │ │ ├── v-node.ts │ │ ├── component.ts │ │ ├── dom.ts │ │ └── diff.ts │ ├── package.json │ └── README.MD └── jeact-components │ ├── index.ts │ ├── index.js │ ├── src │ ├── index.ts │ ├── modal │ │ ├── index.less │ │ └── index.tsx │ ├── pm-selector │ │ ├── index.less │ │ └── index.tsx │ ├── picture-selector │ │ ├── index.less │ │ └── index.tsx │ ├── single-cascader │ │ ├── index.less │ │ └── index.tsx │ └── cascader │ │ ├── index.less │ │ └── index.tsx │ └── package.json ├── .prettierignore ├── .eslintrc.js ├── .prettierrc.js ├── scripts ├── dev.js ├── utils.js ├── build.js └── release.js ├── .gitignore ├── .stylelintrc ├── LICENSE ├── tsconfig.json ├── README.MD ├── package.json ├── rollup.config.js ├── docs ├── index.html └── js │ └── jeact.iife.js └── CHANGELOG.md /packages/jeact/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /packages/jeact-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | -------------------------------------------------------------------------------- /packages/jeact/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = require('./dist/jeact.umd.js') 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/jeact-components/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = require('./dist/jqvm-components.umd.js') 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | module.exports = { 3 | ...fabric.prettier, 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /packages/jeact-components/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cascader'; 2 | export * from './single-cascader'; 3 | export * from './pm-selector'; 4 | export * from './picture-selector'; 5 | export * from './modal'; 6 | -------------------------------------------------------------------------------- /packages/jeact/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 简易的MVVM插件 3 | * 可直接嵌入到任何JS代码中,把某个DOM和其子节点设置为MVVM管理 4 | * 5 | * author mifan 6 | */ 7 | 8 | export * from './utils'; 9 | export * from './component'; 10 | export * from './v-node'; 11 | -------------------------------------------------------------------------------- /packages/jeact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jeact", 3 | "version": "0.2.0", 4 | "description": "jeact-类react嵌入式UI库", 5 | "main": "index.js", 6 | "module": "src/index.ts", 7 | "author": "fanzicai", 8 | "license": "MIT", 9 | "homepage": "https://github.com/qqabcv520/jeact", 10 | "bugs": "https://github.com/qqabcv520/jeact/issues", 11 | "repository": { 12 | "url": "https://github.com/qqabcv520/jeact", 13 | "type": "git" 14 | }, 15 | "files": [ 16 | "index.js", 17 | "src/**/*.d.ts", 18 | "dist", 19 | "README.MD" 20 | ], 21 | "peerDependencies": { 22 | "jquery": "^3.5.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/jeact-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jeact/component", 3 | "version": "0.2.0", 4 | "description": "@jeact/component", 5 | "main": "index.js", 6 | "module": "dist/jeact-component.es.js", 7 | "author": "fanzicai", 8 | "license": "MIT", 9 | "dependencies": { 10 | "classname": "^0.0.0", 11 | "sortablejs": "^1.10.2", 12 | "jeact": "~0.2.0", 13 | "jquery": "^3.5.0" 14 | }, 15 | "devDependencies": { 16 | "@types/sortablejs": "^1.10.4" 17 | }, 18 | "files": [ 19 | "index.js", 20 | "src/**/*.d.ts", 21 | "dist" 22 | ], 23 | "peerDependencies": { 24 | "jquery": "^3.5.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const execa = require('execa') 4 | const { fuzzyMatchTarget } = require('./utils') 5 | const args = require('minimist')(process.argv.slice(2)) 6 | const target = args._.length ? fuzzyMatchTarget(args._)[0] : 'jeact' 7 | const sourceMap = args.sourcemap || args.s || true 8 | const formats = args.format || args.f 9 | 10 | execa( 11 | 'rollup', 12 | [ 13 | '-wc', 14 | '--environment', 15 | [ 16 | `TARGET:${target}`, 17 | `FORMATS:${formats || 'iife'}`, 18 | sourceMap ? `SOURCE_MAP:true` : `` 19 | ] 20 | .filter(Boolean) 21 | .join(',') 22 | ], 23 | { 24 | stdio: 'inherit' 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /tmp 5 | /out-tsc 6 | /**/dist/ 7 | *.d.ts 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | *.iml 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | .vscode 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | .history 44 | -------------------------------------------------------------------------------- /packages/jeact-components/src/modal/index.less: -------------------------------------------------------------------------------- 1 | .bgx-modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | overflow: scroll; 8 | background-color: rgba(0, 0, 0, 0.4); 9 | z-index: 1000; 10 | &-wrapper { 11 | margin: 40px auto; 12 | border-radius: 4px; 13 | overflow: hidden; 14 | width: 1000px; 15 | } 16 | &-title { 17 | background-color: #fff; 18 | padding: 8px; 19 | border-bottom: 1px #eee solid; 20 | } 21 | &-close { 22 | float: right; 23 | cursor: pointer; 24 | } 25 | &-mask { 26 | position: fixed; 27 | top: 0; 28 | bottom: 0; 29 | width: 100%; 30 | } 31 | &-buttons { 32 | height: 50px; 33 | background-color: #fff; 34 | display: flex; 35 | align-items: center; 36 | flex-direction: row-reverse; 37 | & > * { 38 | margin-right: 8px; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "stylelint-config-standard" ], 3 | "plugins": [ 4 | "stylelint-declaration-block-no-ignored-properties" 5 | ], 6 | "rules": { 7 | "plugin/declaration-block-no-ignored-properties": true, 8 | "selector-max-id": 0, 9 | "selector-class-pattern": ["^[a-z\\-]+\\(?", { 10 | "resolveNestedSelectors": true 11 | }], 12 | "rule-empty-line-before": ["always", { 13 | "except": "inside-block", 14 | "ignore": "after-comment" 15 | }], 16 | "selector-pseudo-element-no-unknown": [ 17 | true, 18 | { 19 | "ignorePseudoElements": ["ng-deep"] 20 | } 21 | ], 22 | "no-descending-specificity": null, 23 | "selector-type-no-unknown": null, 24 | "function-name-case": null, 25 | "function-whitespace-after": null 26 | }, 27 | "ignoreFiles": ["src/assets/**/*"] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Fanzicai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "noImplicitAny": false, //强类型检查 5 | "noImplicitThis": true, // 禁止this为any类型 6 | "module": "esnext", //组织代码方式 7 | "target": "ES6", //编译目标平台 8 | "allowJs": true, //允许使用js 9 | "allowSyntheticDefaultImports": true, //允许从没有设置默认导出的模块中默认导入。这并不影响代码的显示,仅为了类型检查。 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "noUnusedLocals": true, 14 | "skipLibCheck": true, 15 | "experimentalDecorators": true, 16 | "strict": false, 17 | "esModuleInterop": true, 18 | "moduleResolution": "node", 19 | "types": ["node", "jquery"], 20 | "jsx": "react", 21 | "jsxFactory": "createVNode", 22 | "lib": [ 23 | "es2015", 24 | "es2016.array.include", 25 | "esnext.array", 26 | "dom" 27 | ], 28 | "rootDir": ".", 29 | "paths": { 30 | "@jeact/*": ["packages/*"], 31 | "jeact": ["packages/jeact"] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const chalk = require('chalk') 3 | 4 | const packagesName = fs.readdirSync('packages').filter(f => { 5 | if (!fs.statSync(`packages/${f}`).isDirectory()) { 6 | return false 7 | } 8 | if (!fs.existsSync(`packages/${f}/package.json`)) { 9 | return false 10 | } 11 | const pkg = require(`../packages/${f}/package.json`) 12 | if (pkg.private && !pkg.buildOptions) { 13 | return false 14 | } 15 | return true 16 | }) 17 | 18 | exports.packagesName = packagesName; 19 | 20 | exports.fuzzyMatchTarget = (partialTargets, includeAllMatching) => { 21 | const matched = [] 22 | partialTargets.forEach(partialTarget => { 23 | for (const target of packagesName) { 24 | if (target.match(partialTarget)) { 25 | matched.push(target) 26 | if (!includeAllMatching) { 27 | break 28 | } 29 | } 30 | } 31 | }) 32 | if (matched.length) { 33 | return matched 34 | } else { 35 | console.log() 36 | console.error( 37 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 38 | `Target ${chalk.underline(partialTargets)} not found!` 39 | )}` 40 | ) 41 | console.log() 42 | 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const execa = require('execa') 2 | const fs = require('fs-extra'); 3 | const path = require('path') 4 | const { fuzzyMatchTarget, packagesName: allTargets } = require('./utils') 5 | const args = require('minimist')(process.argv.slice(2)) 6 | 7 | const targets = args._ 8 | const formats = args.formats || args.f 9 | const buildAllMatching = args.all || args.a 10 | const sourceMap = args.sourcemap || args.s 11 | const type = args.type || args.t 12 | const prod = args.prod || args.p 13 | 14 | 15 | 16 | if (!targets.length) { 17 | buildAll(allTargets) 18 | } else { 19 | buildAll(fuzzyMatchTarget(targets, buildAllMatching)) 20 | } 21 | 22 | 23 | async function buildAll(names) { 24 | for (const packageName of names) { 25 | await build(packageName) 26 | } 27 | } 28 | 29 | 30 | async function build(target) { 31 | execa( 32 | 'rollup', 33 | [ 34 | '-c', 35 | '--environment', 36 | [ 37 | `NODE_ENV:${prod ? 'production' : 'development'}`, 38 | `TARGET:${target}`, 39 | formats ? `FORMATS:${formats}` : ``, 40 | type ? `TYPES:true` : ``, 41 | sourceMap ? `SOURCE_MAP:true` : `` 42 | ] 43 | .filter(Boolean) 44 | .join(',') 45 | ], 46 | { stdio: 'inherit' } 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # jeact 2 | ![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg) 3 | 4 | 在JQuery环境下用MVVM开发复杂组件,仅几kb超小体积。 5 | 6 | 定义一个switch组件,挂载到jquery后,`$('#el').switch()`就可以将#el变成一个开关组件,并且`$('#el').val()`就能获取值(js也能取),放进form也能直接提交,专门解决老旧jquery项目维护难的问题。 7 | 8 | 9 | ## 支持环境 10 | 11 | | Edge | Firefox | Chrome | 12 | | --- | --- | --- | 13 | | last 2 versions | last 2 versions | last 2 versions | 14 | 15 | ## 安装 16 | 17 | npm 18 | ```shell script 19 | npm i jeact --save 20 | ``` 21 | yarn 22 | ```shell script 23 | yarn add jeact 24 | ``` 25 | 或者直接引入 26 | ```html 27 | 28 | ``` 29 | 30 | 31 | ## 使用 32 | 33 | 34 | [在线演示](https://qqabcv520.github.io/jeact/) 35 | 36 | 定义一个表单组件并挂载到jquery 37 | 38 | ```jsx 39 | import { ValueComponent, createVNode } from 'jeact'; 40 | // 定义组件 41 | class SwitchComponent extends ValueComponent { 42 | 43 | // xxx 44 | 45 | render () { 46 | return ( 47 |
48 | {/*xxx*/} 49 |
50 | ); 51 | } 52 | } 53 | // 挂载到jquery,名称为customSwitch 54 | mountComponent({ 55 | name: 'customSwitch', 56 | componentType: SwitchComponent, 57 | }) 58 | ``` 59 | 60 | 直接在HTML中使用刚刚挂载的`customSwitch`组件,初始值为`true` 61 | ```html 62 | 63 | ``` 64 | 65 | 使用jquery初始化`customSwitch`组件 66 | 67 | ```html 68 | 69 | ``` 70 | ```js 71 | $('#switchInput').customSwitch(); // 初始化组件 72 | $('#switchInput').customSwitch('setOpen', true) // 调用组件方法,设置open为true 73 | $('#switchInput').on('change', function() { // 监听事件 74 | console.log($(this).val()); 75 | }) 76 | ``` 77 | 78 | 完整用法看[这里](https://github.com/qqabcv520/jeact/blob/master/packages/jeact/README.MD) 79 | 80 | ## License 81 | 82 | [MIT](http://opensource.org/licenses/MIT) 83 | 84 | Copyright (c) 2018-present, Fanzica 85 | -------------------------------------------------------------------------------- /packages/jeact/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Component, Type } from './component'; 2 | 3 | // 判空 4 | export function isEmpty(value: any) { 5 | return value == null || value.length === 0; 6 | } 7 | 8 | // 驼峰/下划线 转 短横线命名(kebab-case) 9 | export function getKebabCase(str: string): string { 10 | const reg = /^([A-Z$]+)/g; 11 | const reg2 = /_([a-zA-Z$]+)/g; 12 | const reg3 = /([A-Z$]+)/g; 13 | return str 14 | .replace(reg, ($, $1) => $1.toLowerCase()) 15 | .replace(reg2, ($, $1) => '-' + $1.toLowerCase()) 16 | .replace(reg3, ($, $1) => '-' + $1.toLowerCase()); 17 | } 18 | 19 | export interface MountComponentArgs { 20 | name: string; 21 | componentType: Type; 22 | props: string[]; 23 | $?: any; 24 | } 25 | 26 | // 挂载为jquery插件 27 | export function mountComponent({ 28 | name, 29 | componentType, 30 | props, 31 | $ = window['jQuery'], 32 | }: MountComponentArgs) { 33 | if ($ == null) { 34 | return; 35 | } 36 | $.fn[name] = function (...args: any[]) { 37 | if (typeof args[0] === 'string') { 38 | const [propName, ...methodArgs] = args; 39 | const component = this.data(name); 40 | if (!component) { 41 | $.error(`节点不是一个 ${name} 组件`); 42 | } 43 | if (typeof component[propName] === 'function') { 44 | component[propName](...methodArgs); 45 | } else if (methodArgs != null && methodArgs.length === 1) { 46 | return (component[propName] = methodArgs[0]); 47 | } else { 48 | return component[propName]; 49 | } 50 | } else if (args[0] == null || typeof args[0] === 'object') { 51 | const methodArgs = args[0]; 52 | const component = Component.create(componentType, methodArgs, this[0]); 53 | this.data(name, component); 54 | } else { 55 | $.error('第一个参数只能是string或object'); 56 | } 57 | return this; 58 | }; 59 | 60 | $(function () { 61 | $(`[${getKebabCase(name)}]`).each(function (this) { 62 | const $selected = $(this); 63 | const propsValue = (props || []).reduce((pre, curr) => { 64 | pre[curr] = $selected.attr(getKebabCase(curr)); 65 | return pre; 66 | }, {}); 67 | $selected[name](propsValue); 68 | }); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "node scripts/build.js", 9 | "dev": "node scripts/dev.js", 10 | "release": "node scripts/release.js", 11 | "prettier": "prettier --write \"packages/*/src/**/*\"", 12 | "lint": "npm run eslint && npm run stylelint", 13 | "lint:fix": "npm run eslint:fix && npm run stylelint:fix", 14 | "eslint": "eslint \"packages/*/src/**/*.{ts,tsx,js,jsx}\"", 15 | "eslint:fix": "eslint \"packages/*/src/**/*.{ts,tsx,js,jsx}\" --fix", 16 | "stylelint": "stylelint \"packages/*/src/**/*.less\" --syntax less", 17 | "stylelint:fix": "stylelint \"packages/*/src/**/*.less\" --syntax less --fix", 18 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 19 | }, 20 | "author": "fanzicai", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@commitlint/cli": "^8.2.0", 24 | "@commitlint/config-conventional": "^8.2.0", 25 | "@rollup/plugin-commonjs": "^13.0.0", 26 | "@rollup/plugin-json": "^4.1.0", 27 | "@rollup/plugin-node-resolve": "^8.1.0", 28 | "@rollup/plugin-replace": "^2.3.3", 29 | "@types/core-js": "^2.5.1", 30 | "@types/jquery": "^3.3.38", 31 | "@types/node": "^10.12.18", 32 | "@umijs/fabric": "^2.4.11", 33 | "autoprefixer": "^9.8.4", 34 | "chalk": "^4.1.0", 35 | "codelyzer": "^6.0.0", 36 | "conventional-changelog-cli": "^2.0.31", 37 | "cross-env": "^7.0.2", 38 | "cssnano": "^4.1.10", 39 | "enquirer": "^2.3.5", 40 | "execa": "^4.0.2", 41 | "fs-extra": "^9.0.1", 42 | "husky": "^4.2.5", 43 | "less": "^3.11.3", 44 | "optimize-css-assets-webpack-plugin": "^5.0.1", 45 | "prettier": "^2.2.1", 46 | "rollup": "^2.18.1", 47 | "rollup-plugin-postcss": "^3.1.2", 48 | "rollup-plugin-terser": "^6.1.0", 49 | "rollup-plugin-typescript2": "^0.27.1", 50 | "semver": "^7.3.2", 51 | "stylelint": "^13.6.1", 52 | "stylelint-config-standard": "^19.0.0", 53 | "stylelint-declaration-block-no-ignored-properties": "^2.3.0", 54 | "tslint": "~6.1.2", 55 | "typescript": "^3.9.3" 56 | }, 57 | "dependencies": {}, 58 | "browserslist": [ 59 | "last 2 Chrome versions", 60 | "last 2 Edge versions", 61 | "last 2 Firefox versions" 62 | ], 63 | "lint-staged": { 64 | "packages/*/src/**/*.ts": [ 65 | "npm run lint:ts", 66 | "git add" 67 | ], 68 | "packages/*/src/**/*.less": [ 69 | "npm run lint:style", 70 | "git add" 71 | ] 72 | }, 73 | "commitlint": { 74 | "extends": [ 75 | "@commitlint/config-conventional" 76 | ] 77 | }, 78 | "husky": { 79 | "hooks": { 80 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import typescript from 'rollup-plugin-typescript2' 3 | import json from '@rollup/plugin-json' 4 | import { terser } from "rollup-plugin-terser"; 5 | import postcss from 'rollup-plugin-postcss' 6 | import autoprefixer from 'autoprefixer' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import { nodeResolve } from '@rollup/plugin-node-resolve' 9 | 10 | if (!process.env.TARGET) { 11 | throw new Error('TARGET package must be specified via --environment flag.') 12 | } 13 | 14 | const packagesDir = path.resolve(__dirname, 'packages') 15 | const packageDir = path.resolve(packagesDir, process.env.TARGET) 16 | const name = path.basename(packageDir) 17 | const resolve = p => path.resolve(packageDir, p) 18 | const pkg = require(resolve(`package.json`)) 19 | 20 | // 对多个formats,只执行一次检查 21 | let hasTSChecked = false 22 | 23 | const defaultFormats = ['es', 'iife'] 24 | const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',') 25 | const packageFormats = inlineFormats || defaultFormats 26 | 27 | const enableProd = process.env.NODE_ENV === 'production' 28 | const enableSourceMap = !!process.env.SOURCE_MAP 29 | const enableType = !!process.env.TYPES 30 | 31 | 32 | export default packageFormats.map(format => createConfig(format)) 33 | 34 | function createConfig(format) { 35 | 36 | const output = { 37 | file: resolve(`dist/${name}.${format}.js`), 38 | sourcemap: enableSourceMap, 39 | externalLiveBindings: false, 40 | format, 41 | name: getUpperCamelCase(name), 42 | } 43 | 44 | const isBrowser = ['umd', 'iife', 'amd', 'system'].includes(format) 45 | 46 | const shouldEmitDeclarations = enableType && !hasTSChecked 47 | 48 | hasTSChecked = true 49 | 50 | const external = [...Object.keys(pkg.peerDependencies || {})] 51 | if (!isBrowser) { 52 | external.push(...Object.keys(pkg.dependencies || {})) 53 | } 54 | 55 | const plugins = [ 56 | json({ 57 | namedExports: false 58 | }), 59 | typescript({ 60 | check: enableProd && !hasTSChecked, 61 | tsconfig: path.resolve(__dirname, 'tsconfig.json'), 62 | cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'), 63 | tsconfigOverride: { 64 | compilerOptions: { 65 | sourceMap: enableSourceMap, 66 | declaration: shouldEmitDeclarations, 67 | }, 68 | include: [ 69 | `./packages/${name}/src/**/*` 70 | ] 71 | }, 72 | useTsconfigDeclarationDir: true, 73 | }), 74 | postcss({ 75 | plugins: [autoprefixer], 76 | extract: true, 77 | sourceMap: enableSourceMap, 78 | minimize: enableProd 79 | }), 80 | commonjs({ 81 | sourceMap: false 82 | }), 83 | nodeResolve({ 84 | preferBuiltins: true 85 | }), 86 | ]; 87 | if (enableProd) { 88 | plugins.push( 89 | terser({ 90 | module: format === 'es', 91 | compress: { 92 | ecma: 2015, 93 | pure_getters: true 94 | } 95 | }) 96 | ); 97 | } 98 | return { 99 | input: resolve('index.ts'), 100 | plugins, 101 | external, 102 | output, 103 | } 104 | } 105 | 106 | 107 | /** 108 | * 短横线/下划线/小驼峰 转 大驼峰命名(UpperCamelCase) 109 | */ 110 | export function getUpperCamelCase(str) { 111 | const reg = /(^|-|_)(\w)/g; 112 | return str.replace(reg, ($, $1, $2) => $2.toUpperCase()); 113 | } 114 | -------------------------------------------------------------------------------- /packages/jeact/src/v-node.ts: -------------------------------------------------------------------------------- 1 | import { Component, FunctionComponent, Type } from './component'; 2 | 3 | export abstract class VNode { 4 | el: Node; 5 | key: any; 6 | } 7 | 8 | export class VElement extends VNode { 9 | el: HTMLElement; 10 | key: any; 11 | ref: string; 12 | constructor( 13 | public type: string | Function | Type, 14 | public attributes: { [key: string]: any } = {}, 15 | public handles: { [key: string]: () => void } = {}, 16 | public children: VNode[] = [], 17 | ) { 18 | super(); 19 | } 20 | } 21 | 22 | export interface VFunction { 23 | el: Node; 24 | key: any; 25 | ref: string; 26 | type: () => VNode; 27 | component: FunctionComponent; 28 | attributes: { [key: string]: any }; 29 | handles: { [key: string]: () => void }; 30 | children: VNode[]; 31 | } 32 | 33 | export interface VDom { 34 | el: HTMLElement; 35 | key: any; 36 | ref: string; 37 | type: string; 38 | attributes: { [key: string]: any }; 39 | handles: { [key: string]: () => void }; 40 | children: VNode[]; 41 | } 42 | 43 | export interface VComponent { 44 | el: Node; 45 | key: any; 46 | ref: string; 47 | type: Type; 48 | component: T; 49 | attributes: { [key: string]: any }; 50 | handles: { [key: string]: () => void }; 51 | children: VNode[]; 52 | } 53 | 54 | export class VText extends VNode { 55 | el: Text; 56 | content: string; 57 | key: any; 58 | constructor(content: any) { 59 | super(); 60 | this.content = String(content || content === 0 ? content : ''); 61 | } 62 | } 63 | 64 | export function createVNode( 65 | type: string | Function | Type, 66 | props: { [key: string]: any }, 67 | ...children: any[] 68 | ) { 69 | let handle = {}; 70 | let attribute = {}; 71 | if (props) { 72 | handle = Object.keys(props) 73 | .filter((value) => value.startsWith('on')) 74 | .reduce((pre, curr) => { 75 | pre[curr] = props[curr]; 76 | return pre; 77 | }, {}); 78 | attribute = Object.keys(props) 79 | .filter((value) => !value.startsWith('on')) 80 | .reduce((pre, curr) => { 81 | pre[curr] = props[curr]; 82 | return pre; 83 | }, {}); 84 | } 85 | const vNodeChildren = children.flat(2).map((value) => { 86 | return isVElement(value) ? value : new VText(value); 87 | }); 88 | return new VElement(type, attribute, handle, vNodeChildren); 89 | } 90 | 91 | export function isVNode(vNode: any): vNode is VNode { 92 | return vNode instanceof VNode; 93 | } 94 | 95 | export function isVElement(vNode: VNode): vNode is VElement { 96 | return vNode && (vNode).type != null; 97 | } 98 | 99 | export function isVText(vNode: VNode): vNode is VText { 100 | return vNode && (vNode).content !== undefined; 101 | } 102 | 103 | export function isVDom(vNode: VNode): vNode is VDom { 104 | return isVElement(vNode) && typeof vNode.type === 'string'; 105 | } 106 | 107 | export function isVComponent(vNode: VNode): vNode is VComponent { 108 | return ( 109 | isVElement(vNode) && 110 | typeof vNode.type === 'function' && 111 | vNode.type.prototype && 112 | vNode.type.prototype.render 113 | ); 114 | } 115 | 116 | export function isVFunction(vNode: VNode): vNode is VFunction { 117 | return ( 118 | isVElement(vNode) && 119 | typeof vNode.type === 'function' && 120 | !(vNode.type.prototype && vNode.type.prototype.render) 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /packages/jeact-components/src/modal/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | import type { Type, VNode, ComponentProps } from 'jeact'; 3 | import { Component, isVNode, createVNode } from 'jeact'; 4 | 5 | export type ModalComponentProps = { 6 | content: Type; 7 | width: string; 8 | contentProps: any; 9 | } & ComponentProps 10 | 11 | export class ModalComponent extends Component { 12 | content: Type | (() => VNode); 13 | style: string; 14 | contentProps: Partial; 15 | 16 | maskCloseable: boolean; 17 | title: VNode | string; 18 | 19 | buttons: VNode | VNode[]; 20 | onCancel: (instance: T) => void; 21 | onOk: (instance: T) => Promise | boolean | void; 22 | 23 | constructor(args: ModalComponentProps) { 24 | super(args); 25 | } 26 | 27 | readonly maskClick = (e: any) => { 28 | if (e.target === this.refs.modal && this.maskCloseable) { 29 | this.dom.removeChild(this.el, this.vNode.el); 30 | } 31 | }; 32 | 33 | readonly cancelClick = () => { 34 | this.close(); 35 | }; 36 | 37 | readonly closeClick = () => { 38 | this.close(); 39 | }; 40 | 41 | readonly onOkClick = async () => { 42 | if (this.onOk) { 43 | let result: boolean | void; 44 | try { 45 | result = await this.onOk(this.refs.instance as T); 46 | } catch (e) {} 47 | if (result !== false) { 48 | this.close(); 49 | } 50 | } 51 | }; 52 | 53 | close() { 54 | this.dom.removeChild(this.el, this.vNode.el); 55 | if (this.onCancel) { 56 | this.onCancel(this.refs.instance as T); 57 | } 58 | } 59 | 60 | render() { 61 | const Title = () => { 62 | if (typeof this.title === 'string') { 63 | return ( 64 |
65 | {this.title || '未命名'} 66 | 67 |
68 | ); 69 | } if (isVNode(this.title)) { 70 | return this.title; 71 | } 72 | }; 73 | const Content = this.content; 74 | const Buttons = () => { 75 | return this.buttons !== undefined ? ( 76 | this.buttons 77 | ) : ( 78 |
79 | 82 | 85 |
86 | ); 87 | }; 88 | return ( 89 |
90 |
91 | {Title && } 92 | {Content && <Content ref="instance" {...this.contentProps} />} 93 | {Buttons && <Buttons />} 94 | </div> 95 | </div> 96 | ); 97 | } 98 | } 99 | 100 | export function mountModal<T extends Component>(args: MountModalArgs<T>) { 101 | const { name, $ = jQuery, ...restProp } = args; 102 | $[name] = function (contentProps: Partial<T>) { 103 | return Component.create<ModalComponent<T>>( 104 | ModalComponent, 105 | { 106 | ...restProp, 107 | ...contentProps, 108 | }, 109 | document.body, 110 | ); 111 | }; 112 | } 113 | 114 | export type MountModalArgs<T extends Component> = { 115 | name: string; 116 | title: string; 117 | $?: JQueryStatic; 118 | } & Partial<ModalComponent<T>> 119 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title>Title 6 | 7 | 8 | 简单例子: 9 |
10 |
11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 | 挂载为jquery组件,可直接获取value: 20 |
21 |
22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 143 | 144 | -------------------------------------------------------------------------------- /packages/jeact/README.MD: -------------------------------------------------------------------------------- 1 | # jeact 2 | ![GitHub](https://img.shields.io/github/license/mashape/apistatus.svg) 3 | 4 | 在JQuery环境下用MVVM开发复杂组件,仅几kb超小体积。 5 | 6 | ## 支持环境 7 | 8 | | Edge | Firefox | Chrome | 9 | | --- | --- | --- | 10 | | last 2 versions | last 2 versions | last 2 versions | 11 | 12 | ## 安装 13 | 14 | npm 15 | ```shell script 16 | npm i jeact --save 17 | ``` 18 | yarn 19 | ```shell script 20 | yarn add jeact 21 | ``` 22 | 或者直接引入 23 | ```html 24 | 25 | ``` 26 | 27 | 28 | ## 使用 29 | 30 | 31 | [在线演示](https://qqabcv520.github.io/jeact/) 32 | 33 | js创建组件 34 | 35 | ```js 36 | 37 | // 定义functional组件 38 | function Counter({count}) { 39 | return el('span', {}, 40 | el('span', {}, ' 点击了:'), 41 | el('span', {}, count), 42 | el('span', {}, '次') 43 | ) 44 | } 45 | 46 | // 定义class组件 47 | class TestComponent extends Component { 48 | constructor (props) { 49 | super(props); 50 | this.count = 0; 51 | } 52 | buttonClick = () => { 53 | this.count++; 54 | this.update(); 55 | } 56 | render () { 57 | return el('div', {}, 58 | el('button', {onclick: this.buttonClick}, '按钮'), 59 | el(Counter, {count: this.count}), 60 | ) 61 | } 62 | } 63 | 64 | // 挂载到ID为el的节点上 65 | const vm = Component.create(TestComponent, {}, '#el') 66 | ``` 67 | 68 | jsx创建组件 69 | 70 | ```jsx 71 | import { Component, createVNode } from 'jeact'; 72 | 73 | class TestComponent extends Component { 74 | constructor (props) { 75 | super(props); 76 | this.count = 0; 77 | } 78 | buttonClick = () => { 79 | this.count++; 80 | this.update(); 81 | } 82 | render () { 83 | return ( 84 |
85 | 86 | 87 |
88 | ) 89 | } 90 | } 91 | ``` 92 | 93 | 定义一个表单组件并挂载到jquery 94 | 95 | ```jsx 96 | import { ValueComponent, createVNode } from 'jeact'; 97 | 98 | class SwitchComponent extends ValueComponent { 99 | 100 | constructor (props) { 101 | super(props); 102 | this.open = false; 103 | } 104 | 105 | setOpen(val) { 106 | this.open = val; 107 | this.onChange(this.open); 108 | this.update(); // 当data变化之后,需要调用update方法,将该组件加到更新队列中 109 | } 110 | // 必须实现这个抽象方法,用于将input的value和组件的vm绑定 111 | writeValue(value) { 112 | this.open = value && value !== 'false'; 113 | } 114 | 115 | buttonClick = () => { 116 | this.setOpen(!this.open); 117 | } 118 | render () { 119 | const switchWrapperStyle = { 120 | userSelect: 'none', 121 | cursor: 'pointer', 122 | border: '1px solid #ccc', 123 | display: 'inline-block', 124 | borderRadius: '10px', 125 | height: '20px', 126 | width: '35px', 127 | } 128 | const switchHandlerStyle = { 129 | width: '20px', 130 | height: '100%', 131 | backgroundColor: '#3587ff', 132 | borderRadius: '10px', 133 | transition: 'margin 0.15s, background-color 0.3s' 134 | } 135 | if (this.open) { 136 | switchHandlerStyle.marginLeft = '15px'; 137 | switchHandlerStyle.backgroundColor = '#3587ff'; 138 | } else { 139 | switchHandlerStyle.marginLeft = '0'; 140 | switchHandlerStyle.backgroundColor = '#999'; 141 | } 142 | return ( 143 |
144 |
145 |
146 | ); 147 | } 148 | } 149 | // 挂载到jquery,名称为customSwitch 150 | mountComponent({ 151 | name: 'customSwitch', 152 | componentType: SwitchComponent, 153 | }) 154 | ``` 155 | 156 | 直接在HTML中使用刚刚挂载的`customSwitch`组件,初始值为`true` 157 | ```html 158 | 159 | ``` 160 | 161 | 使用jquery初始化`customSwitch`组件 162 | 163 | ```html 164 | 165 | ``` 166 | ```js 167 | $('#switchInput').customSwitch(); // 初始化组件 168 | $('#switchInput').customSwitch('setOpen', true) // 调用组件方法,设置open为true 169 | $('#switchInput').on('change', function() { // 监听事件 170 | console.log($(this).val()); 171 | }) 172 | ``` 173 | -------------------------------------------------------------------------------- /packages/jeact/src/component.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from './v-node'; 2 | import { DomOperate } from './dom'; 3 | import { Differentiator } from './diff'; 4 | 5 | export type Type = new (...args: any[]) => T; 6 | 7 | export interface ComponentProps { 8 | el?: HTMLElement; 9 | children?: VNode[]; 10 | rootUpdate: () => void; 11 | } 12 | 13 | export interface ValueComponentProps extends ComponentProps { 14 | valueChange: (options: T) => void; 15 | } 16 | 17 | export abstract class Component { 18 | private updateFlag = false; 19 | protected readonly dom = new DomOperate(this); 20 | protected readonly diff = new Differentiator(this.dom); 21 | el: HTMLElement; 22 | readonly refs: { 23 | [key: string]: Element | Component; 24 | } = {}; 25 | 26 | readonly rootUpdate?: () => void; 27 | children?: VNode[]; 28 | vNode: VNode | null; 29 | 30 | constructor(args: ComponentProps) { 31 | if (args) { 32 | Object.assign(this, args); 33 | } 34 | } 35 | 36 | static create( 37 | componentType: Type, 38 | props: Partial, 39 | el?: HTMLElement | string, 40 | ): T { 41 | const dom = typeof el === 'string' ? document.querySelector(el) : el; 42 | const component = new componentType({ ...props }); 43 | component.el = dom as HTMLElement; 44 | component.beforeMount(); 45 | component.mount(); 46 | component.mounted(); 47 | return component; 48 | } 49 | 50 | protected mount() { 51 | this.vNode = this.render(); 52 | const node = this.dom.createElement(this.vNode, this.update.bind(this)); 53 | this.appendToEl(node); 54 | } 55 | 56 | appendToEl(node: Node) { 57 | if (this.el && node) { 58 | this.dom.appendChild(this.el, node); 59 | } 60 | } 61 | 62 | reappendToEl(oldNode: Node, newNode: Node) { 63 | if (oldNode === newNode || this.el == null) { 64 | return; 65 | } 66 | const parentNode = this.dom.parentNode(oldNode); 67 | if (parentNode == null) { 68 | return; 69 | } 70 | this.dom.removeChild(parentNode, oldNode); 71 | this.appendToEl(newNode); 72 | } 73 | 74 | update() { 75 | if (this.updateFlag) { 76 | return; 77 | } 78 | this.updateFlag = true; 79 | Promise.resolve().then(() => { 80 | this.updateFlag = false; 81 | if (this.rootUpdate) { 82 | this.rootUpdate(); 83 | return; 84 | } 85 | this.runDiff(); 86 | }); 87 | } 88 | 89 | runDiff() { 90 | if (this.vNode == null || this.vNode.el == null) { 91 | return null; 92 | } 93 | const newVNode = this.render(); 94 | this.diff.patch(this.vNode, newVNode, this.update.bind(this)); 95 | this.reappendToEl(this.vNode.el, newVNode.el); 96 | this.vNode = newVNode; 97 | return newVNode; 98 | } 99 | 100 | beforeMount() {} 101 | 102 | mounted() {} 103 | 104 | beforeUpdate() {} 105 | 106 | updated() {} 107 | 108 | destroy() {} 109 | 110 | render(): VNode | null { 111 | return null; 112 | } 113 | } 114 | 115 | export abstract class ValueComponent extends Component { 116 | readonly el: HTMLInputElement; 117 | protected valueChange: (options: T) => void; 118 | 119 | abstract writeValue(value: string): void; 120 | 121 | protected constructor(props: ValueComponentProps) { 122 | super(props); 123 | this.valueChange = props.valueChange; 124 | } 125 | 126 | mount() { 127 | if (this.el) { 128 | this.writeValue(this.el.value); 129 | } 130 | super.mount(); 131 | } 132 | 133 | readValue(value: any): string { 134 | return value != null ? String(value) : ''; 135 | } 136 | 137 | onChange(value: T) { 138 | if (this.valueChange) { 139 | this.valueChange(value); 140 | } 141 | if (this.el) { 142 | this.el.value = this.readValue(value); 143 | this.el.dispatchEvent(new InputEvent('input')); 144 | this.el.dispatchEvent(new UIEvent('change')); 145 | } 146 | } 147 | 148 | appendToEl(node: HTMLElement) { 149 | const parentNode = this.el && this.dom.parentNode(this.el); 150 | if (parentNode && node) { 151 | this.dom.insertBefore(parentNode, node, this.el); 152 | this.el.hidden = true; 153 | } 154 | } 155 | } 156 | 157 | export class FunctionComponent extends Component { 158 | functionProps: T; 159 | renderFunction: (T) => VNode; 160 | 161 | render(): VNode { 162 | return this.renderFunction(this.functionProps); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.2.0](https://github.com/qqabcv520/jeact/compare/v0.1.1...v0.2.0) (2020-08-08) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * lint代码修复 ([5d5b93c](https://github.com/qqabcv520/jeact/commit/5d5b93c2b6cc58483432e6e00836f1ab99709044)) 7 | * 修复ref对component无效的问题 ([0379293](https://github.com/qqabcv520/jeact/commit/037929325068faeb1152a6b08c79dcba0c4ebb0d)) 8 | * 修复ValueComponent作为子组件使用的问题 ([ba3c1bf](https://github.com/qqabcv520/jeact/commit/ba3c1bf809418de00f78eb87af10b7ba9d97bdcc)) 9 | * 修复挂载顺序导致的ValueComponent初始值无效的问题 ([786e370](https://github.com/qqabcv520/jeact/commit/786e37095d6a2c7cfda401fc73c18aa605262228)) 10 | 11 | 12 | ### Performance Improvements 13 | 14 | * release.js自动更新monorepo依赖 ([fd8b250](https://github.com/qqabcv520/jeact/commit/fd8b25049afae3374b334bfe139eeca9850a329e)) 15 | * 图片选择控件添加限制属性 ([f72f3c3](https://github.com/qqabcv520/jeact/commit/f72f3c3786a48e7c901969c6f0133110fc2cda21)) 16 | * 图片选择控件限制触发弹窗 ([c71f6ed](https://github.com/qqabcv520/jeact/commit/c71f6edf265e67cebed2fe278a9ad52244a02649)) 17 | * 完善图片选择控件 ([624e249](https://github.com/qqabcv520/jeact/commit/624e249ce34bdf7b8f57b181b732bedacd068fb3)) 18 | 19 | 20 | 21 | ## [0.1.1](https://github.com/qqabcv520/jeact/compare/v0.1.0...v0.1.1) (2020-06-27) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * 修复函数式组件更新问题 ([a98ce8e](https://github.com/qqabcv520/jeact/commit/a98ce8e5fe4b9b6c4c2fd1821586b5fa126ebfa9)) 27 | * 修复函数式组件更新问题 ([4ac8a66](https://github.com/qqabcv520/jeact/commit/4ac8a664c90f8331f4cbad6260b858b8ba005043)) 28 | 29 | 30 | 31 | # [0.1.0](https://github.com/qqabcv520/jeact/compare/0.1.0...v0.1.0) (2020-06-17) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * popup关闭事件冒泡被拦截,改为捕获阶段 ([b463e8c](https://github.com/qqabcv520/jeact/commit/b463e8cc3ed51798b5c8b29ab8b62a8727d386ba)) 37 | * 修复option初始化parent字段设置错误 ([70b5845](https://github.com/qqabcv520/jeact/commit/70b5845f40722c22869930edc02eba6d93788c82)) 38 | * 多选级联组件bug修复 ([fce1a57](https://github.com/qqabcv520/jeact/commit/fce1a57cc22eade8d935dc2ef303c4dc95be5cfa)) 39 | * 多选级联组件bug修复 ([e6832fd](https://github.com/qqabcv520/jeact/commit/e6832fd7cdf72ffbf366276dc4494649c4affcbd)) 40 | * 样式前缀修复 ([e179009](https://github.com/qqabcv520/jeact/commit/e17900921580bd6383ea79b25eb2ddeee7dbdf1d)) 41 | * 经理控件bug修复 ([9d493b8](https://github.com/qqabcv520/jeact/commit/9d493b82a81e1f8c868329d8119b24c08053ce3c)) 42 | 43 | 44 | ### Features 45 | 46 | * mvvm初始化 ([37f24e6](https://github.com/qqabcv520/jeact/commit/37f24e6866a2b79f1fbbee563a9ef7dabc0c55f6)) 47 | * mvvm初始化,基础功能 ([39f7b79](https://github.com/qqabcv520/jeact/commit/39f7b79c97b1d5d4064e6e231f8e3f159cd2a96b)) 48 | * mvvm基础功能 ([2f79c7f](https://github.com/qqabcv520/jeact/commit/2f79c7fe3fb40f453943ffa644976606a3ee705d)) 49 | * mvvm基础功能 ([85f5826](https://github.com/qqabcv520/jeact/commit/85f5826994a840565d79636177fd49d62d58a1b5)) 50 | * mvvm基础功能 ([66afa73](https://github.com/qqabcv520/jeact/commit/66afa732e5b03f0c11965cf2fcbe6ef219ad86d5)) 51 | * mvvm基础功能 ([de60862](https://github.com/qqabcv520/jeact/commit/de608622ef2c5009978ec3a5a3e08931c8ac3ae0)) 52 | * mvvm基础功能 ([c38cae9](https://github.com/qqabcv520/jeact/commit/c38cae952878aeb21e3d17018bb57c616bd42174)) 53 | * 多选级联组件 ([cb77571](https://github.com/qqabcv520/jeact/commit/cb77571d8c5a1c4ebc7492229561f2291fffb189)) 54 | * 多选级联组件 ([7d35591](https://github.com/qqabcv520/jeact/commit/7d3559146a548c7c1c932f4e1517352786f5a73d)) 55 | * 多选级联组件 ([073afaa](https://github.com/qqabcv520/jeact/commit/073afaa239cc376af9e9650216eac3c404a2fda1)) 56 | * 多选级联组件 ([f481f62](https://github.com/qqabcv520/jeact/commit/f481f6274ae51cebe99f7ad9d5fc4cc559083261)) 57 | * 常用选择添加cacheName ([3c8e062](https://github.com/qqabcv520/jeact/commit/3c8e0626921feb65acb0e68c352de59d89e49830)) 58 | * 常用选择添加cacheName ([6facc3c](https://github.com/qqabcv520/jeact/commit/6facc3c30264cb3bf4665264b15771ebc0aba4a3)) 59 | * 新增single-cascader组件 ([01117ea](https://github.com/qqabcv520/jeact/commit/01117ea9f2d80e83737555499fc1cca0c7bcac65)) 60 | * 新增single-cascader组件 ([95c2f53](https://github.com/qqabcv520/jeact/commit/95c2f5345f71a5ec7edddb833da67c37ae27289d)) 61 | * 新增single-cascader组件 ([7d905fc](https://github.com/qqabcv520/jeact/commit/7d905fce124ca882bc6643fb4275fd98fd21524b)) 62 | * 级联组件 ([d0c770c](https://github.com/qqabcv520/jeact/commit/d0c770caaff572771753c9fc2ca53bc2e0fad2f7)) 63 | * 级联组件 ([0b71469](https://github.com/qqabcv520/jeact/commit/0b714694fd87d2ce4438c67d360588830f5e6f73)) 64 | * 级联组件 ([3d6a68f](https://github.com/qqabcv520/jeact/commit/3d6a68f3df9bf494a222342a4faa75eb7dea5ac6)) 65 | * 级联组件bug修复 ([f4f075f](https://github.com/qqabcv520/jeact/commit/f4f075fafbb4102c63ce70639638ae9e996ad6a7)) 66 | * 级联组件优化 ([8c51a16](https://github.com/qqabcv520/jeact/commit/8c51a168f6632d8086d65bb2c6c4ee1550f3c443)) 67 | * 级联组件问题修复 ([07bdfff](https://github.com/qqabcv520/jeact/commit/07bdfffabe2ad423a78593b1397721aab8708311)) 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /packages/jeact-components/src/pm-selector/index.less: -------------------------------------------------------------------------------- 1 | .ps-selector { 2 | display: inline-block; 3 | width: 150px; 4 | position: relative; 5 | .ps-label { 6 | display: inline-flex; 7 | align-items: center; 8 | margin: 8px 0; 9 | } 10 | input[type='checkbox'].ps-checkbox { 11 | margin: 4px; 12 | position: relative; 13 | width: 14px; 14 | height: 14px; 15 | cursor: pointer; 16 | } 17 | input[type='checkbox'].ps-checkbox::before { 18 | content: ''; 19 | line-height: 14px; 20 | font-size: 10px; 21 | color: #fff; 22 | background-color: #fff; 23 | display: inline-block; 24 | width: 14px; 25 | height: 14px; 26 | border: 1px #ccc solid; 27 | border-radius: 2px; 28 | vertical-align: top; 29 | } 30 | input[type='checkbox'].ps-checkbox:checked::before { 31 | background-color: #2599ab; 32 | border-color: transparent; 33 | } 34 | input[type='checkbox'].ps-checkbox::after { 35 | content: ''; 36 | } 37 | input[type='checkbox'].ps-checkbox:checked::after { 38 | position: absolute; 39 | top: 4px; 40 | left: 3px; 41 | width: 8px; 42 | height: 5px; 43 | border-left: 2px solid #fff; 44 | border-bottom: 2px solid #fff; 45 | transform: rotateZ(-45deg); 46 | } 47 | .ps-close { 48 | font-family: '黑体', serif; 49 | font-weight: bold; 50 | cursor: pointer; 51 | color: #666; 52 | } 53 | .ps-close:hover { 54 | color: #888; 55 | } 56 | .ps-close:active { 57 | color: #333; 58 | } 59 | &::-webkit-scrollbar { 60 | width: 8px; 61 | height: 8px; 62 | } 63 | &::-webkit-scrollbar-button { 64 | width: 0; 65 | height: 0; 66 | } 67 | &::-webkit-scrollbar-thumb { 68 | background: #e1e1e1; 69 | border: none; 70 | border-radius: 0; 71 | } 72 | &::-webkit-scrollbar-thumb:hover { 73 | background: #ccc; 74 | } 75 | &::-webkit-scrollbar-thumb:active { 76 | background: #999; 77 | } 78 | &::-webkit-scrollbar-track { 79 | border: 0 none #fff; 80 | border-radius: 0; 81 | } 82 | &::-webkit-scrollbar-track:hover { 83 | background: #eee; 84 | } 85 | &::-webkit-scrollbar-corner { 86 | background: transparent; 87 | } 88 | .ps-input.form-control[readonly] { 89 | background-color: #fff; 90 | box-shadow: none; 91 | cursor: pointer; 92 | } 93 | .ps-popup { 94 | font-size: 12px; 95 | top: 30px; 96 | left: 0; 97 | position: absolute; 98 | border: 1px #ddd solid; 99 | width: 724px; 100 | z-index: 1000; 101 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 102 | } 103 | .ps-search-bar { 104 | background-color: #fff; 105 | padding: 8px; 106 | display: flex; 107 | justify-content: flex-end; 108 | } 109 | .ps-search-input { 110 | width: 300px; 111 | margin-right: 8px; 112 | position: relative; 113 | } 114 | .ps-search-input > input.form-control { 115 | width: 100%; 116 | } 117 | .ps-commonly-used { 118 | background-color: #eee; 119 | padding: 4px 0; 120 | } 121 | .ps-commonly-used-options { 122 | display: flex; 123 | flex-wrap: wrap; 124 | min-height: 60px; 125 | } 126 | .ps-commonly-used-option { 127 | margin-top: 0; 128 | font-weight: normal; 129 | width: 20%; 130 | white-space: nowrap; 131 | overflow: hidden; 132 | text-overflow: ellipsis; 133 | } 134 | .ps-options { 135 | display: flex; 136 | height: 250px; 137 | width: 100%; 138 | overflow: auto; 139 | background-color: #fff; 140 | } 141 | .ps-column { 142 | border-right: 1px #eee solid; 143 | flex-shrink: 0; 144 | width: 120px; 145 | overflow: auto; 146 | } 147 | .ps-option { 148 | margin: 1px 0; 149 | padding: 4px 0; 150 | cursor: pointer; 151 | position: relative; 152 | display: flex; 153 | align-items: center; 154 | } 155 | .ps-option:hover { 156 | background-color: #eee; 157 | } 158 | .ps-option-selected { 159 | background-color: #eee; 160 | } 161 | .ps-option.ps-option-next { 162 | padding-right: 20px; 163 | } 164 | .ps-option.ps-option-next::after { 165 | position: absolute; 166 | right: 0; 167 | top: 10px; 168 | content: ''; 169 | width: 14px; 170 | height: 10px; 171 | border: 5px transparent solid; 172 | border-left: 7px #999 solid; 173 | } 174 | .ps-option-text { 175 | flex-grow: 1; 176 | white-space: nowrap; 177 | overflow: hidden; 178 | text-overflow: ellipsis; 179 | } 180 | .ps-selected-cnt { 181 | padding: 6px 6px 0; 182 | background-color: #fff; 183 | } 184 | .ps-selected-label { 185 | font-weight: bold; 186 | margin-right: 5px; 187 | } 188 | .ps-selected-tags { 189 | display: flex; 190 | flex-wrap: wrap; 191 | padding: 4px; 192 | max-height: 300px; 193 | overflow: auto; 194 | background-color: #fff; 195 | } 196 | .ps-selected-tag { 197 | font-size: 13px; 198 | padding: 3px; 199 | width: auto; 200 | border: 1px #ccc solid; 201 | margin: 4px; 202 | background-color: #eee; 203 | color: #555; 204 | border-radius: 2px; 205 | } 206 | .ps-search-popup { 207 | position: absolute; 208 | background-color: #fff; 209 | top: 30px; 210 | width: 100%; 211 | max-height: 380px; 212 | overflow: auto; 213 | border: 1px #eee solid; 214 | padding: 4px; 215 | z-index: 200; 216 | } 217 | .ps-search-options { 218 | display: flex; 219 | flex-wrap: wrap; 220 | min-height: 60px; 221 | } 222 | .ps-search-option { 223 | font-size: 12px; 224 | margin-top: 0; 225 | font-weight: normal; 226 | width: 33.333333%; 227 | white-space: nowrap; 228 | overflow: hidden; 229 | text-overflow: ellipsis; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /packages/jeact-components/src/picture-selector/index.less: -------------------------------------------------------------------------------- 1 | // 禁止选中 2 | .no-select { 3 | -webkit-user-select: none; 4 | -moz-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | .bgx-picture-selector { 10 | width: 1000px; 11 | background-color: #fff; 12 | .bgx-pic-tabs { 13 | display: flex; 14 | padding: 6px 4px 0 4px; 15 | border-bottom: 1px solid #ddd; 16 | overflow-x: auto; 17 | overflow-y: hidden; 18 | .bgx-pic-tab { 19 | position: relative; 20 | padding: 8px 16px; 21 | border: 1px solid #ddd; 22 | border-bottom-color: transparent; 23 | border-top-left-radius: 4px; 24 | border-top-right-radius: 4px; 25 | margin-left: 2px; 26 | margin-right: 2px; 27 | margin-bottom: -1px; 28 | cursor: pointer; 29 | background-color: #eee; 30 | .no-select; 31 | } 32 | .bgx-pic-tab-selected { 33 | border-bottom-color: #fff; 34 | background-color: #fff; 35 | } 36 | } 37 | .bgx-pic-selected { 38 | &-images { 39 | height: 160px; 40 | padding: 8px 0 8px 8px; 41 | overflow-y: scroll; 42 | display: flex; 43 | flex-wrap: wrap; 44 | } 45 | &-operation { 46 | position: absolute; 47 | left: 0; 48 | right: 0; 49 | top: 0; 50 | bottom: 0; 51 | opacity: 0; 52 | text-align: center; 53 | background-color: rgba(0, 0, 0, 0.5); 54 | transition: opacity 0.2s; 55 | &:hover { 56 | opacity: 1; 57 | } 58 | } 59 | &-icon { 60 | color: #fff; 61 | cursor: pointer; 62 | line-height: 110px; 63 | font-size: 18px; 64 | } 65 | &-close { 66 | cursor: pointer; 67 | position: absolute; 68 | right: -8px; 69 | top: -8px; 70 | color: #ff4d4d; 71 | font-size: 18px; 72 | } 73 | &-option-wrapper { 74 | width: 112px; 75 | height: 112px; 76 | border: 1px solid #ddd; 77 | margin-right: 8px; 78 | margin-bottom: 8px; 79 | position: relative; 80 | } 81 | &-img { 82 | width: 100%; 83 | height: 100%; 84 | } 85 | } 86 | .bgx-pic-selector-content { 87 | margin-top: 8px; 88 | border: 1px solid #ddd; 89 | border-radius: 4px; 90 | } 91 | .bgx-pic-toolbar { 92 | padding: 8px; 93 | border-bottom: 1px solid #ddd; 94 | display: flex; 95 | &-input-group { 96 | width: 340px; 97 | } 98 | &-input { 99 | width: 170px; 100 | } 101 | &-space { 102 | flex: 1; 103 | } 104 | & > * { 105 | margin-right: 8px; 106 | } 107 | } 108 | .bgx-pic-images { 109 | padding: 4px 0 4px 4px; 110 | min-height: 300px; 111 | max-height: 490px; 112 | overflow: auto; 113 | display: flex; 114 | flex-wrap: wrap; 115 | &-option { 116 | margin: 4px; 117 | color: #666; 118 | line-height: 20px; 119 | cursor: pointer; 120 | height: 130px; 121 | .no-select; 122 | &-wrapper { 123 | position: relative; 124 | overflow: hidden; 125 | border-radius: 4px; 126 | border: 1px solid #ddd; 127 | width: 106px; 128 | height: 106px; 129 | } 130 | &-img { 131 | width: 100%; 132 | height: 100%; 133 | } 134 | &-mask { 135 | position: absolute; 136 | top: 0; 137 | left: 0; 138 | right: 0; 139 | bottom: 0; 140 | background-color: rgba(0, 0, 0, 0.5); 141 | opacity: 0; 142 | &:hover { 143 | opacity: 1; 144 | } 145 | } 146 | &-icon { 147 | color: #fff; 148 | font-size: 18px; 149 | position: absolute; 150 | right: 1px; 151 | top: 0; 152 | } 153 | &-preview { 154 | color: #fff; 155 | font-size: 18px; 156 | position: absolute; 157 | left: 50%; 158 | top: 50%; 159 | transform: translate(-50%, -50%); 160 | } 161 | &:hover { 162 | .bgx-pic-images-option-wrapper { 163 | border: 1px solid #999; 164 | } 165 | span { 166 | color: #000; 167 | } 168 | } 169 | &-selected { 170 | .bgx-pic-images-option-mask { 171 | opacity: 1; 172 | } 173 | } 174 | } 175 | } 176 | 177 | .preview-icon() { 178 | position: fixed; 179 | top: 50%; 180 | transform: translateY(-50%); 181 | background-color: rgba(0, 0, 0, 0.5); 182 | color: #fff; 183 | font-size: 30px; 184 | width: 50px; 185 | height: 50px; 186 | line-height: 50px; 187 | cursor: pointer; 188 | .no-select; 189 | } 190 | .preview-min { 191 | .preview-icon; 192 | 193 | left: 50px; 194 | text-align: left; 195 | } 196 | .preview-add { 197 | .preview-icon; 198 | 199 | right: 50px; 200 | text-align: right; 201 | } 202 | .preview-selected { 203 | font-size: 40px; 204 | color: #fff; 205 | position: absolute; 206 | top: 50%; 207 | left: 50%; 208 | transform: translate(-50%, -50%); 209 | } 210 | .preview-mask { 211 | content: ''; 212 | display: block; 213 | position: absolute; 214 | top: 0; 215 | left: 0; 216 | width: 100%; 217 | height: 100%; 218 | background-color: rgba(0, 0, 0, 0.5); 219 | } 220 | } 221 | 222 | .preview() { 223 | position: absolute; 224 | top: 400px; 225 | font-size: 25px; 226 | cursor: pointer; 227 | color: #fff; 228 | background-color: rgba(0, 0, 0, 0.2); 229 | width: 50px; 230 | height: 50px; 231 | line-height: 50px; 232 | text-align: center; 233 | user-select: none; 234 | &:hover { 235 | background-color: rgba(0, 0, 0, 0.3); 236 | } 237 | } 238 | 239 | .preview-left { 240 | left: 5px; 241 | .preview(); 242 | } 243 | 244 | .preview-right { 245 | right: 5px; 246 | .preview(); 247 | } 248 | -------------------------------------------------------------------------------- /packages/jeact-components/src/single-cascader/index.less: -------------------------------------------------------------------------------- 1 | .bgx-single-cascader { 2 | display: inline-block; 3 | vertical-align: middle; 4 | width: 100%; 5 | position: relative; 6 | .input-group { 7 | width: 100%; 8 | } 9 | .bgx-label { 10 | display: inline-flex; 11 | align-items: center; 12 | margin: 0; 13 | } 14 | input[type='checkbox'].bgx-checkbox { 15 | margin: 4px; 16 | position: relative; 17 | width: 14px; 18 | height: 14px; 19 | cursor: pointer; 20 | } 21 | input[type='checkbox'].bgx-checkbox::before { 22 | content: ''; 23 | line-height: 14px; 24 | font-size: 10px; 25 | color: #fff; 26 | background-color: #fff; 27 | display: inline-block; 28 | width: 14px; 29 | height: 14px; 30 | border: 1px #ccc solid; 31 | border-radius: 2px; 32 | vertical-align: top; 33 | } 34 | input[type='checkbox'].bgx-checkbox:checked::before { 35 | background-color: #2599ab; 36 | border-color: transparent; 37 | } 38 | input[type='checkbox'].bgx-checkbox::after { 39 | content: ''; 40 | } 41 | input[type='checkbox'].bgx-checkbox:checked::after { 42 | position: absolute; 43 | top: 4px; 44 | left: 3px; 45 | width: 8px; 46 | height: 5px; 47 | border-left: 2px solid #fff; 48 | border-bottom: 2px solid #fff; 49 | transform: rotateZ(-45deg); 50 | } 51 | .bgx-close { 52 | font-family: '黑体', serif; 53 | font-weight: bold; 54 | cursor: pointer; 55 | color: #666; 56 | } 57 | .bgx-close:hover { 58 | color: #888; 59 | } 60 | .bgx-close:active { 61 | color: #333; 62 | } 63 | .bgx-selector { 64 | width: 150px; 65 | position: relative; 66 | } 67 | .bgx-selector ::-webkit-scrollbar { 68 | width: 8px; 69 | height: 8px; 70 | } 71 | .bgx-selector ::-webkit-scrollbar-button { 72 | width: 0; 73 | height: 0; 74 | } 75 | .bgx-selector ::-webkit-scrollbar-thumb { 76 | background: #e1e1e1; 77 | border: none; 78 | border-radius: 0; 79 | } 80 | .bgx-selector ::-webkit-scrollbar-thumb:hover { 81 | background: #ccc; 82 | } 83 | .bgx-selector ::-webkit-scrollbar-thumb:active { 84 | background: #999; 85 | } 86 | .bgx-selector ::-webkit-scrollbar-track { 87 | border: 0 none #fff; 88 | border-radius: 0; 89 | } 90 | .bgx-selector ::-webkit-scrollbar-track:hover { 91 | background: #eee; 92 | } 93 | .bgx-selector ::-webkit-scrollbar-corner { 94 | background: transparent; 95 | } 96 | .bgx-input.form-control[readonly] { 97 | background-color: #fff; 98 | box-shadow: none; 99 | cursor: pointer; 100 | } 101 | .bgx-popup { 102 | font-size: 12px; 103 | top: 30px; 104 | left: 0; 105 | position: absolute; 106 | border: 1px #ddd solid; 107 | min-width: 600px; 108 | max-width: 960px; 109 | z-index: 1000; 110 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 111 | } 112 | .bgx-search-bar { 113 | background-color: #fff; 114 | padding: 8px; 115 | display: flex; 116 | justify-content: flex-end; 117 | } 118 | .bgx-search-input { 119 | width: 450px; 120 | margin-right: 8px; 121 | position: relative; 122 | } 123 | .bgx-search-input > input.form-control { 124 | width: 100%; 125 | } 126 | .bgx-commonly-used { 127 | background-color: #fff; 128 | border: #eee 1px solid; 129 | padding: 4px; 130 | } 131 | .bgx-commonly-used-head { 132 | display: flex; 133 | justify-content: space-between; 134 | align-items: center; 135 | & > label { 136 | margin: 0; 137 | } 138 | & > i { 139 | flex: 1; 140 | font-size: 20px; 141 | color: #333; 142 | text-align: right; 143 | padding-right: 10px; 144 | cursor: pointer; 145 | } 146 | } 147 | .bgx-commonly-used-options { 148 | display: flex; 149 | flex-wrap: wrap; 150 | } 151 | .bgx-commonly-used-option { 152 | font-weight: normal; 153 | cursor: pointer; 154 | white-space: nowrap; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | padding: 4px 8px; 158 | border: 1px solid #eee; 159 | border-radius: 4px; 160 | margin: 2px; 161 | &:hover { 162 | background-color: #eee; 163 | } 164 | } 165 | .bgx-options { 166 | display: flex; 167 | height: 250px; 168 | width: 100%; 169 | overflow: auto; 170 | background-color: #fff; 171 | } 172 | .bgx-column { 173 | border-right: 1px #eee solid; 174 | flex-shrink: 0; 175 | overflow: auto; 176 | } 177 | .bgx-option { 178 | margin: 1px 0; 179 | padding: 6px 4px; 180 | cursor: pointer; 181 | position: relative; 182 | display: flex; 183 | align-items: center; 184 | } 185 | .bgx-option:hover { 186 | background-color: #eee; 187 | } 188 | .bgx-option-selected { 189 | background-color: #eee; 190 | } 191 | .bgx-option.bgx-option-next { 192 | padding-right: 20px; 193 | } 194 | .bgx-option.bgx-option-next::after { 195 | position: absolute; 196 | right: 0; 197 | top: 10px; 198 | content: ''; 199 | width: 14px; 200 | height: 10px; 201 | border: 5px transparent solid; 202 | border-left: 7px #999 solid; 203 | } 204 | .bgx-option-text { 205 | flex-grow: 1; 206 | white-space: nowrap; 207 | overflow: hidden; 208 | text-overflow: ellipsis; 209 | } 210 | .bgx-search-popup { 211 | position: absolute; 212 | background-color: #fff; 213 | top: 30px; 214 | width: 100%; 215 | max-height: 380px; 216 | overflow: auto; 217 | border: 1px #eee solid; 218 | padding: 4px; 219 | z-index: 200; 220 | .no-search-result { 221 | padding: 20px 0; 222 | text-align: center; 223 | color: #999; 224 | } 225 | } 226 | .bgx-search-options { 227 | display: flex; 228 | flex-wrap: wrap; 229 | flex-direction: column; 230 | } 231 | .bgx-search-option { 232 | font-size: 12px; 233 | margin-top: 0; 234 | margin-bottom: 0; 235 | padding: 4px 8px; 236 | font-weight: normal; 237 | cursor: pointer; 238 | &:hover { 239 | background-color: #eee; 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const args = require('minimist')(process.argv.slice(2)) 2 | const fs = require('fs') 3 | const path = require('path') 4 | const chalk = require('chalk') 5 | const semver = require('semver') 6 | const currentVersion = require('../package.json').version 7 | const { prompt } = require('enquirer') 8 | const execa = require('execa') 9 | const prereleaseVersion = semver.prerelease(currentVersion) 10 | const preId = args.preid || (prereleaseVersion && prereleaseVersion[0]) || 'alpha' 11 | const isDryRun = args.dry 12 | const skipBuild = args.skipBuild 13 | const packages = fs 14 | .readdirSync(path.resolve(__dirname, '../packages')) 15 | .filter(p => !p.endsWith('.ts') && !p.startsWith('.')) 16 | 17 | const skippedPackages = [] 18 | 19 | const versionIncrements = [ 20 | 'patch', 21 | 'minor', 22 | 'major', 23 | 'prepatch', 24 | 'preminor', 25 | 'premajor', 26 | 'prerelease' 27 | ] 28 | 29 | const corePkgName = 'jeact' 30 | const pkgName = '@jeact' 31 | 32 | const inc = i => semver.inc(currentVersion, i, preId) 33 | const bin = name => path.resolve(__dirname, '../node_modules/.bin/' + name) 34 | const run = (bin, args, opts = {}) => 35 | execa(bin, args, { stdio: 'inherit', ...opts }) 36 | const dryRun = (bin, args, opts = {}) => 37 | console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts) 38 | const runIfNotDry = isDryRun ? dryRun : run 39 | const getPkgRoot = pkg => path.resolve(__dirname, '../packages/' + pkg) 40 | const step = msg => console.log(chalk.cyan(msg)) 41 | 42 | async function main() { 43 | let targetVersion = args._[0] 44 | 45 | if (!targetVersion) { 46 | // no explicit version, offer suggestions 47 | const { release } = await prompt({ 48 | type: 'select', 49 | name: 'release', 50 | message: 'Select release type', 51 | choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom']) 52 | }) 53 | 54 | if (release === 'custom') { 55 | targetVersion = (await prompt({ 56 | type: 'input', 57 | name: 'version', 58 | message: 'Input custom version', 59 | initial: currentVersion 60 | })).version 61 | } else { 62 | targetVersion = release.match(/\((.*)\)/)[1] 63 | } 64 | } 65 | 66 | if (!semver.valid(targetVersion)) { 67 | throw new Error(`invalid target version: ${targetVersion}`) 68 | } 69 | 70 | const { yes } = await prompt({ 71 | type: 'confirm', 72 | name: 'yes', 73 | message: `Releasing v${targetVersion}. Confirm?` 74 | }) 75 | 76 | if (!yes) { 77 | return 78 | } 79 | 80 | // update all package versions and inter-dependencies 81 | step('\nUpdating cross dependencies...') 82 | updateVersions(targetVersion) 83 | 84 | // build all packages with types 85 | step('\nBuilding all packages...') 86 | if (!skipBuild && !isDryRun) { 87 | await run('yarn', ['build', '--release']) 88 | } else { 89 | console.log(`(skipped)`) 90 | } 91 | 92 | // generate changelog 93 | await run(`yarn`, ['changelog']) 94 | 95 | const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) 96 | if (stdout) { 97 | step('\nCommitting changes...') 98 | await runIfNotDry('git', ['add', '-A']) 99 | await runIfNotDry('git', ['commit', '-m', `chore: v${targetVersion}`]) 100 | } else { 101 | console.log('No changes to commit.') 102 | } 103 | 104 | // publish packages 105 | step('\nPublishing packages...') 106 | for (const pkg of packages) { 107 | await publishPackage(pkg, targetVersion, runIfNotDry) 108 | } 109 | 110 | // push to GitHub 111 | step('\nPushing to GitHub...') 112 | await runIfNotDry('git', ['tag', `v${targetVersion}`]) 113 | await runIfNotDry('git', ['push', 'origin', `refs/tags/v${targetVersion}`]) 114 | await runIfNotDry('git', ['push']) 115 | 116 | if (isDryRun) { 117 | console.log(`\nDry run finished - run git diff to see package changes.`) 118 | } 119 | 120 | if (skippedPackages.length) { 121 | console.log( 122 | chalk.yellow( 123 | `The following packages are skipped and NOT published:\n- ${skippedPackages.join( 124 | '\n- ' 125 | )}` 126 | ) 127 | ) 128 | } 129 | console.log() 130 | } 131 | 132 | function updateVersions(version) { 133 | // 1. update root package.json 134 | updatePackage(path.resolve(__dirname, '..'), version) 135 | // 2. update all packages 136 | packages.forEach(p => updatePackage(getPkgRoot(p), version)) 137 | } 138 | 139 | function updatePackage(pkgRoot, version) { 140 | const pkgPath = path.resolve(pkgRoot, 'package.json') 141 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) 142 | pkg.version = version 143 | updateDeps(pkg, 'dependencies', version) 144 | updateDeps(pkg, 'peerDependencies', version) 145 | fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') 146 | } 147 | 148 | function updateDeps(pkg, depType, version) { 149 | const deps = pkg[depType] 150 | if (!deps) return 151 | Object.keys(deps).forEach(dep => { 152 | if ( 153 | dep === corePkgName || 154 | (dep.startsWith(pkgName) && packages.includes(dep.replace(RegExp(`^${pkgName}\/`), ''))) 155 | ) { 156 | console.log( 157 | chalk.yellow(`${pkg.name} -> ${depType} -> ${dep}@${version}`) 158 | ) 159 | deps[dep] = version 160 | } 161 | }) 162 | } 163 | 164 | async function publishPackage(pkgName, version, runIfNotDry) { 165 | if (skippedPackages.includes(pkgName)) { 166 | return 167 | } 168 | const pkgRoot = getPkgRoot(pkgName) 169 | const pkgPath = path.resolve(pkgRoot, 'package.json') 170 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) 171 | if (pkg.private) { 172 | return 173 | } 174 | 175 | step(`Publishing ${pkgName}...`) 176 | try { 177 | await runIfNotDry( 178 | 'yarn', 179 | [ 180 | 'publish', 181 | '--new-version', 182 | version, 183 | '--access', 184 | 'public' 185 | ], 186 | { 187 | cwd: pkgRoot, 188 | stdio: 'pipe' 189 | } 190 | ) 191 | console.log(chalk.green(`Successfully published ${pkgName}@${version}`)) 192 | } catch (e) { 193 | if (e.stderr.match(/previously published/)) { 194 | console.log(chalk.red(`Skipping already published: ${pkgName}`)) 195 | } else { 196 | throw e 197 | } 198 | } 199 | } 200 | 201 | main().catch(err => { 202 | console.error(err) 203 | }) 204 | -------------------------------------------------------------------------------- /packages/jeact-components/src/cascader/index.less: -------------------------------------------------------------------------------- 1 | .bgx-cascader { 2 | display: inline-block; 3 | vertical-align: middle; 4 | width: 150px; 5 | position: relative; 6 | .bgx-label { 7 | display: inline-flex; 8 | align-items: center; 9 | margin: 8px 0; 10 | } 11 | input[type='checkbox'].bgx-checkbox { 12 | margin: 4px; 13 | position: relative; 14 | width: 14px; 15 | height: 14px; 16 | cursor: pointer; 17 | } 18 | input[type='checkbox'].bgx-checkbox::before { 19 | content: ''; 20 | line-height: 14px; 21 | font-size: 10px; 22 | color: #fff; 23 | background-color: #fff; 24 | display: inline-block; 25 | width: 14px; 26 | height: 14px; 27 | border: 1px #ccc solid; 28 | border-radius: 2px; 29 | vertical-align: top; 30 | } 31 | input[type='checkbox'].bgx-checkbox:checked::before { 32 | background-color: #2599ab; 33 | border-color: transparent; 34 | } 35 | input[type='checkbox'].bgx-checkbox::after { 36 | content: ''; 37 | } 38 | input[type='checkbox'].bgx-checkbox:checked::after { 39 | position: absolute; 40 | top: 4px; 41 | left: 3px; 42 | width: 8px; 43 | height: 5px; 44 | border-left: 2px solid #fff; 45 | border-bottom: 2px solid #fff; 46 | transform: rotateZ(-45deg); 47 | } 48 | .bgx-close { 49 | font-family: '黑体', serif; 50 | font-weight: bold; 51 | cursor: pointer; 52 | color: #666; 53 | } 54 | .bgx-close:hover { 55 | color: #888; 56 | } 57 | .bgx-close:active { 58 | color: #333; 59 | } 60 | &::-webkit-scrollbar { 61 | width: 8px; 62 | height: 8px; 63 | } 64 | &::-webkit-scrollbar-button { 65 | width: 0; 66 | height: 0; 67 | } 68 | &::-webkit-scrollbar-thumb { 69 | background: #e1e1e1; 70 | border: none; 71 | border-radius: 0; 72 | } 73 | &::-webkit-scrollbar-thumb:hover { 74 | background: #ccc; 75 | } 76 | &::-webkit-scrollbar-thumb:active { 77 | background: #999; 78 | } 79 | &::-webkit-scrollbar-track { 80 | border: 0 none #fff; 81 | border-radius: 0; 82 | } 83 | &::-webkit-scrollbar-track:hover { 84 | background: #eee; 85 | } 86 | &::-webkit-scrollbar-corner { 87 | background: transparent; 88 | } 89 | .bgx-input.form-control[readonly] { 90 | background-color: #fff; 91 | box-shadow: none; 92 | cursor: pointer; 93 | } 94 | .bgx-popup { 95 | font-size: 12px; 96 | top: 30px; 97 | left: 0; 98 | position: absolute; 99 | border: 1px #ddd solid; 100 | width: 724px; 101 | z-index: 1000; 102 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 103 | } 104 | .bgx-search-bar { 105 | background-color: #fff; 106 | padding: 8px; 107 | display: flex; 108 | justify-content: flex-end; 109 | } 110 | .bgx-search-input { 111 | width: 450px; 112 | margin-right: 8px; 113 | position: relative; 114 | } 115 | .bgx-search-input > input.form-control { 116 | width: 100%; 117 | } 118 | .bgx-commonly-used { 119 | background-color: #eee; 120 | padding: 4px 0; 121 | } 122 | .bgx-commonly-used-head { 123 | display: flex; 124 | justify-content: space-between; 125 | align-items: center; 126 | & > label { 127 | margin: 0; 128 | } 129 | & > i { 130 | flex: 1; 131 | font-size: 20px; 132 | color: #333; 133 | text-align: right; 134 | padding-right: 10px; 135 | cursor: pointer; 136 | } 137 | } 138 | .bgx-commonly-used-options { 139 | display: flex; 140 | flex-wrap: wrap; 141 | } 142 | .bgx-commonly-used-option { 143 | margin-top: 0; 144 | font-weight: normal; 145 | width: 20%; 146 | white-space: nowrap; 147 | overflow: hidden; 148 | text-overflow: ellipsis; 149 | } 150 | .bgx-options { 151 | display: flex; 152 | height: 250px; 153 | width: 100%; 154 | overflow: auto; 155 | background-color: #fff; 156 | } 157 | .bgx-column { 158 | border-right: 1px #eee solid; 159 | flex-shrink: 0; 160 | width: 120px; 161 | overflow: auto; 162 | } 163 | .bgx-option { 164 | margin: 1px 0; 165 | padding: 4px 0; 166 | cursor: pointer; 167 | position: relative; 168 | display: flex; 169 | align-items: center; 170 | } 171 | .bgx-option:hover { 172 | background-color: #eee; 173 | } 174 | .bgx-option-selected { 175 | background-color: #eee; 176 | } 177 | .bgx-option.bgx-option-next { 178 | padding-right: 20px; 179 | } 180 | .bgx-option.bgx-option-next::after { 181 | position: absolute; 182 | right: 0; 183 | top: 10px; 184 | content: ''; 185 | width: 14px; 186 | height: 10px; 187 | border: 5px transparent solid; 188 | border-left: 7px #999 solid; 189 | } 190 | .bgx-option-text { 191 | flex-grow: 1; 192 | white-space: nowrap; 193 | overflow: hidden; 194 | text-overflow: ellipsis; 195 | } 196 | .bgx-selected-cnt { 197 | padding: 6px 6px 0; 198 | background-color: #fff; 199 | } 200 | .bgx-selected-label { 201 | font-weight: bold; 202 | margin-right: 5px; 203 | } 204 | .bgx-selected-tags { 205 | display: flex; 206 | flex-wrap: wrap; 207 | padding: 4px; 208 | max-height: 300px; 209 | overflow: auto; 210 | background-color: #fff; 211 | } 212 | .bgx-selected-tag { 213 | font-size: 13px; 214 | padding: 3px; 215 | width: auto; 216 | border: 1px #ccc solid; 217 | margin: 4px; 218 | background-color: #eee; 219 | color: #555; 220 | border-radius: 2px; 221 | } 222 | .bgx-search-popup { 223 | position: absolute; 224 | background-color: #fff; 225 | top: 30px; 226 | width: 100%; 227 | max-height: 380px; 228 | overflow: auto; 229 | border: 1px #eee solid; 230 | padding: 4px; 231 | z-index: 200; 232 | .no-search-result { 233 | padding: 20px 0; 234 | text-align: center; 235 | color: #999; 236 | } 237 | } 238 | .bgx-search-head { 239 | display: flex; 240 | & > label { 241 | margin: 0 8px; 242 | } 243 | } 244 | .bgx-search-options { 245 | display: flex; 246 | flex-wrap: wrap; 247 | flex-direction: column; 248 | } 249 | .bgx-search-option { 250 | font-size: 12px; 251 | margin-top: 0; 252 | margin-bottom: 0; 253 | padding: 4px 8px; 254 | font-weight: normal; 255 | cursor: pointer; 256 | &:hover { 257 | background-color: #eee; 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /packages/jeact/src/dom.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isVComponent, 3 | isVDom, 4 | isVElement, 5 | isVFunction, 6 | isVText, 7 | VDom, 8 | VNode, 9 | VText, 10 | } from './v-node'; 11 | import { Component, FunctionComponent } from './component'; 12 | 13 | export class DomOperate { 14 | constructor(private context: Component) {} 15 | 16 | createElement(vNode: VText, rootUpdate: () => void): Text; 17 | createElement(vNode: VDom, rootUpdate: () => void): HTMLElement; 18 | createElement(vNode: VNode, rootUpdate: () => void): Node; 19 | createElement(vNode: VNode, rootUpdate: () => void): any { 20 | if (isVComponent(vNode)) { 21 | vNode.component = Component.create(vNode.type, { 22 | ...vNode.attributes, 23 | ...vNode.handles, 24 | children: vNode.children, 25 | rootUpdate, 26 | }); 27 | vNode.el = vNode.component.vNode?.el; 28 | Object.keys(vNode.attributes).forEach((key) => { 29 | const value = vNode.attributes[key]; 30 | this.setAttribute(vNode.component, key, value); 31 | }); 32 | return vNode.el; 33 | } else if (isVFunction(vNode)) { 34 | vNode.component = Component.create>(FunctionComponent, { 35 | renderFunction: vNode.type, 36 | functionProps: { 37 | ...vNode.attributes, 38 | ...vNode.handles, 39 | children: vNode.children, 40 | }, 41 | rootUpdate, 42 | }); 43 | vNode.el = vNode.component.vNode?.el; 44 | return vNode.el; 45 | } else if (isVDom(vNode)) { 46 | const el: HTMLElement = document.createElement(vNode.type); 47 | vNode.el = el; 48 | Object.keys(vNode.handles).forEach((key) => { 49 | const value = vNode.handles[key]; 50 | const handleName = key.toLowerCase().replace(/^on/, ''); 51 | el.addEventListener(handleName, value); 52 | }); 53 | Object.keys(vNode.attributes).forEach((key) => { 54 | const value = vNode.attributes[key]; 55 | this.setAttribute(el, key, value); 56 | }); 57 | vNode.children?.forEach((value) => { 58 | const node = this.createElement(value, rootUpdate); 59 | if (node) { 60 | el.appendChild(node); 61 | } 62 | }); 63 | return el; 64 | } else if (isVText(vNode)) { 65 | vNode.el = document.createTextNode(vNode.content); 66 | return vNode.el; 67 | } 68 | } 69 | 70 | /** 71 | * 72 | * @param el VNode对应的真是dom 73 | * @param newVNode 74 | * @param oldVNode 75 | */ 76 | updateVDom(el: Node, newVNode: VDom, oldVNode: VDom) { 77 | if (isVDom(newVNode) && isVDom(oldVNode) && el instanceof HTMLElement) { 78 | Object.keys(oldVNode.handles).forEach((key) => { 79 | if (!newVNode.handles.hasOwnProperty(key)) { 80 | const value = oldVNode.handles[key]; 81 | const handleName = key.toLowerCase().replace(/^on/, ''); 82 | el.removeEventListener(handleName, value); 83 | } 84 | }); 85 | Object.keys(newVNode.handles).forEach((key) => { 86 | const handleName = key.toLowerCase().replace(/^on/, ''); 87 | const value = newVNode.handles[key]; 88 | const oldValue = oldVNode.handles[key]; 89 | if (value === oldValue) { 90 | return; 91 | } 92 | if (oldVNode.handles.hasOwnProperty(key)) { 93 | el.removeEventListener(handleName, oldValue); 94 | } 95 | el.addEventListener(handleName, value); 96 | }); 97 | Object.keys(oldVNode.attributes).forEach((key) => { 98 | if (!newVNode.attributes.hasOwnProperty(key)) { 99 | this.removeAttribute(el, key, oldVNode.attributes[key]); 100 | } 101 | }); 102 | 103 | Object.keys(newVNode.attributes).forEach((key) => { 104 | const value = newVNode.attributes[key]; 105 | const oldValue = oldVNode.attributes[key]; 106 | if (!(value instanceof Object) && value === oldValue) { 107 | // 避免引用类型被修改后不更新 108 | return; 109 | } else if (value && value !== 0) { 110 | this.setAttribute(el, key, value, oldValue); 111 | } else { 112 | this.removeAttribute(el, key, oldValue); 113 | } 114 | }); 115 | } else if (isVText(newVNode) && isVText(oldVNode) && newVNode.content !== oldVNode.content) { 116 | newVNode.el.data = newVNode.content; 117 | } 118 | } 119 | 120 | updateVText(el: Node, newVNode: VText, oldVNode: VText) { 121 | if (newVNode.content !== oldVNode.content) { 122 | newVNode.el.data = newVNode.content; 123 | } 124 | } 125 | 126 | setAttribute(el: Element | Component, attrName: string, attrValue: any, oldValue: any = {}) { 127 | if (el instanceof HTMLInputElement && el.type === 'checkbox' && attrName === 'checked') { 128 | el['checked'] = attrValue; 129 | return; 130 | } 131 | if (el instanceof HTMLInputElement && attrName === 'value') { 132 | el['value'] = attrValue; 133 | return; 134 | } 135 | if (el instanceof HTMLElement && attrName === 'dangerouslySetInnerHTML') { 136 | el.innerHTML = attrValue; 137 | return; 138 | } 139 | if (typeof attrValue !== 'string' && el instanceof HTMLElement && attrName === 'style') { 140 | Object.keys(oldValue).forEach((key) => { 141 | if (!attrValue.hasOwnProperty(key)) { 142 | el.style[key] = ''; 143 | } 144 | }); 145 | Object.keys(attrValue).forEach((key) => { 146 | const value = attrValue[key]; 147 | if (value === oldValue[key]) { 148 | return; 149 | } else if (value && value !== 0) { 150 | el.style[key] = value; 151 | } else { 152 | el.style[key] = ''; 153 | } 154 | }); 155 | return; 156 | } 157 | if (attrName === 'ref') { 158 | this.context.refs[attrValue] = el; 159 | } 160 | if (el instanceof HTMLElement && attrValue != null) { 161 | if (attrValue === true) { 162 | el.setAttribute(attrName, ''); 163 | } else if (attrValue === false) { 164 | el.removeAttribute(attrName); 165 | } else { 166 | el.setAttribute(attrName, String(attrValue)); 167 | } 168 | } 169 | } 170 | 171 | removeAttribute(el: HTMLElement, attrName: string, oldValue: any = {}) { 172 | if (el instanceof HTMLInputElement && el.type === 'checkbox' && attrName === 'checked') { 173 | el[attrName] = false; 174 | return; 175 | } 176 | if (el instanceof HTMLInputElement && attrName === 'value') { 177 | el[attrName] = ''; 178 | return; 179 | } 180 | if (attrName === 'dangerouslySetInnerHTML') { 181 | el.innerHTML = ''; 182 | return; 183 | } 184 | if (el instanceof HTMLElement && attrName === 'style') { 185 | Object.keys(oldValue).forEach((key) => { 186 | el.style[key] = ''; 187 | }); 188 | return; 189 | } 190 | el.removeAttribute(attrName); 191 | } 192 | 193 | // 移除el的所有子节点 194 | removeChildren(el: Node) { 195 | const children = el.childNodes; 196 | for (let i = children.length - 1; i >= 0; i--) { 197 | this.removeChild(el, children[i]); 198 | } 199 | } 200 | 201 | appendChild(parentNode: Node, childNode: Node) { 202 | parentNode.appendChild(childNode); 203 | } 204 | 205 | removeChild(parentNode: Node, childNode: Node) { 206 | parentNode.removeChild(childNode); 207 | } 208 | 209 | insertBefore(parentNode: Node, newNode: Node, referenceNode: Node) { 210 | parentNode.insertBefore(newNode, referenceNode); 211 | } 212 | 213 | parentNode(node: Node): Node { 214 | return node.parentNode; 215 | } 216 | 217 | nextSibling(node: Node) { 218 | return node.nextSibling; 219 | } 220 | 221 | removeVNode(vNode: VNode) { 222 | if (isVComponent(vNode) || isVFunction(vNode)) { 223 | this.callDestroy(vNode); 224 | } 225 | const pNode = this.parentNode(vNode.el); 226 | if (pNode) { 227 | this.removeChild(pNode, vNode.el); 228 | } 229 | } 230 | 231 | // 递归销毁所有子节点 232 | callDestroy(vnode: VNode) { 233 | if (isVElement(vnode)) { 234 | for (let i = 0; i < vnode.children.length; ++i) { 235 | this.callDestroy(vnode.children[i]); 236 | } 237 | } 238 | if (isVComponent(vnode)) { 239 | if (vnode.component?.vNode) { 240 | this.callDestroy(vnode.component?.vNode); 241 | vnode.component.destroy(); 242 | } 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /packages/jeact/src/diff.ts: -------------------------------------------------------------------------------- 1 | import { isVComponent, isVDom, isVFunction, isVText, VElement, VNode } from './v-node'; 2 | import { isEmpty } from './utils'; 3 | import { DomOperate } from './dom'; 4 | 5 | export class Differentiator { 6 | constructor(private dom: DomOperate) {} 7 | 8 | /** 9 | * 判断input是否有相同的type 10 | * @param a 11 | * @param b 12 | */ 13 | sameInputType(a: VElement, b: VElement) { 14 | if (a.type !== 'input') { 15 | return true; 16 | } 17 | const aType = a.attributes.type; 18 | const bType = b.attributes.type; 19 | if (aType == null && bType == null) { 20 | return true; 21 | } 22 | return aType === bType; 23 | } 24 | 25 | /** 26 | * 判断是否是相同的VNode 27 | * @param a 28 | * @param b 29 | */ 30 | sameVNode(a: VNode, b: VNode) { 31 | if (a.key !== b.key) { 32 | return false; 33 | } 34 | if (isVDom(a) && isVDom(b)) { 35 | return ( 36 | a.type === b.type && this.sameInputType(a, b) // 当标签是的时候,type必须相同 37 | ); 38 | } 39 | if (isVComponent(a) && isVComponent(b) && a.type === b.type) { 40 | return true; 41 | } 42 | if (isVFunction(a) && isVFunction(b)) { 43 | return true; 44 | } 45 | return isVText(a) && isVText(b); 46 | } 47 | 48 | /** 49 | * 根据vNode的key生成map 50 | * @param children 待生成的vNode数组 51 | * @param beginIdx 生成范围 52 | * @param endIdx 生成范围 53 | */ 54 | createIndexMap(children: VNode[], beginIdx: number, endIdx: number) { 55 | let i, key; 56 | const map = new Map(); 57 | for (i = beginIdx; i <= endIdx; ++i) { 58 | key = children[i].key; 59 | if (key != null) { 60 | map.set(key, i); 61 | } 62 | } 63 | return map; 64 | } 65 | 66 | findVNodeIndex(vNodes: VNode[], targetNode: VNode, start: number, end: number) { 67 | for (let i = start; i < end; i++) { 68 | const currVNode = vNodes[i]; 69 | if (currVNode != null && this.sameVNode(targetNode, currVNode)) { 70 | return i; 71 | } 72 | } 73 | return null; 74 | } 75 | 76 | updateChildren( 77 | parentEl: Node, 78 | oldChildren: VNode[], 79 | newChildren: VNode[], 80 | rootUpdate?: () => void, 81 | ) { 82 | let oldStartIdx = 0; 83 | let oldStartVNode = oldChildren[0]; 84 | let newStartIdx = 0; 85 | let newStartVNode = newChildren[0]; 86 | let oldEndIdx = oldChildren.length - 1; 87 | let oldEndVNode = oldChildren[oldEndIdx]; 88 | let newEndIdx = newChildren.length - 1; 89 | let newEndVNode = newChildren[newEndIdx]; 90 | let oldKeyToIdx: Map; 91 | let idxInOld: number; 92 | let vNodeToMove: VNode; 93 | 94 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 95 | if (oldStartVNode == null) { 96 | // 对于vnode.key的比较,会把oldVnode = null 97 | oldStartVNode = oldChildren[++oldStartIdx]; 98 | } else if (oldEndVNode == null) { 99 | oldEndVNode = oldChildren[--oldEndIdx]; 100 | } else if (this.sameVNode(oldStartVNode, newStartVNode)) { 101 | this.patchVNode(oldStartVNode, newStartVNode); 102 | oldStartVNode = oldChildren[++oldStartIdx]; 103 | newStartVNode = newChildren[++newStartIdx]; 104 | } else if (this.sameVNode(oldEndVNode, newEndVNode)) { 105 | this.patchVNode(oldEndVNode, newEndVNode); 106 | oldEndVNode = oldChildren[--oldEndIdx]; 107 | newEndVNode = newChildren[--newEndIdx]; 108 | } else if (this.sameVNode(oldStartVNode, newEndVNode)) { 109 | // VNode 右移 110 | this.patchVNode(oldStartVNode, newEndVNode); 111 | this.dom.insertBefore(parentEl, oldStartVNode.el, this.dom.nextSibling(newEndVNode.el)); 112 | oldStartVNode = oldChildren[++oldStartIdx]; 113 | newEndVNode = newChildren[--newEndIdx]; 114 | } else if (this.sameVNode(oldEndVNode, newStartVNode)) { 115 | // VNode 左移 116 | this.patchVNode(oldEndVNode, newStartVNode); 117 | this.dom.insertBefore(parentEl, oldEndVNode.el, newStartVNode.el); 118 | oldEndVNode = oldChildren[--oldEndIdx]; 119 | newStartVNode = newChildren[++newStartIdx]; 120 | } else { 121 | if (oldKeyToIdx === undefined) { 122 | oldKeyToIdx = this.createIndexMap(oldChildren, oldStartIdx, oldEndIdx); 123 | } 124 | // 根据新vNode的key在oldVNode中寻找符合条件的 125 | idxInOld = 126 | newStartVNode.key != null 127 | ? oldKeyToIdx.get(newStartVNode.key) 128 | : this.findVNodeIndex(oldChildren, newStartVNode, oldStartIdx, oldEndIdx); 129 | if (idxInOld == null) { 130 | // New element 131 | newStartVNode.el = this.dom.createElement(newStartVNode, rootUpdate); 132 | this.dom.insertBefore(parentEl, newStartVNode.el, oldStartVNode.el); 133 | } else { 134 | vNodeToMove = oldChildren[idxInOld]; 135 | if (this.sameVNode(vNodeToMove, newStartVNode)) { 136 | this.patchVNode(vNodeToMove, newStartVNode); 137 | oldChildren[idxInOld] = undefined; 138 | this.dom.insertBefore(parentEl, vNodeToMove.el, oldStartVNode.el); 139 | } else { 140 | // key相同但是element不同 141 | newStartVNode.el = this.dom.createElement(newStartVNode, rootUpdate); 142 | this.dom.insertBefore(parentEl, newStartVNode.el, oldStartVNode.el); 143 | } 144 | } 145 | newStartVNode = newChildren[++newStartIdx]; 146 | } 147 | } 148 | if (oldStartIdx > oldEndIdx) { 149 | const ref = newChildren[newEndIdx + 1]; 150 | const refEl = isVDom(ref) ? ref.el : null; 151 | for (; newStartIdx <= newEndIdx; newStartIdx++) { 152 | const el = this.dom.createElement(newChildren[newStartIdx], rootUpdate); 153 | newChildren[newStartIdx].el = el; 154 | this.dom.insertBefore(parentEl, el, refEl); 155 | } 156 | } else if (newStartIdx > newEndIdx) { 157 | for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) { 158 | const child = oldChildren[oldStartIdx]; 159 | if (child != null) { 160 | this.dom.removeVNode(child); 161 | } 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * 对类型相同的的两个node同步 168 | * @param oldVNode 169 | * @param newVNode 170 | * @param rootUpdate 171 | */ 172 | patchVNode(oldVNode: VNode, newVNode: VNode, rootUpdate?: () => void) { 173 | const el = (newVNode.el = oldVNode.el); 174 | if (oldVNode === newVNode) { 175 | return; 176 | } 177 | if (isVText(oldVNode) && isVText(newVNode)) { 178 | this.dom.updateVText(el, newVNode, oldVNode); 179 | return; 180 | } 181 | if (isVDom(oldVNode) && isVDom(newVNode)) { 182 | this.dom.updateVDom(el, newVNode, oldVNode); 183 | const oldChildren = oldVNode.children; 184 | const newChildren = newVNode.children; 185 | if (!isEmpty(oldChildren) && !isEmpty(newChildren) && oldChildren !== newChildren) { 186 | this.updateChildren(el, oldChildren, newChildren, rootUpdate); 187 | } else if (!isEmpty(newChildren)) { 188 | newChildren.forEach((value) => 189 | this.dom.appendChild(el, this.dom.createElement(value, rootUpdate)), 190 | ); 191 | } else if (!isEmpty(oldChildren)) { 192 | this.dom.removeChildren(el); 193 | } 194 | return; 195 | } 196 | if (isVComponent(oldVNode) && isVComponent(newVNode)) { 197 | newVNode.component = oldVNode.component; 198 | const v = oldVNode.component.runDiff(); 199 | oldVNode.el = v && v.el; 200 | return; 201 | } 202 | if (isVFunction(oldVNode) && isVFunction(newVNode)) { 203 | newVNode.component = oldVNode.component; 204 | newVNode.component.functionProps = { 205 | ...newVNode.attributes, 206 | ...newVNode.handles, 207 | children: newVNode.children, 208 | }; 209 | const v = oldVNode.component.runDiff(); 210 | oldVNode.el = v && v.el; 211 | return; 212 | } 213 | } 214 | 215 | patch(oldVNode: VNode, newVNode: VNode, rootUpdate: () => void) { 216 | if (this.sameVNode(oldVNode, newVNode)) { 217 | this.patchVNode(oldVNode, newVNode); 218 | } else { 219 | const oldEl = oldVNode.el; // 当前oldVnode对应的真实元素节点 220 | const parentEl = oldEl.parentNode; // 父元素 221 | newVNode.el = this.dom.createElement(newVNode, rootUpdate); // 根据Vnode生成新元素 222 | this.dom.insertBefore(parentEl, newVNode.el, oldEl); 223 | this.dom.removeChild(parentEl, oldEl); // 将新元素添加进父元素 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /packages/jeact-components/src/picture-selector/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | import classname from 'classname'; 3 | import Sortable from 'sortablejs'; 4 | import { ModalComponent, mountModal } from '../modal'; 5 | import { Component, createVNode } from 'jeact'; 6 | 7 | type ImageInfo = { 8 | url: string; 9 | width?: number; 10 | height?: number; 11 | type?: string; 12 | propertyCode?: string; 13 | platform?: string; 14 | } 15 | 16 | export class PictureSelectorComponent extends Component { 17 | imageList: ImageInfo[] = []; 18 | 19 | poaList: { code: string; label: string }[] = []; 20 | poaFilter: string; 21 | platformList: { value: string; label: string }[] = []; 22 | platformFilter: string; 23 | currentCode: string; 24 | selectedImg: Record = { [this.currentCode]: [] }; 25 | 26 | bgxCode: string; 27 | bgxSubCode: string; 28 | bgxMaxImageNum: number; 29 | bgxMaxImageSize: number; 30 | bgxMinImageSize: number; 31 | bgxMustSquare: boolean; 32 | 33 | bgxLoadImageByCode: (code) => Promise; 34 | bgxOnOk: (imageList: Record) => void; 35 | 36 | set bgxInitImg(value: Record) { 37 | this.currentCode = Object.keys(value)[0] || ''; 38 | Object.keys(value).forEach((code) => { 39 | this.getImageInfos(value[code].map((value1) => ({ url: value1 }))).then((res) => { 40 | this.selectedImg[code] = res; 41 | }); 42 | }); 43 | } 44 | 45 | get displayImage() { 46 | return this.imageList.filter( 47 | (value) => 48 | (!this.poaFilter || value.propertyCode === this.poaFilter) && 49 | (!this.platformFilter || value.platform === this.platformFilter), 50 | ); 51 | } 52 | 53 | loadPoaList() { 54 | const poaList = [ 55 | ...new Set(this.imageList.map((value) => value.propertyCode).filter((value) => value)), 56 | ]; 57 | return poaList.map(function (value) { 58 | return { 59 | code: value, 60 | label: value, 61 | }; 62 | }); 63 | } 64 | 65 | loadPlatformList() { 66 | const platformList = [ 67 | ...new Set(this.imageList.map((value) => value.platform).filter((value) => value)), 68 | ]; 69 | return platformList.map(function (value) { 70 | return { 71 | value, 72 | label: value, 73 | }; 74 | }); 75 | } 76 | 77 | getImageInfos(images: ImageInfo[]) { 78 | return Promise.all(images.map((image) => this.getImgInfo(image))); 79 | } 80 | 81 | getImgInfo(image: ImageInfo): Promise { 82 | const newImage: ImageInfo = { ...image }; 83 | return new Promise((resolve, reject) => { 84 | const img = new Image(); 85 | img.src = image.url; 86 | img.onerror = function (e) { 87 | resolve(newImage); 88 | }; 89 | const arr = newImage.url.split('.'); 90 | newImage.type = arr[arr.length - 1].toUpperCase(); 91 | if (img.complete) { 92 | newImage.width = img.width; 93 | newImage.height = img.height; 94 | resolve(newImage); 95 | } else { 96 | img.onload = function () { 97 | newImage.width = img.width; 98 | newImage.height = img.height; 99 | img.onload = null; // 避免重复加载 100 | resolve(newImage); 101 | }; 102 | } 103 | }); 104 | } 105 | 106 | constructor(args) { 107 | super(args); 108 | } 109 | 110 | mounted() { 111 | super.mounted(); 112 | 113 | const wrapper = this.refs.sortWrapper as HTMLElement; 114 | // tslint:disable-next-line:no-unused-expression 115 | new Sortable(wrapper, { 116 | animation: 150, 117 | onUpdate: (event) => { 118 | const item = this.selectedImg[this.currentCode].splice(event.oldIndex, 1)[0]; 119 | this.selectedImg[this.currentCode].splice(event.newIndex, 0, item); 120 | this.update(); 121 | }, 122 | }); 123 | this.loadByCode(); 124 | } 125 | 126 | async loadByCode() { 127 | if (this.bgxLoadImageByCode && this.bgxCode) { 128 | try { 129 | this.imageList = await this.bgxLoadImageByCode(this.bgxCode); 130 | } catch (e) { 131 | this.imageList = []; 132 | } 133 | this.update(); 134 | this.poaList = this.loadPoaList(); 135 | if (this.bgxSubCode && this.poaList.some((value) => value.code === this.bgxSubCode)) { 136 | this.poaFilter = this.bgxSubCode; 137 | this.bgxSubCode = null; 138 | } 139 | this.platformList = this.loadPlatformList(); 140 | this.imageList = await this.getImageInfos(this.imageList); 141 | this.update(); 142 | } 143 | } 144 | 145 | selectAll = (e) => { 146 | const num = 147 | this.bgxMaxImageNum != null 148 | ? this.bgxMaxImageNum - this.selectedImg[this.currentCode].length 149 | : this.selectedImg.length; 150 | const unselectedImg = this.displayImage.filter( 151 | (value) => !this.isSelected(value) && this.sizeCheck(value), 152 | ); 153 | const newImgs = unselectedImg[this.currentCode].slice(0, num); 154 | this.selectedImg[this.currentCode] = this.selectedImg[this.currentCode].concat(newImgs); 155 | this.update(); 156 | }; 157 | 158 | isSelected = (item: ImageInfo) => { 159 | return ( 160 | this.selectedImg && 161 | this.selectedImg[this.currentCode].some((val) => val.url === (item && item.url)) 162 | ); 163 | }; 164 | 165 | private clickImg(item: ImageInfo) { 166 | if (this.isSelected(item)) { 167 | this.removeImg(item); 168 | } else if (this.sizeCheck(item)) { 169 | this.selectImg(item); 170 | } 171 | this.update(); 172 | } 173 | 174 | selectImg(item: ImageInfo) { 175 | if (this.selectedImg[this.currentCode].length >= this.bgxMaxImageNum) { 176 | alert(`最多选择${this.bgxMaxImageNum}张图片!`); 177 | return; 178 | } 179 | this.selectedImg[this.currentCode] = [...this.selectedImg[this.currentCode], item]; 180 | } 181 | 182 | removeImg(item: ImageInfo) { 183 | const index = this.selectedImg[this.currentCode].findIndex((val) => val.url === item.url); 184 | this.selectedImg[this.currentCode].splice(index, 1); 185 | this.selectedImg[this.currentCode] = [...this.selectedImg[this.currentCode]]; 186 | this.update(); 187 | } 188 | 189 | preview(item: ImageInfo, imageList: ImageInfo[]) { 190 | let currItem = item; 191 | 192 | const modal = Component.create( 193 | ModalComponent, 194 | { 195 | title: '预览图片', 196 | content: () => { 197 | return ( 198 |
199 | 200 |
lastImg()}> 201 | 202 |
203 |
nextImg()}> 204 | 205 |
206 |
207 | ); 208 | }, 209 | maskCloseable: true, 210 | style: 'width: 900px;', 211 | buttons: null, 212 | }, 213 | document.body, 214 | ); 215 | this.update(); 216 | 217 | const nextImg = () => { 218 | const index = imageList.findIndex((value) => value === currItem); 219 | currItem = imageList[(index + 1) % imageList.length]; 220 | modal.update(); 221 | }; 222 | const lastImg = () => { 223 | const index = imageList.findIndex((value) => value === currItem); 224 | const newIndex = index - 1 < 0 ? index + imageList.length : index - 1; 225 | currItem = imageList[newIndex]; 226 | modal.update(); 227 | }; 228 | } 229 | 230 | async codeChange(value: any) { 231 | this.bgxCode = value; 232 | this.loadByCode(); 233 | } 234 | 235 | poaFilterChange() { 236 | const that = this; 237 | return function (this: HTMLSelectElement) { 238 | that.poaFilter = this.value; 239 | that.loadByCode(); 240 | }; 241 | } 242 | 243 | platformFilterChange() { 244 | const that = this; 245 | return function (this: HTMLSelectElement) { 246 | that.platformFilter = this.value; 247 | that.loadByCode(); 248 | }; 249 | } 250 | 251 | render() { 252 | const SelectedImg = ({ item, imageList }: { item: ImageInfo; imageList: ImageInfo[] }) => { 253 | return ( 254 |
255 | 256 |
257 | this.removeImg(item)} 260 | title="取消选择" 261 | /> 262 | this.preview(item, imageList)} 265 | title="查看大图" 266 | /> 267 |
268 |
269 | ); 270 | }; 271 | 272 | const DisplayImg = ({ item, imageList }: { item: ImageInfo; imageList: ImageInfo[] }) => { 273 | return ( 274 |
this.clickImg(item)} 276 | class={classname([ 277 | 'bgx-pic-images-option', 278 | { 'bgx-pic-images-option-selected': this.isSelected(item) }, 279 | ])} 280 | > 281 |
282 | 283 |
284 | {this.isSelected(item) && ( 285 | 286 | )} 287 | { 290 | this.preview(item, imageList); 291 | e.stopPropagation(); 292 | }} 293 | title="查看大图" 294 | /> 295 |
296 |
297 |
298 | 299 | {item.width}*{item.height} {item.type} 300 | 301 |
302 |
303 | ); 304 | }; 305 | 306 | return ( 307 |
308 |
309 | {Object.keys(this.selectedImg) 310 | .filter(Boolean) 311 | .map((code) => { 312 | return ( 313 |
{ 319 | this.currentCode = code; 320 | this.update(); 321 | }} 322 | > 323 | {code} 324 |
325 | ); 326 | })} 327 |
328 |
329 | {this.selectedImg[this.currentCode].map((item) => ( 330 | 331 | ))} 332 |
333 |
334 |
335 | this.codeChange(e.target.value)} 340 | onkeydown={(e) => e.key === 'Enter' && this.codeChange(e.target.value)} 341 | /> 342 | 357 | {/* */} 361 | 372 |
373 | 376 |
377 |
378 | {this.displayImage.map((item) => ( 379 | 380 | ))} 381 |
382 |
383 |
384 | ); 385 | } 386 | 387 | private sizeCheck(value: ImageInfo) { 388 | if ( 389 | this.bgxMaxImageSize != null && 390 | value.height > this.bgxMaxImageSize && 391 | value.width > this.bgxMaxImageSize 392 | ) { 393 | alert(`请选择长宽小于 ${this.bgxMaxImageSize} 的图片`); 394 | return false; 395 | } 396 | if ( 397 | this.bgxMinImageSize != null && 398 | value.height < this.bgxMinImageSize && 399 | value.width < this.bgxMinImageSize 400 | ) { 401 | alert(`请选择长宽大于 ${this.bgxMinImageSize} 的图片`); 402 | return false; 403 | } 404 | if (this.bgxMustSquare && value.height !== value.width) { 405 | alert(`请选择长度宽度相等的图片`); 406 | return false; 407 | } 408 | return true; 409 | } 410 | } 411 | 412 | // 挂载为jquery插件 413 | mountModal({ 414 | name: 'pictureSelector', 415 | title: '图片选择', 416 | content: PictureSelectorComponent, 417 | style: 'width: 1000px;', 418 | onOk(instance: PictureSelectorComponent) { 419 | instance?.bgxOnOk( 420 | Object.keys(instance.selectedImg).reduce((obj, key) => { 421 | obj[key] = instance.selectedImg[key].map((value) => value.url); 422 | return obj; 423 | }, {}), 424 | ); 425 | }, 426 | }); 427 | -------------------------------------------------------------------------------- /packages/jeact-components/src/pm-selector/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.less'; 2 | import { ValueComponentProps, ValueComponent, VNode, mountComponent, createVNode } from 'jeact'; 3 | 4 | export interface PmSelectorOption { 5 | value: any; 6 | label: string; 7 | checked?: boolean; 8 | parent?: PmSelectorOption; 9 | children?: PmSelectorOption[]; 10 | } 11 | 12 | export interface PmSelectorComponentProps extends ValueComponentProps { 13 | placeholder?: string; 14 | options?: any[]; 15 | valueField?: string; 16 | labelField?: string; 17 | childrenField?: string; 18 | cacheName?: string; 19 | url: string; 20 | psPlaceholder?: string; 21 | psOptions?: any[]; 22 | psValueField?: string; 23 | psLabelField?: string; 24 | psChildrenField?: string; 25 | psCacheName?: string; 26 | value?: any[]; 27 | psUrl: string; 28 | } 29 | 30 | export class PmSelectorComponent extends ValueComponent { 31 | placeholder: string; 32 | valueField: string; 33 | labelField: string; 34 | childrenField: string; 35 | cacheName: string; 36 | value: any[]; 37 | private _options: any[] = []; 38 | get options(): any[] { 39 | return this._options; 40 | } 41 | set options(value: any[]) { 42 | this._options = value; 43 | this.convertedOptions = this.convert( 44 | value, 45 | this.valueField, 46 | this.labelField, 47 | this.childrenField, 48 | null, 49 | this.value, 50 | ); 51 | this.leafOptions = this.leafChildren(this.convertedOptions); 52 | this.loadCommonOption(); 53 | this.update(); 54 | } 55 | private _url: string; 56 | get url(): string { 57 | return this._url; 58 | } 59 | 60 | set url(value: string) { 61 | this._url = value; 62 | window['$'].get(value, (res) => { 63 | if (res && res.success) { 64 | this.options = res.result; 65 | } 66 | }); 67 | } 68 | 69 | convertedOptions: PmSelectorOption[] = []; 70 | readonly saveCommonMax = 10; 71 | open = false; 72 | commonOptions: PmSelectorOption[] = []; 73 | commonCheckedAll = false; 74 | selectedIndexes: number[] = []; 75 | leafOptions: PmSelectorOption[] = []; 76 | searchText = ''; 77 | searchOptions: PmSelectorOption[] = []; 78 | searchCheckedAll = false; 79 | showSearch = true; 80 | 81 | get columns(): PmSelectorOption[][] { 82 | let list = this.convertedOptions; 83 | const result = [list]; 84 | for (let i = 0; this.selectedIndexes[i] != null; i++) { 85 | const selectedIndex = this.selectedIndexes[i]; 86 | if ( 87 | list[selectedIndex] && 88 | list[selectedIndex].children && 89 | list[selectedIndex].children.length > 0 90 | ) { 91 | list = list[selectedIndex].children; 92 | result.push(list); 93 | } else { 94 | break; 95 | } 96 | } 97 | return result; 98 | } 99 | 100 | // 获取被勾选的所有叶子节点 101 | get checkedOptions(): PmSelectorOption[] { 102 | let checkedOptions = []; 103 | let options = this.convertedOptions; // 待搜索列表 104 | while (options.length > 0) { 105 | // 新增放进待搜索列表 106 | const currChecked = options.filter( 107 | (value) => (!value.children || !value.children.length) && value.checked, 108 | ); 109 | checkedOptions = checkedOptions.concat(currChecked); 110 | options = options 111 | .filter((value) => value.children && value.children.length) 112 | .flatMap((value) => value.children); // 搜索待搜索列表 113 | } 114 | return checkedOptions; 115 | } 116 | 117 | // 用于界面展示 118 | get checkedOptionStr(): string { 119 | return this.checkedOptions.map((value) => value.label).join(','); 120 | } 121 | 122 | constructor(args: PmSelectorComponentProps) { 123 | super(args); 124 | this.placeholder = args.psPlaceholder || args.placeholder; 125 | this.valueField = args.psValueField || args.valueField || 'value'; 126 | this.labelField = args.psLabelField || args.labelField || 'label'; 127 | this.childrenField = args.psChildrenField || args.childrenField || 'children'; 128 | this.cacheName = args.psCacheName || args.cacheName || 'commonOptions'; 129 | this.value = args.value || []; 130 | if (args.psOptions || args.options) { 131 | this.options = args.psOptions || args.options; 132 | } else if (args.psUrl || args.url) { 133 | this.url = args.psUrl || args.url; 134 | } 135 | } 136 | 137 | writeValue(value: string) { 138 | this.value = value ? value.split(',') : []; 139 | if (this.convertedOptions != null) { 140 | this.leafOptions.forEach((value1) => (value1.checked = this.value.includes(value1.value))); 141 | this.update(); 142 | } 143 | } 144 | 145 | // 组件声明周期hook,当组件创建后调用,此时尚未挂载DOM 146 | beforeMount() {} 147 | 148 | // 组件声明周期hook,当组件挂载DOM后调用 149 | mounted() { 150 | document.addEventListener( 151 | 'click', 152 | (e: any) => { 153 | if (this.refs.popup) { 154 | const path = e.path || (e.composedPath && e.composedPath()); 155 | if ( 156 | !(this.refs.popup as HTMLElement).contains(e.target) && 157 | !path.includes(this.refs.popup) 158 | ) { 159 | this.closePopup(); 160 | } 161 | } 162 | if (this.refs.search) { 163 | const path = e.path || (e.composedPath && e.composedPath()); 164 | if ( 165 | !(this.refs.search as HTMLElement).contains(e.target) && 166 | !path.includes(this.refs.search) 167 | ) { 168 | this.closeSearchPopup(); 169 | } 170 | } 171 | }, 172 | true, 173 | ); 174 | } 175 | 176 | // 关闭搜索弹窗 177 | closeSearchPopup() { 178 | this.showSearch = false; 179 | this.update(); 180 | } 181 | 182 | // 打开搜索弹窗 183 | openSearchPopup = () => { 184 | this.showSearch = true; 185 | this.update(); 186 | }; 187 | 188 | // 勾选 189 | checkOption(option: PmSelectorOption, checked: boolean) { 190 | option.checked = checked; 191 | this.checkChildren(option, checked); 192 | this.checkAll(option.parent); 193 | this.checkCommonOption(); 194 | this.checkSearchOption(); 195 | this.onChange(this.checkedOptions.map((value1) => value1.value)); 196 | this.update(); 197 | } 198 | 199 | commonCheckAll = (e: Event) => { 200 | const checked = e.target['checked']; 201 | this.commonOptions.forEach((value) => this.checkOption(value, checked)); 202 | this.update(); 203 | }; 204 | 205 | leafChildren(options: PmSelectorOption[]): PmSelectorOption[] { 206 | const childrenLeaf = options.flatMap((value) => this.leafChildren(value.children)); 207 | const leaf = options.filter((value) => !value.children || !value.children.length); 208 | return [...childrenLeaf, ...leaf]; 209 | } 210 | 211 | clear = () => { 212 | this.searchText = ''; 213 | this.commonCheckedAll = false; 214 | this.checkedOptions.forEach((value) => { 215 | value.checked = false; 216 | this.checkAll(value.parent); 217 | }); 218 | this.update(); 219 | this.onChange([]); 220 | }; 221 | 222 | searchChange = (e: Event) => { 223 | this.searchText = e.target['value']; 224 | this.searchOptions = this.leafOptions.filter((value) => { 225 | return value.label && value.label.includes(this.searchText); 226 | }); 227 | this.checkSearchOption(); 228 | this.update(); 229 | }; 230 | 231 | searchCheckAll = (e) => { 232 | const checked = e.target['checked']; 233 | this.searchOptions.forEach((value) => this.checkOption(value, checked)); 234 | this.update(); 235 | }; 236 | 237 | openPopup = (e: Event) => { 238 | e.stopPropagation(); 239 | this.open = true; 240 | this.update(); 241 | }; 242 | 243 | closePopup() { 244 | this.open = false; 245 | this.searchText = ''; 246 | this.update(); 247 | } 248 | 249 | // 格式化外部option为内部option 250 | convert( 251 | options: any[], 252 | valueField, 253 | labelField, 254 | childrenField, 255 | parent: PmSelectorOption, 256 | values?: any[], 257 | ): PmSelectorOption[] { 258 | return (options || []).map((option) => { 259 | const obj: PmSelectorOption = { 260 | value: option[valueField], 261 | label: option[labelField], 262 | checked: (values || []).includes(String(option[valueField])), 263 | parent, 264 | }; 265 | obj.children = this.convert( 266 | option[childrenField] || [], 267 | valueField, 268 | labelField, 269 | childrenField, 270 | obj, 271 | values, 272 | ); 273 | return obj; 274 | }); 275 | } 276 | 277 | optionChange(e: Event, option: PmSelectorOption) { 278 | const checked = e.target['checked']; 279 | this.checkOption(option, checked); 280 | this.update(); 281 | } 282 | 283 | // 判断父节点是否需要勾选(当子节点全选时) 284 | checkAll(option: PmSelectorOption) { 285 | if (!option) { 286 | return; 287 | } 288 | const check = 289 | option.children && 290 | option.children.length > 0 && 291 | option.children.every((value) => value.checked); 292 | if (check !== option.checked) { 293 | option.checked = check; 294 | this.checkAll(option.parent); 295 | } 296 | } 297 | 298 | // 设置option的check状态,并递归更新子节点,然后saveCommonOption 299 | checkChildren(option: PmSelectorOption, check: boolean) { 300 | option.checked = check; 301 | if (option.children && option.children.length > 0) { 302 | option.children.forEach((value) => { 303 | this.checkChildren(value, check); 304 | }); 305 | } else if (check) { 306 | // 如果是被选中的叶子节点 307 | this.saveCommonOption(option); 308 | } 309 | } 310 | 311 | // 展开下一级菜单 312 | nextLevel(level: number, index: number) { 313 | this.selectedIndexes = this.selectedIndexes.slice(0, level); 314 | this.selectedIndexes[level] = index; 315 | this.update(); 316 | } 317 | 318 | // 保存常用选择到localStorage中 319 | saveCommonOption(option: PmSelectorOption) { 320 | if (this.commonOptions.includes(option)) { 321 | return; 322 | } 323 | this.commonOptions.unshift(option); 324 | if (this.commonOptions.length > this.saveCommonMax) { 325 | this.commonOptions = this.commonOptions.slice(0, this.saveCommonMax); 326 | } 327 | const commonOptions = this.commonOptions.map((value) => value.value); 328 | localStorage.setItem(this.cacheName, JSON.stringify(commonOptions)); 329 | } 330 | 331 | // 加载localStorage中的常用选择 332 | loadCommonOption() { 333 | const commonOptions: string[] = JSON.parse(localStorage.getItem(this.cacheName)) || []; 334 | this.commonOptions = commonOptions 335 | .map((value) => this.leafOptions.find((value1) => value1.value === value)) 336 | .filter((value) => value); 337 | } 338 | 339 | // 更新常用选择全选状态 340 | checkCommonOption() { 341 | this.commonCheckedAll = 342 | this.commonOptions && 343 | this.commonOptions.length && 344 | this.commonOptions.every((value) => value.checked); 345 | } 346 | 347 | // 更新搜索选择全选状态 348 | checkSearchOption() { 349 | this.searchCheckedAll = 350 | this.searchOptions && 351 | this.searchOptions.length && 352 | this.searchOptions.every((value) => value.checked); 353 | } 354 | 355 | render() { 356 | const checkedOptions = this.checkedOptions; 357 | let popup: VNode; 358 | if (this.open) { 359 | popup = ( 360 |
361 | {/*搜索栏*/} 362 | 407 | {/*常用选择*/} 408 |
409 | 418 |
419 | {this.commonOptions.map((value) => ( 420 | 429 | ))} 430 |
431 |
432 | {/*options*/} 433 |
434 | {this.columns.map( 435 | (value, level) => 436 | value && ( 437 |
438 | {value.map((value1, index) => ( 439 |
0 ? 'ps-option-next' : '', 443 | index === this.selectedIndexes[level] ? 'ps-option-selected' : '', 444 | ].join(' ')} 445 | onclick={() => this.nextLevel(level, index)} 446 | > 447 | this.optionChange(e, value1)} 451 | checked={value1.checked} 452 | /> 453 |
454 | {value1.label} 455 |
456 |
457 | ))} 458 |
459 | ), 460 | )} 461 |
462 | {/*已选择计数*/} 463 |
464 | 已选择 465 | {String(checkedOptions.length)}/{this.leafOptions.length} 466 |
467 | {/*已选择option*/} 468 |
469 | {checkedOptions.map((value) => ( 470 |
471 | {value.label} 472 | this.checkOption(value, false)}> 473 | × 474 | 475 |
476 | ))} 477 |
478 |
479 | ); 480 | } 481 | 482 | return ( 483 |
484 |
485 | 494 | 495 | {this.checkedOptions.length}项 496 | 497 |
498 | {popup} 499 |
500 | ); 501 | } 502 | } 503 | 504 | // 挂载为jquery插件 505 | mountComponent({ 506 | name: 'pmSelector', 507 | componentType: PmSelectorComponent, 508 | props: [ 509 | 'psValueField', 510 | 'psLabelField', 511 | 'psChildrenField', 512 | 'psPlaceholder', 513 | 'psUrl', 514 | 'psCacheName', 515 | ], 516 | }); 517 | -------------------------------------------------------------------------------- /packages/jeact-components/src/single-cascader/index.tsx: -------------------------------------------------------------------------------- 1 | import { ValueComponentProps, ValueComponent, VNode, mountComponent, createVNode } from 'jeact'; 2 | import './index.less'; 3 | 4 | export interface SingleCascaderOption { 5 | value: any; 6 | label: string; 7 | selected?: boolean; 8 | parent?: SingleCascaderOption; 9 | children?: SingleCascaderOption[]; 10 | } 11 | 12 | export interface SingleCascaderComponentProps extends ValueComponentProps { 13 | placeholder?: string; 14 | options?: any[]; 15 | valueField?: string; 16 | labelField?: string; 17 | childrenField?: string; 18 | cacheName?: string; 19 | value?: any; 20 | } 21 | 22 | export class SingleCascaderComponent extends ValueComponent { 23 | placeholder: string; 24 | valueField: string; 25 | labelField: string; 26 | childrenField: string; 27 | value: any; 28 | cacheName: string; 29 | private _options: any[] = []; 30 | get options(): any[] { 31 | return this._options; 32 | } 33 | set options(value: any[]) { 34 | this._options = value; 35 | this.convertedOptions = this.convert( 36 | value, 37 | this.valueField, 38 | this.labelField, 39 | this.childrenField, 40 | null, 41 | this.value, 42 | ); 43 | this.leafOptions = this.leafChildren(this.convertedOptions); 44 | if (this.selectedOptions.length) { 45 | this.selectedIndexes = this.getSelectedIndexes(this.selectedOptions[0]); 46 | this.selectedString = this.getSelectedString(this.selectedOptions[0]); 47 | } 48 | this.loadCommonOption(); 49 | this.update(); 50 | } 51 | 52 | convertedOptions: SingleCascaderOption[]; 53 | readonly saveCommonMax = 10; 54 | open = false; 55 | commonOptions: SingleCascaderOption[] = []; 56 | showCommon = true; 57 | selectedIndexes: number[] = []; 58 | selectedString = ''; 59 | leafOptions: SingleCascaderOption[] = []; 60 | searchText = ''; 61 | searchOptions: any[] = []; 62 | showSearch = false; 63 | 64 | get columns(): SingleCascaderOption[][] { 65 | let list = this.convertedOptions; 66 | const result = [list]; 67 | for (let i = 0; this.selectedIndexes[i] != null; i++) { 68 | const selectedIndex = this.selectedIndexes[i]; 69 | if ( 70 | list[selectedIndex] && 71 | list[selectedIndex].children && 72 | list[selectedIndex].children.length > 0 73 | ) { 74 | list = list[selectedIndex].children; 75 | result.push(list); 76 | } else { 77 | break; 78 | } 79 | } 80 | return result; 81 | } 82 | 83 | get selectedOptions(): SingleCascaderOption[] { 84 | return this.leafOptions.filter((value) => value.selected); 85 | } 86 | 87 | constructor(args: SingleCascaderComponentProps) { 88 | super(args); 89 | } 90 | 91 | writeValue(value: string) { 92 | this.value = value; 93 | if (this.convertedOptions != null) { 94 | this.leafOptions.forEach( 95 | (value1) => (value1.selected = String(this.value) === String(value1.value)), 96 | ); 97 | if (this.selectedOptions.length) { 98 | this.selectedIndexes = this.getSelectedIndexes(this.selectedOptions[0]); 99 | this.selectedString = this.getSelectedString(this.selectedOptions[0]); 100 | } 101 | this.update(); 102 | } 103 | } 104 | 105 | // 组件声明周期hook,当组件创建后调用,此时尚未挂载DOM 106 | beforeMount() {} 107 | 108 | // 组件声明周期hook,当组件挂载DOM后调用 109 | mounted() { 110 | document.addEventListener( 111 | 'click', 112 | (e: any) => { 113 | if (this.refs.popup) { 114 | const path = e.path || (e.composedPath && e.composedPath()); 115 | if ( 116 | !(this.refs.popup as HTMLElement).contains(e.target) && 117 | !path.includes(this.refs.popup) 118 | ) { 119 | this.closePopup(); 120 | } 121 | } 122 | if (this.refs.search) { 123 | const path = e.path || (e.composedPath && e.composedPath()); 124 | if ( 125 | !(this.refs.search as HTMLElement).contains(e.target) && 126 | !path.includes(this.refs.search) 127 | ) { 128 | this.closeSearchPopup(); 129 | } 130 | } 131 | }, 132 | true, 133 | ); 134 | } 135 | 136 | // 选择 137 | selectOption(option: SingleCascaderOption, level?: number, index?: number) { 138 | if (level != null && index != null) { 139 | this.nextLevel(level, index); 140 | } else { 141 | this.selectedIndexes = this.getSelectedIndexes(option); 142 | } 143 | if (this.isLeaf(option)) { 144 | this.leafOptions.forEach((item) => (item.selected = false)); 145 | option.selected = true; 146 | this.selectedString = this.getSelectedString(option); 147 | this.saveCommonOption(option); 148 | this.onChange(option.value); 149 | this.closePopup(); 150 | } 151 | this.update(); 152 | } 153 | 154 | // 展开下一级菜单 155 | nextLevel(level: number, index: number) { 156 | this.selectedIndexes = this.selectedIndexes.slice(0, level); 157 | this.selectedIndexes[level] = index; 158 | this.update(); 159 | } 160 | 161 | getSelectedIndexes(option: SingleCascaderOption): number[] { 162 | const indexes = []; 163 | let selectedOption = option; 164 | while (selectedOption.parent) { 165 | const index = selectedOption.parent.children.findIndex( 166 | (val) => String(val.value) === String(selectedOption.value), 167 | ); 168 | selectedOption = selectedOption.parent; 169 | indexes.unshift(index); 170 | } 171 | // 获取第一级index 172 | const firstIndex = this.convertedOptions.findIndex( 173 | (val) => String(val.value) === String(selectedOption.value), 174 | ); 175 | indexes.unshift(firstIndex); 176 | return indexes; 177 | } 178 | 179 | getSelectedString(option: SingleCascaderOption): string { 180 | const stringArr = []; 181 | let selectedOption = option; 182 | while (selectedOption.parent) { 183 | const o = selectedOption.parent.children.find( 184 | (val) => String(val.value) === String(selectedOption.value), 185 | ); 186 | selectedOption = selectedOption.parent; 187 | stringArr.unshift(o); 188 | } 189 | // 获取第一级index 190 | const firstOption = this.convertedOptions.find( 191 | (val) => String(val.value) === String(selectedOption.value), 192 | ); 193 | stringArr.unshift(firstOption); 194 | return stringArr.map((val) => val.label).join(' > '); 195 | } 196 | 197 | clear = () => { 198 | this.searchText = ''; 199 | this.searchOptions = []; 200 | this.selectedOptions.forEach((value) => { 201 | value.selected = false; 202 | }); 203 | this.selectedIndexes = []; 204 | this.selectedString = ''; 205 | this.update(); 206 | this.onChange([]); 207 | }; 208 | 209 | searchInput = (e: InputEvent) => { 210 | this.searchText = e.target['value']; 211 | this.update(); 212 | }; 213 | 214 | searchKeydown = (e: KeyboardEvent) => { 215 | if (e.code === 'Enter') { 216 | e.preventDefault(); 217 | e.stopPropagation(); 218 | this.searchChange(); 219 | } 220 | }; 221 | 222 | searchChange = () => { 223 | if (!this.searchText) { 224 | this.searchOptions = []; 225 | return; 226 | } 227 | const searchedOptionsLinked = this.searchChildren(this.convertedOptions, this.searchText); 228 | this.searchOptions = searchedOptionsLinked.map((value) => { 229 | const text = value.map((value1) => value1.label).join(' > '); 230 | return { 231 | text, 232 | html: text.replace(RegExp(this.searchText, 'ig'), (str) => str.fontcolor('red')), 233 | ids: value.map((value1) => value1.value), 234 | originOption: value[value.length - 1], 235 | }; 236 | }); 237 | this.openSearchPopup(); 238 | this.update(); 239 | }; 240 | 241 | // 递归搜索children 242 | searchChildren(options: SingleCascaderOption[], searchText: string): SingleCascaderOption[][] { 243 | if (!options) { 244 | return []; 245 | } 246 | const lowerCaseSearchText = searchText.toLowerCase(); 247 | const searchedOptions = options.filter((value) => 248 | (value.label || '').toLowerCase().includes(lowerCaseSearchText), 249 | ); 250 | const childrenOptionsLinked = this.leafChildrenLinked(searchedOptions); 251 | 252 | const notSearchedOptions = options.filter( 253 | (value) => !(value.label || '').toLowerCase().includes(lowerCaseSearchText), 254 | ); 255 | const searchedOptionsLinked = notSearchedOptions 256 | .filter((value) => !this.isLeaf(value)) 257 | .flatMap((value) => { 258 | return this.searchChildren(value.children, searchText).map((value1) => [value, ...value1]); 259 | }); 260 | return [...searchedOptionsLinked, ...childrenOptionsLinked]; 261 | } 262 | 263 | // options到叶子节点的数组的数组 264 | leafChildrenLinked(options: SingleCascaderOption[]): SingleCascaderOption[][] { 265 | if (!options) { 266 | return []; 267 | } 268 | const leafLinked = options.filter((value) => this.isLeaf(value)).map((value) => [value]); 269 | const childrenLeafLinked = options 270 | .filter((value) => !this.isLeaf(value)) 271 | .flatMap((value) => { 272 | // 所有子节点和当前节点,拼成链 273 | return this.leafChildrenLinked(value.children).map((value1) => [value, ...value1]); 274 | }); 275 | return [...leafLinked, ...childrenLeafLinked]; 276 | } 277 | 278 | // 打开搜索弹窗 279 | openSearchPopup = () => { 280 | this.showSearch = true; 281 | this.update(); 282 | }; 283 | 284 | // 关闭搜索弹窗 285 | closeSearchPopup() { 286 | this.showSearch = false; 287 | this.update(); 288 | } 289 | 290 | openPopup = (e: Event) => { 291 | e.stopPropagation(); 292 | this.open = true; 293 | this.update(); 294 | }; 295 | 296 | closePopup() { 297 | this.open = false; 298 | this.searchText = ''; 299 | this.searchOptions = []; 300 | this.closeSearchPopup(); 301 | this.update(); 302 | } 303 | 304 | switchCommon = () => { 305 | this.showCommon = !this.showCommon; 306 | this.update(); 307 | }; 308 | 309 | // 格式化外部option为内部option 310 | convert( 311 | options: any[], 312 | valueField, 313 | labelField, 314 | childrenField, 315 | parent: SingleCascaderOption, 316 | value?: any, 317 | ): SingleCascaderOption[] { 318 | return (options || []).map((option) => { 319 | const obj: SingleCascaderOption = { 320 | value: option[valueField], 321 | label: option[labelField], 322 | selected: String(value || '') === String(option[valueField]), 323 | parent, 324 | }; 325 | obj.children = this.convert( 326 | option[childrenField] || [], 327 | valueField, 328 | labelField, 329 | childrenField, 330 | obj, 331 | value, 332 | ); 333 | return obj; 334 | }); 335 | } 336 | 337 | // 获取所有叶子节点 338 | leafChildren(options: SingleCascaderOption[]): SingleCascaderOption[] { 339 | const childrenLeaf = options.flatMap((value) => this.leafChildren(value.children)); 340 | const leaf = options.filter((value) => this.isLeaf(value)); 341 | return [...childrenLeaf, ...leaf]; 342 | } 343 | 344 | isLeaf(option: SingleCascaderOption): boolean { 345 | return !option.children || !option.children.length; 346 | } 347 | 348 | // 保存常用选择到localStorage中 349 | saveCommonOption(option: SingleCascaderOption) { 350 | if (this.commonOptions.includes(option) || this.cacheName == null) { 351 | return; 352 | } 353 | this.commonOptions.unshift(option); 354 | if (this.commonOptions.length > this.saveCommonMax) { 355 | this.commonOptions = this.commonOptions.slice(0, this.saveCommonMax); 356 | } 357 | const commonOptions = this.commonOptions.map((value) => value.value); 358 | localStorage.setItem(this.cacheName, JSON.stringify(commonOptions)); 359 | } 360 | 361 | // 加载localStorage中的常用选择 362 | loadCommonOption() { 363 | const commonOptions: string[] = JSON.parse(localStorage.getItem(this.cacheName)) || []; 364 | this.commonOptions = commonOptions 365 | .map((value) => this.leafOptions.find((value1) => value1.value === value)) 366 | .filter((value) => value); 367 | } 368 | 369 | render() { 370 | let popup: VNode; 371 | if (this.open) { 372 | popup = ( 373 |
374 | {/*搜索栏*/} 375 | 422 | {/*常用选择*/} 423 |
424 |
425 | 426 | {this.showCommon ? '-' : '+'} 427 |
428 |
429 | {this.showCommon && 430 | this.commonOptions.length > 1 && 431 | this.commonOptions.map((value) => ( 432 | 442 | ))} 443 |
444 |
445 | {/*options*/} 446 |
447 | {this.columns.map( 448 | (value, level) => 449 | value && ( 450 |
451 | {value.map((value1, index) => ( 452 |
0 ? 'bgx-option-next' : '', 456 | index === this.selectedIndexes[level] ? 'bgx-option-selected' : '', 457 | ].join(' ')} 458 | onclick={() => this.selectOption(value1, level, index)} 459 | > 460 |
461 | {value1.label} 462 |
463 |
464 | ))} 465 |
466 | ), 467 | )} 468 |
469 |
470 | ); 471 | } 472 | 473 | return ( 474 |
475 |
476 | 485 |
486 | {popup} 487 |
488 | ); 489 | } 490 | } 491 | 492 | // 挂载为jquery插件 493 | mountComponent({ 494 | name: 'singleCascader', 495 | componentType: SingleCascaderComponent, 496 | props: ['valueField', 'labelField', 'childrenField', 'placeholder', 'cacheName'], 497 | }); 498 | -------------------------------------------------------------------------------- /packages/jeact-components/src/cascader/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ValueComponentProps, VNode } from 'jeact'; 2 | import { ValueComponent, createVNode, mountComponent } from 'jeact'; 3 | 4 | export type CascaderOption = { 5 | value: any; 6 | label: string; 7 | checked?: boolean; 8 | parent?: CascaderOption; 9 | children?: CascaderOption[]; 10 | } 11 | 12 | export type CascaderComponentProps = { 13 | placeholder?: string; 14 | options?: any[]; 15 | valueField?: string; 16 | labelField?: string; 17 | childrenField?: string; 18 | cacheName?: string; 19 | value?: any[]; 20 | } & ValueComponentProps 21 | 22 | export class CascaderComponent extends ValueComponent { 23 | placeholder: string; 24 | valueField: string; 25 | labelField: string; 26 | childrenField: string; 27 | cacheName: string; 28 | value: any[]; 29 | private _options: any[] = []; 30 | get options(): any[] { 31 | return this._options; 32 | } 33 | set options(value: any[]) { 34 | this._options = value; 35 | this.convertedOptions = this.convert( 36 | value, 37 | this.valueField, 38 | this.labelField, 39 | this.childrenField, 40 | null, 41 | this.value, 42 | ); 43 | this.leafOptions = this.leafChildren(this.convertedOptions); 44 | this.loadCommonOption(); 45 | this.update(); 46 | } 47 | 48 | convertedOptions: CascaderOption[] = []; 49 | readonly saveCommonMax = 10; 50 | open = false; 51 | commonOptions: CascaderOption[] = []; 52 | commonCheckedAll = false; 53 | showCommon = true; 54 | selectedIndexes: number[] = []; 55 | leafOptions: CascaderOption[] = []; 56 | searchText = ''; 57 | searchOptions: any[] = []; 58 | searchCheckedAll = false; 59 | showSearch = false; 60 | 61 | get columns(): CascaderOption[][] { 62 | let list = this.convertedOptions; 63 | const result = [list]; 64 | for (let i = 0; this.selectedIndexes[i] != null; i++) { 65 | const selectedIndex = this.selectedIndexes[i]; 66 | if ( 67 | list[selectedIndex] && 68 | list[selectedIndex].children && 69 | list[selectedIndex].children.length > 0 70 | ) { 71 | list = list[selectedIndex].children; 72 | result.push(list); 73 | } else { 74 | break; 75 | } 76 | } 77 | return result; 78 | } 79 | 80 | // 获取被勾选的所有叶子节点 81 | get checkedOptions(): CascaderOption[] { 82 | let checkedOptions = []; 83 | let options = this.convertedOptions; // 待搜索列表 84 | while (options.length > 0) { 85 | // 新增放进待搜索列表 86 | const currChecked = options.filter( 87 | (value) => (!value.children || !value.children.length) && value.checked, 88 | ); 89 | checkedOptions = checkedOptions.concat(currChecked); 90 | options = options 91 | .filter((value) => value.children && value.children.length) 92 | .flatMap((value) => value.children); // 搜索待搜索列表 93 | } 94 | return checkedOptions; 95 | } 96 | 97 | // 用于界面展示 98 | get checkedOptionStr(): string { 99 | return this.checkedOptions.map((value) => value.label).join(','); 100 | } 101 | 102 | constructor(args: CascaderComponentProps) { 103 | super(args); 104 | } 105 | 106 | writeValue(value: string) { 107 | this.value = value ? value.split(',') : []; 108 | if (this.convertedOptions != null) { 109 | this.leafOptions.forEach((value1) => { 110 | const localValue = value1; 111 | localValue.checked = this.value.includes(value1.value); 112 | }); 113 | this.update(); 114 | } 115 | } 116 | 117 | // 组件声明周期hook,当组件创建后调用,此时尚未挂载DOM 118 | beforeMount() {} 119 | 120 | // 组件声明周期hook,当组件挂载DOM后调用 121 | mounted() { 122 | document.addEventListener( 123 | 'click', 124 | (e: any) => { 125 | if (this.refs.popup) { 126 | const path = e.path || (e.composedPath && e.composedPath()); 127 | if ( 128 | !(this.refs.popup as HTMLElement).contains(e.target) && 129 | !path.includes(this.refs.popup) 130 | ) { 131 | this.closePopup(); 132 | } 133 | } 134 | if (this.refs.search) { 135 | const path = e.path || (e.composedPath && e.composedPath()); 136 | if ( 137 | !(this.refs.search as HTMLElement).contains(e.target) && 138 | !path.includes(this.refs.search) 139 | ) { 140 | this.closeSearchPopup(); 141 | } 142 | } 143 | }, 144 | true, 145 | ); 146 | } 147 | 148 | // 勾选 149 | checkOption(option: CascaderOption, checked: boolean) { 150 | option.checked = checked; 151 | this.checkChildren(option, checked); 152 | this.checkAll(option.parent); 153 | this.checkCommonOption(); 154 | this.checkSearchOption(); 155 | this.onChange(this.checkedOptions.map((value1) => value1.value)); 156 | this.update(); 157 | } 158 | 159 | // 展开下一级菜单 160 | nextLevel(level: number, index: number) { 161 | this.selectedIndexes[level] = index; 162 | this.update(); 163 | } 164 | 165 | clear = () => { 166 | this.searchText = ''; 167 | this.searchOptions = []; 168 | this.searchCheckedAll = false; 169 | this.commonCheckedAll = false; 170 | this.checkedOptions.forEach((value) => { 171 | value.checked = false; 172 | this.checkAll(value.parent); 173 | }); 174 | this.update(); 175 | this.onChange([]); 176 | }; 177 | 178 | searchInput = (e: InputEvent) => { 179 | this.searchText = (e.target as HTMLInputElement).value; 180 | this.update(); 181 | }; 182 | 183 | searchKeydown = (e: KeyboardEvent) => { 184 | if (e.code === 'Enter') { 185 | e.preventDefault(); 186 | e.stopPropagation(); 187 | this.searchChange(); 188 | } 189 | }; 190 | 191 | searchChange = () => { 192 | if (!this.searchText) { 193 | this.searchOptions = []; 194 | return; 195 | } 196 | const searchedOptionsLinked = this.searchChildren(this.convertedOptions, this.searchText); 197 | this.searchOptions = searchedOptionsLinked.map((value) => { 198 | const text = value.map((value1) => value1.label).join(' > '); 199 | return { 200 | text, 201 | html: text.replace(RegExp(this.searchText, 'ig'), (str) => str.fontcolor('red')), 202 | ids: value.map((value1) => value1.value), 203 | originOption: value[value.length - 1], 204 | }; 205 | }); 206 | this.checkSearchOption(); 207 | this.openSearchPopup(); 208 | this.update(); 209 | }; 210 | 211 | // 递归搜索children 212 | searchChildren(options: CascaderOption[], searchText: string): CascaderOption[][] { 213 | if (!options) { 214 | return []; 215 | } 216 | const lowerCaseSearchText = searchText.toLowerCase(); 217 | const searchedOptions = options.filter((value) => 218 | (value.label || '').toLowerCase().includes(lowerCaseSearchText), 219 | ); 220 | const childrenOptionsLinked = this.leafChildrenLinked(searchedOptions); 221 | 222 | const notSearchedOptions = options.filter( 223 | (value) => !(value.label || '').toLowerCase().includes(lowerCaseSearchText), 224 | ); 225 | const searchedOptionsLinked = notSearchedOptions 226 | .filter((value) => !this.isLeaf(value)) 227 | .flatMap((value) => { 228 | return this.searchChildren(value.children, searchText).map((value1) => [value, ...value1]); 229 | }); 230 | return [...searchedOptionsLinked, ...childrenOptionsLinked]; 231 | } 232 | 233 | // options到叶子节点的数组的数组 234 | leafChildrenLinked(options: CascaderOption[]): CascaderOption[][] { 235 | if (!options) { 236 | return []; 237 | } 238 | const leafLinked = options.filter((value) => this.isLeaf(value)).map((value) => [value]); 239 | const childrenLeafLinked = options 240 | .filter((value) => !this.isLeaf(value)) 241 | .flatMap((value) => { 242 | // 所有子节点和当前节点,拼成链 243 | return this.leafChildrenLinked(value.children).map((value1) => [value, ...value1]); 244 | }); 245 | return [...leafLinked, ...childrenLeafLinked]; 246 | } 247 | 248 | searchCheckAll = (e: InputEvent) => { 249 | const checked = (e.target as HTMLInputElement).checked; 250 | this.searchOptions.forEach((value) => this.checkOption(value.originOption, checked)); 251 | this.update(); 252 | }; 253 | 254 | // 打开搜索弹窗 255 | openSearchPopup = () => { 256 | this.showSearch = true; 257 | this.update(); 258 | }; 259 | 260 | // 关闭搜索弹窗 261 | closeSearchPopup() { 262 | this.showSearch = false; 263 | this.update(); 264 | } 265 | 266 | openPopup = (e: Event) => { 267 | e.stopPropagation(); 268 | this.open = true; 269 | this.update(); 270 | }; 271 | 272 | closePopup() { 273 | this.open = false; 274 | this.searchText = ''; 275 | this.searchOptions = []; 276 | this.searchCheckedAll = false; 277 | this.closeSearchPopup(); 278 | this.update(); 279 | } 280 | 281 | switchCommon = () => { 282 | this.showCommon = !this.showCommon; 283 | this.update(); 284 | }; 285 | 286 | commonCheckAll = (e: InputEvent) => { 287 | const checked = (e.target as HTMLInputElement).checked; 288 | this.commonOptions.forEach((value) => this.checkOption(value, checked)); 289 | this.update(); 290 | }; 291 | 292 | // 格式化外部option为内部option 293 | convert( 294 | options: any[], 295 | valueField, 296 | labelField, 297 | childrenField, 298 | parent: CascaderOption, 299 | values?: any[], 300 | ): CascaderOption[] { 301 | return (options || []).map((option) => { 302 | const obj: CascaderOption = { 303 | value: option[valueField], 304 | label: option[labelField], 305 | checked: (values || []).includes(String(option[valueField])), 306 | parent, 307 | }; 308 | obj.children = this.convert( 309 | option[childrenField] || [], 310 | valueField, 311 | labelField, 312 | childrenField, 313 | obj, 314 | values, 315 | ); 316 | return obj; 317 | }); 318 | } 319 | 320 | leafChildren(options: CascaderOption[]): CascaderOption[] { 321 | const childrenLeaf = options.flatMap((value) => this.leafChildren(value.children)); 322 | const leaf = options.filter((value) => this.isLeaf(value)); 323 | return [...childrenLeaf, ...leaf]; 324 | } 325 | 326 | isLeaf(option: CascaderOption): boolean { 327 | return !option.children || !option.children.length; 328 | } 329 | 330 | optionChange(e: InputEvent, option: CascaderOption) { 331 | const checked = (e.target as HTMLInputElement).checked; 332 | this.checkOption(option, checked); 333 | this.update(); 334 | } 335 | 336 | // 判断父节点是否需要勾选(当子节点全选时) 337 | checkAll(option: CascaderOption) { 338 | if (!option) { 339 | return; 340 | } 341 | const check = !this.isLeaf(option) && option.children.every((value) => value.checked); 342 | if (check !== option.checked) { 343 | option.checked = check; 344 | this.checkAll(option.parent); 345 | } 346 | } 347 | 348 | // 设置option的check状态,并递归更新子节点,然后saveCommonOption 349 | checkChildren(option: CascaderOption, check: boolean) { 350 | option.checked = check; 351 | if (!this.isLeaf(option)) { 352 | option.children.forEach((value) => { 353 | this.checkChildren(value, check); 354 | }); 355 | } else if (check) { 356 | // 如果是被选中的叶子节点 357 | this.saveCommonOption(option); 358 | } 359 | } 360 | 361 | // 保存常用选择到localStorage中 362 | saveCommonOption(option: CascaderOption) { 363 | if (this.commonOptions.includes(option) || this.cacheName == null) { 364 | return; 365 | } 366 | this.commonOptions.unshift(option); 367 | if (this.commonOptions.length > this.saveCommonMax) { 368 | this.commonOptions = this.commonOptions.slice(0, this.saveCommonMax); 369 | } 370 | const commonOptions = this.commonOptions.map((value) => value.value); 371 | localStorage.setItem(this.cacheName, JSON.stringify(commonOptions)); 372 | } 373 | 374 | // 加载localStorage中的常用选择 375 | loadCommonOption() { 376 | const commonOptions: string[] = JSON.parse(localStorage.getItem(this.cacheName)) || []; 377 | this.commonOptions = commonOptions 378 | .map((value) => this.leafOptions.find((value1) => value1.value === value)) 379 | .filter((value) => value); 380 | } 381 | 382 | // 更新常用选择全选状态 383 | checkCommonOption() { 384 | this.commonCheckedAll = 385 | this.commonOptions && 386 | this.commonOptions.length && 387 | this.commonOptions.every((value) => value.checked); 388 | } 389 | 390 | // 更新搜索选择全选状态 391 | checkSearchOption() { 392 | this.searchCheckedAll = 393 | this.searchOptions?.length && this.searchOptions.every((value) => value.originOption.checked); 394 | } 395 | 396 | render() { 397 | let popup: VNode; 398 | if (this.open) { 399 | popup = ( 400 |
401 | {/* 搜索栏 */} 402 | 459 | {/* 常用选择 */} 460 |
461 |
462 | 471 | {this.showCommon ? '-' : '+'} 472 |
473 |
474 | {this.showCommon && 475 | this.commonOptions.length > 1 && 476 | this.commonOptions.map((value) => ( 477 | 490 | ))} 491 |
492 |
493 | {/* options */} 494 |
495 | {this.columns.map( 496 | (value, level) => 497 | value && ( 498 |
499 | {value.map((value1, index) => ( 500 |
0 ? 'bgx-option-next' : '', 504 | index === this.selectedIndexes[level] ? 'bgx-option-selected' : '', 505 | ].join(' ')} 506 | onclick={() => this.nextLevel(level, index)} 507 | > 508 | this.optionChange(e, value1)} 512 | checked={value1.checked} 513 | /> 514 |
515 | {value1.label} 516 |
517 |
518 | ))} 519 |
520 | ), 521 | )} 522 |
523 | {/* 已选择计数 */} 524 |
525 | 已选择 526 | {String(this.checkedOptions.length)}/{this.leafOptions.length} 527 |
528 | {/* 已选择option */} 529 |
530 | {this.checkedOptions.map((value) => ( 531 |
532 | {value.label} 533 | this.checkOption(value, false)}> 534 | × 535 | 536 |
537 | ))} 538 |
539 |
540 | ); 541 | } 542 | 543 | return ( 544 |
545 |
546 | 555 | 556 | {this.checkedOptions.length}项 557 | 558 |
559 | {popup} 560 |
561 | ); 562 | } 563 | } 564 | 565 | // 挂载为jquery插件 566 | mountComponent({ 567 | name: 'cascader', 568 | componentType: CascaderComponent, 569 | props: ['valueField', 'labelField', 'childrenField', 'placeholder', 'cacheName'], 570 | }); 571 | -------------------------------------------------------------------------------- /docs/js/jeact.iife.js: -------------------------------------------------------------------------------- 1 | var Jeact = (function (exports) { 2 | 'use strict'; 3 | 4 | class VNode { 5 | } 6 | class VElement extends VNode { 7 | constructor(type, attributes = {}, handles = {}, children = []) { 8 | super(); 9 | this.type = type; 10 | this.attributes = attributes; 11 | this.handles = handles; 12 | this.children = children; 13 | } 14 | } 15 | class VText extends VNode { 16 | constructor(content) { 17 | super(); 18 | this.content = String(content || content === 0 ? content : ''); 19 | } 20 | } 21 | function createVNode(type, props, ...children) { 22 | let handle = {}; 23 | let attribute = {}; 24 | if (props) { 25 | handle = Object.keys(props).filter(value => value.startsWith('on')).reduce((pre, curr) => { 26 | pre[curr] = props[curr]; 27 | return pre; 28 | }, {}); 29 | attribute = Object.keys(props).filter(value => !value.startsWith('on')).reduce((pre, curr) => { 30 | pre[curr] = props[curr]; 31 | return pre; 32 | }, {}); 33 | } 34 | const vNodeChildren = children.flat(2).map(value => { 35 | return isVElement(value) ? value : new VText(value); 36 | }); 37 | return new VElement(type, attribute, handle, vNodeChildren); 38 | } 39 | function isVNode(vNode) { 40 | return vNode instanceof VNode; 41 | } 42 | function isVElement(vNode) { 43 | return vNode && vNode.type != null; 44 | } 45 | function isVText(vNode) { 46 | return vNode && vNode.content !== undefined; 47 | } 48 | function isVDom(vNode) { 49 | return isVElement(vNode) && typeof vNode.type === 'string'; 50 | } 51 | function isVComponent(vNode) { 52 | return isVElement(vNode) && typeof vNode.type === 'function' && vNode.type.prototype && vNode.type.prototype.render; 53 | } 54 | function isVFunction(vNode) { 55 | return isVElement(vNode) && typeof vNode.type === 'function' && !(vNode.type.prototype && vNode.type.prototype.render); 56 | } 57 | 58 | class DomOperate { 59 | constructor(context) { 60 | this.context = context; 61 | } 62 | createElement(vNode, rootUpdate) { 63 | var _a, _b, _c; 64 | if (isVComponent(vNode)) { 65 | vNode.component = Component.create(vNode.type, Object.assign(Object.assign(Object.assign({}, vNode.attributes), vNode.handles), { children: vNode.children, rootUpdate })); 66 | vNode.el = (_a = vNode.component.vNode) === null || _a === void 0 ? void 0 : _a.el; 67 | Object.keys(vNode.attributes).forEach(key => { 68 | const value = vNode.attributes[key]; 69 | this.setAttribute(vNode.component, key, value); 70 | }); 71 | return vNode.el; 72 | } 73 | else if (isVFunction(vNode)) { 74 | vNode.component = Component.create(FunctionComponent, { 75 | renderFunction: vNode.type, 76 | functionProps: Object.assign(Object.assign(Object.assign({}, vNode.attributes), vNode.handles), { children: vNode.children }), 77 | rootUpdate, 78 | }); 79 | vNode.el = (_b = vNode.component.vNode) === null || _b === void 0 ? void 0 : _b.el; 80 | return vNode.el; 81 | } 82 | else if (isVDom(vNode)) { 83 | const el = document.createElement(vNode.type); 84 | vNode.el = el; 85 | Object.keys(vNode.handles).forEach(key => { 86 | const value = vNode.handles[key]; 87 | const handleName = key.toLowerCase().replace(/^on/, ''); 88 | el.addEventListener(handleName, value); 89 | }); 90 | Object.keys(vNode.attributes).forEach(key => { 91 | const value = vNode.attributes[key]; 92 | this.setAttribute(el, key, value); 93 | }); 94 | (_c = vNode.children) === null || _c === void 0 ? void 0 : _c.forEach(value => { 95 | const node = this.createElement(value, rootUpdate); 96 | if (node) { 97 | el.appendChild(node); 98 | } 99 | }); 100 | return el; 101 | } 102 | else if (isVText(vNode)) { 103 | vNode.el = document.createTextNode(vNode.content); 104 | return vNode.el; 105 | } 106 | } 107 | /** 108 | * 109 | * @param el VNode对应的真是dom 110 | * @param newVNode 111 | * @param oldVNode 112 | */ 113 | updateVDom(el, newVNode, oldVNode) { 114 | if (isVDom(newVNode) && isVDom(oldVNode) && el instanceof HTMLElement) { 115 | Object.keys(oldVNode.handles).forEach(key => { 116 | if (!newVNode.handles.hasOwnProperty(key)) { 117 | const value = oldVNode.handles[key]; 118 | const handleName = key.toLowerCase().replace(/^on/, ''); 119 | el.removeEventListener(handleName, value); 120 | } 121 | }); 122 | Object.keys(newVNode.handles).forEach(key => { 123 | const handleName = key.toLowerCase().replace(/^on/, ''); 124 | const value = newVNode.handles[key]; 125 | const oldValue = oldVNode.handles[key]; 126 | if (value === oldValue) { 127 | return; 128 | } 129 | if (oldVNode.handles.hasOwnProperty(key)) { 130 | el.removeEventListener(handleName, oldValue); 131 | } 132 | el.addEventListener(handleName, value); 133 | }); 134 | Object.keys(oldVNode.attributes).forEach(key => { 135 | if (!newVNode.attributes.hasOwnProperty(key)) { 136 | this.removeAttribute(el, key, oldVNode.attributes[key]); 137 | } 138 | }); 139 | Object.keys(newVNode.attributes).forEach(key => { 140 | const value = newVNode.attributes[key]; 141 | const oldValue = oldVNode.attributes[key]; 142 | if (value === oldValue) { 143 | return; 144 | } 145 | else if (value && value !== 0) { 146 | this.setAttribute(el, key, value, oldValue); 147 | } 148 | else { 149 | this.removeAttribute(el, key, oldValue); 150 | } 151 | }); 152 | } 153 | else if (isVText(newVNode) && isVText(oldVNode) && newVNode.content !== oldVNode.content) { 154 | newVNode.el.data = newVNode.content; 155 | } 156 | } 157 | updateVText(el, newVNode, oldVNode) { 158 | if (newVNode.content !== oldVNode.content) { 159 | newVNode.el.data = newVNode.content; 160 | } 161 | } 162 | setAttribute(el, attrName, attrValue, oldValue = {}) { 163 | if (el instanceof HTMLInputElement && el.type === 'checkbox' && attrName === 'checked') { 164 | el['checked'] = attrValue; 165 | return; 166 | } 167 | if (el instanceof HTMLInputElement && attrName === 'value') { 168 | el['value'] = attrValue; 169 | return; 170 | } 171 | if (el instanceof HTMLElement && attrName === 'dangerouslySetInnerHTML') { 172 | el.innerHTML = attrValue; 173 | return; 174 | } 175 | if (typeof attrValue !== 'string' && el instanceof HTMLElement && attrName === 'style') { 176 | Object.keys(oldValue).forEach(key => { 177 | if (!attrValue.hasOwnProperty(key)) { 178 | el.style[key] = ''; 179 | } 180 | }); 181 | Object.keys(attrValue).forEach(key => { 182 | const value = attrValue[key]; 183 | if (value === oldValue[key]) { 184 | return; 185 | } 186 | else if (value && value !== 0) { 187 | el.style[key] = value; 188 | } 189 | else { 190 | el.style[key] = ''; 191 | } 192 | }); 193 | return; 194 | } 195 | if (attrName === 'ref') { 196 | this.context.refs[attrValue] = el; 197 | } 198 | if (el instanceof HTMLElement && attrValue != null) { 199 | if (attrValue === true) { 200 | el.setAttribute(attrName, ''); 201 | } 202 | else if (attrValue === false) { 203 | el.removeAttribute(attrName); 204 | } 205 | else { 206 | el.setAttribute(attrName, String(attrValue)); 207 | } 208 | } 209 | } 210 | removeAttribute(el, attrName, oldValue = {}) { 211 | if (el instanceof HTMLInputElement && el.type === 'checkbox' && attrName === 'checked') { 212 | el[attrName] = false; 213 | return; 214 | } 215 | if (el instanceof HTMLInputElement && attrName === 'value') { 216 | el[attrName] = ''; 217 | return; 218 | } 219 | if (attrName === 'dangerouslySetInnerHTML') { 220 | el.innerHTML = ''; 221 | return; 222 | } 223 | if (el instanceof HTMLElement && attrName === 'style') { 224 | Object.keys(oldValue).forEach(key => { 225 | el.style[key] = ''; 226 | }); 227 | return; 228 | } 229 | el.removeAttribute(attrName); 230 | } 231 | // 移除el的所有子节点 232 | removeChildren(el) { 233 | const children = el.childNodes; 234 | for (let i = children.length - 1; i >= 0; i--) { 235 | this.removeChild(el, children[i]); 236 | } 237 | } 238 | appendChild(parentNode, childNode) { 239 | parentNode.appendChild(childNode); 240 | } 241 | removeChild(parentNode, childNode) { 242 | parentNode.removeChild(childNode); 243 | } 244 | insertBefore(parentNode, newNode, referenceNode) { 245 | parentNode.insertBefore(newNode, referenceNode); 246 | } 247 | parentNode(node) { 248 | return node.parentNode; 249 | } 250 | nextSibling(node) { 251 | return node.nextSibling; 252 | } 253 | removeVNode(vNode) { 254 | if (isVComponent(vNode) || isVFunction(vNode)) { 255 | this.callDestroy(vNode); 256 | } 257 | const pNode = this.parentNode(vNode.el); 258 | if (pNode) { 259 | this.removeChild(pNode, vNode.el); 260 | } 261 | } 262 | // 递归销毁所有子节点 263 | callDestroy(vnode) { 264 | var _a, _b; 265 | if (isVElement(vnode)) { 266 | for (let i = 0; i < vnode.children.length; ++i) { 267 | this.callDestroy(vnode.children[i]); 268 | } 269 | } 270 | if (isVComponent(vnode)) { 271 | if ((_a = vnode.component) === null || _a === void 0 ? void 0 : _a.vNode) { 272 | this.callDestroy((_b = vnode.component) === null || _b === void 0 ? void 0 : _b.vNode); 273 | vnode.component.destroy(); 274 | } 275 | } 276 | } 277 | } 278 | 279 | class Differentiator { 280 | constructor(dom) { 281 | this.dom = dom; 282 | } 283 | /** 284 | * 判断input是否有相同的type 285 | * @param a 286 | * @param b 287 | */ 288 | sameInputType(a, b) { 289 | if (a.type !== 'input') { 290 | return true; 291 | } 292 | const aType = a.attributes.type; 293 | const bType = b.attributes.type; 294 | if (aType == null && bType == null) { 295 | return true; 296 | } 297 | return aType === bType; 298 | } 299 | /** 300 | * 判断是否是相同的VNode 301 | * @param a 302 | * @param b 303 | */ 304 | sameVNode(a, b) { 305 | if (isVDom(a) && isVDom(b)) { 306 | return (a.key === b.key && 307 | a.type === b.type && 308 | this.sameInputType(a, b) // 当标签是的时候,type必须相同 309 | ); 310 | } 311 | if (isVElement(a) && isVElement(b) && a.type === b.type) { 312 | return true; 313 | } 314 | return !!(isVText(a) && isVText(b)); 315 | } 316 | /** 317 | * 根据vNode的key生成map 318 | * @param children 待生成的vNode数组 319 | * @param beginIdx 生成范围 320 | * @param endIdx 生成范围 321 | */ 322 | createIndexMap(children, beginIdx, endIdx) { 323 | let i, key; 324 | const map = new Map(); 325 | for (i = beginIdx; i <= endIdx; ++i) { 326 | key = children[i].key; 327 | if (key != null) { 328 | map.set(key, i); 329 | } 330 | } 331 | return map; 332 | } 333 | findVNodeIndex(vNodes, targetNode, start, end) { 334 | for (let i = start; i < end; i++) { 335 | const currVNode = vNodes[i]; 336 | if (currVNode != null && this.sameVNode(targetNode, currVNode)) { 337 | return i; 338 | } 339 | } 340 | return null; 341 | } 342 | updateChildren(parentEl, oldChildren, newChildren, rootUpdate) { 343 | let oldStartIdx = 0; 344 | let oldStartVNode = oldChildren[0]; 345 | let newStartIdx = 0; 346 | let newStartVNode = newChildren[0]; 347 | let oldEndIdx = oldChildren.length - 1; 348 | let oldEndVNode = oldChildren[oldEndIdx]; 349 | let newEndIdx = newChildren.length - 1; 350 | let newEndVNode = newChildren[newEndIdx]; 351 | let oldKeyToIdx; 352 | let idxInOld; 353 | let vNodeToMove; 354 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 355 | if (oldStartVNode == null) { // 对于vnode.key的比较,会把oldVnode = null 356 | oldStartVNode = oldChildren[++oldStartIdx]; 357 | } 358 | else if (oldEndVNode == null) { 359 | oldEndVNode = oldChildren[--oldEndIdx]; 360 | } 361 | else if (this.sameVNode(oldStartVNode, newStartVNode)) { 362 | this.patchVNode(oldStartVNode, newStartVNode); 363 | oldStartVNode = oldChildren[++oldStartIdx]; 364 | newStartVNode = newChildren[++newStartIdx]; 365 | } 366 | else if (this.sameVNode(oldEndVNode, newEndVNode)) { 367 | this.patchVNode(oldEndVNode, newEndVNode); 368 | oldEndVNode = oldChildren[--oldEndIdx]; 369 | newEndVNode = newChildren[--newEndIdx]; 370 | } 371 | else if (this.sameVNode(oldStartVNode, newEndVNode)) { // VNode 右移 372 | this.patchVNode(oldStartVNode, newEndVNode); 373 | this.dom.insertBefore(parentEl, oldStartVNode.el, this.dom.nextSibling(newEndVNode.el)); 374 | oldStartVNode = oldChildren[++oldStartIdx]; 375 | newEndVNode = newChildren[--newEndIdx]; 376 | } 377 | else if (this.sameVNode(oldEndVNode, newStartVNode)) { // VNode 左移 378 | this.patchVNode(oldEndVNode, newStartVNode); 379 | this.dom.insertBefore(parentEl, oldEndVNode.el, newStartVNode.el); 380 | oldEndVNode = oldChildren[--oldEndIdx]; 381 | newStartVNode = newChildren[++newStartIdx]; 382 | } 383 | else { 384 | if (oldKeyToIdx === undefined) { 385 | oldKeyToIdx = this.createIndexMap(oldChildren, oldStartIdx, oldEndIdx); 386 | } 387 | // 根据新vNode的key在oldVNode中寻找符合条件的 388 | idxInOld = newStartVNode.key != null 389 | ? oldKeyToIdx.get(newStartVNode.key) 390 | : this.findVNodeIndex(oldChildren, newStartVNode, oldStartIdx, oldEndIdx); 391 | if (idxInOld == null) { // New element 392 | newStartVNode.el = this.dom.createElement(newStartVNode, rootUpdate); 393 | this.dom.insertBefore(parentEl, newStartVNode.el, oldStartVNode.el); 394 | } 395 | else { 396 | vNodeToMove = oldChildren[idxInOld]; 397 | if (this.sameVNode(vNodeToMove, newStartVNode)) { 398 | this.patchVNode(vNodeToMove, newStartVNode); 399 | oldChildren[idxInOld] = undefined; 400 | this.dom.insertBefore(parentEl, vNodeToMove.el, oldStartVNode.el); 401 | } 402 | else { 403 | // key相同但是element不同 404 | newStartVNode.el = this.dom.createElement(newStartVNode, rootUpdate); 405 | this.dom.insertBefore(parentEl, newStartVNode.el, oldStartVNode.el); 406 | } 407 | } 408 | newStartVNode = newChildren[++newStartIdx]; 409 | } 410 | } 411 | if (oldStartIdx > oldEndIdx) { 412 | const ref = newChildren[newEndIdx + 1]; 413 | const refEl = isVDom(ref) ? ref.el : null; 414 | for (; newStartIdx <= newEndIdx; newStartIdx++) { 415 | const el = this.dom.createElement(newChildren[newStartIdx], rootUpdate); 416 | newChildren[newStartIdx].el = el; 417 | this.dom.insertBefore(parentEl, el, refEl); 418 | } 419 | } 420 | else if (newStartIdx > newEndIdx) { 421 | for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) { 422 | const child = oldChildren[oldStartIdx]; 423 | if (child != null) { 424 | this.dom.removeVNode(child); 425 | } 426 | } 427 | } 428 | } 429 | /** 430 | * 对类型相同的的两个node同步 431 | * @param oldVNode 432 | * @param newVNode 433 | * @param rootUpdate 434 | */ 435 | patchVNode(oldVNode, newVNode, rootUpdate) { 436 | const el = newVNode.el = oldVNode.el; 437 | if (oldVNode === newVNode) { 438 | return; 439 | } 440 | if (isVText(oldVNode) && isVText(newVNode)) { 441 | this.dom.updateVText(el, newVNode, oldVNode); 442 | return; 443 | } 444 | if (isVDom(oldVNode) && isVDom(newVNode)) { 445 | this.dom.updateVDom(el, newVNode, oldVNode); 446 | const oldChildren = oldVNode.children; 447 | const newChildren = newVNode.children; 448 | if (!isEmpty(oldChildren) && !isEmpty(newChildren) && oldChildren !== newChildren) { 449 | this.updateChildren(el, oldChildren, newChildren, rootUpdate); 450 | } 451 | else if (!isEmpty(newChildren)) { 452 | newChildren.forEach(value => this.dom.appendChild(el, this.dom.createElement(value, rootUpdate))); 453 | } 454 | else if (!isEmpty(oldChildren)) { 455 | this.dom.removeChildren(el); 456 | } 457 | return; 458 | } 459 | if (isVComponent(oldVNode) && isVComponent(newVNode)) { 460 | newVNode.component = oldVNode.component; 461 | const v = oldVNode.component.runDiff(); 462 | oldVNode.el = v && v.el; 463 | return; 464 | } 465 | if (isVFunction(oldVNode) && isVFunction(newVNode)) { 466 | newVNode.component = oldVNode.component; 467 | newVNode.component.functionProps = Object.assign(Object.assign(Object.assign({}, newVNode.attributes), newVNode.handles), { children: newVNode.children }); 468 | const v = oldVNode.component.runDiff(); 469 | oldVNode.el = v && v.el; 470 | return; 471 | } 472 | } 473 | patch(oldVNode, newVNode, rootUpdate) { 474 | if (this.sameVNode(oldVNode, newVNode)) { 475 | this.patchVNode(oldVNode, newVNode); 476 | } 477 | else { 478 | const oldEl = oldVNode.el; // 当前oldVnode对应的真实元素节点 479 | const parentEl = oldEl.parentNode; // 父元素 480 | newVNode.el = this.dom.createElement(newVNode, rootUpdate); // 根据Vnode生成新元素 481 | this.dom.insertBefore(parentEl, newVNode.el, oldEl); 482 | this.dom.removeChild(parentEl, oldEl); // 将新元素添加进父元素 483 | } 484 | } 485 | } 486 | 487 | class Component { 488 | constructor(args) { 489 | this.updateFlag = false; 490 | this.dom = new DomOperate(this); 491 | this.diff = new Differentiator(this.dom); 492 | this.refs = {}; 493 | if (args) { 494 | Object.assign(this, args); 495 | } 496 | } 497 | static create(componentType, props, el) { 498 | const dom = typeof el === 'string' ? document.querySelector(el) : el; 499 | const component = new componentType(Object.assign({}, props)); 500 | component.el = dom; 501 | component.beforeMount(); 502 | component.mount(); 503 | component.mounted(); 504 | return component; 505 | } 506 | mount() { 507 | this.vNode = this.render(); 508 | const node = this.dom.createElement(this.vNode, this.update.bind(this)); 509 | this.appendToEl(node); 510 | } 511 | appendToEl(node) { 512 | if (this.el && node) { 513 | this.dom.appendChild(this.el, node); 514 | } 515 | } 516 | reappendToEl(oldNode, newNode) { 517 | if (oldNode === newNode || this.el == null) { 518 | return; 519 | } 520 | const parentNode = this.dom.parentNode(oldNode); 521 | if (parentNode == null) { 522 | return; 523 | } 524 | this.dom.removeChild(parentNode, oldNode); 525 | this.appendToEl(newNode); 526 | } 527 | update() { 528 | if (this.updateFlag) { 529 | return; 530 | } 531 | this.updateFlag = true; 532 | Promise.resolve().then(() => { 533 | this.updateFlag = false; 534 | if (this.rootUpdate) { 535 | this.rootUpdate(); 536 | return; 537 | } 538 | this.runDiff(); 539 | }); 540 | } 541 | runDiff() { 542 | if (this.vNode == null || this.vNode.el == null) { 543 | return null; 544 | } 545 | const newVNode = this.render(); 546 | this.diff.patch(this.vNode, newVNode, this.update.bind(this)); 547 | this.reappendToEl(this.vNode.el, newVNode.el); 548 | this.vNode = newVNode; 549 | return newVNode; 550 | } 551 | beforeMount() { 552 | } 553 | mounted() { 554 | } 555 | beforeUpdate() { 556 | } 557 | updated() { 558 | } 559 | destroy() { 560 | } 561 | render() { 562 | return null; 563 | } 564 | } 565 | class ValueComponent extends Component { 566 | constructor(props) { 567 | super(props); 568 | this.valueChange = props.valueChange; 569 | } 570 | mount() { 571 | if (this.el) { 572 | this.writeValue(this.el.value); 573 | } 574 | super.mount(); 575 | } 576 | readValue(value) { 577 | return value != null ? String(value) : ''; 578 | } 579 | onChange(value) { 580 | if (this.valueChange) { 581 | this.valueChange(value); 582 | } 583 | if (this.el) { 584 | this.el.value = this.readValue(value); 585 | this.el.dispatchEvent(new InputEvent('input')); 586 | this.el.dispatchEvent(new UIEvent('change')); 587 | } 588 | } 589 | appendToEl(node) { 590 | const parentNode = this.el && this.dom.parentNode(this.el); 591 | if (parentNode && node) { 592 | this.dom.insertBefore(parentNode, node, this.el); 593 | this.el.hidden = true; 594 | } 595 | } 596 | } 597 | class FunctionComponent extends Component { 598 | render() { 599 | return this.renderFunction(this.functionProps); 600 | } 601 | } 602 | 603 | // 判空 604 | function isEmpty(value) { 605 | return value == null || value.length === 0; 606 | } 607 | // 驼峰/下划线 转 短横线命名(kebab-case) 608 | function getKebabCase(str) { 609 | const reg = /^([A-Z$]+)/g; 610 | const reg2 = /_([a-zA-Z$]+)/g; 611 | const reg3 = /([A-Z$]+)/g; 612 | return str.replace(reg, ($, $1) => $1.toLowerCase()) 613 | .replace(reg2, ($, $1) => '-' + $1.toLowerCase()) 614 | .replace(reg3, ($, $1) => '-' + $1.toLowerCase()); 615 | } 616 | // 挂载为jquery插件 617 | function mountComponent({ name, componentType, props, $ = window['jQuery'] }) { 618 | if ($ == null) { 619 | return; 620 | } 621 | $.fn[name] = function (...args) { 622 | if (typeof args[0] === 'string') { 623 | const [propName, ...methodArgs] = args; 624 | const component = this.data(name); 625 | if (!component) { 626 | $.error(`节点不是一个 ${name} 组件`); 627 | } 628 | if (typeof component[propName] === 'function') { 629 | component[propName](...methodArgs); 630 | } 631 | else if (methodArgs != null && methodArgs.length === 1) { 632 | return component[propName] = methodArgs[0]; 633 | } 634 | else { 635 | return component[propName]; 636 | } 637 | } 638 | else if (args[0] == null || typeof args[0] === 'object') { 639 | const methodArgs = args[0]; 640 | const component = Component.create(componentType, methodArgs, this[0]); 641 | this.data(name, component); 642 | } 643 | else { 644 | $.error('第一个参数只能是string或object'); 645 | } 646 | return this; 647 | }; 648 | $(function () { 649 | $(`[${getKebabCase(name)}]`).each(function () { 650 | const $selected = $(this); 651 | const propsValue = (props || []).reduce((pre, curr) => { 652 | pre[curr] = $selected.attr(getKebabCase(curr)); 653 | return pre; 654 | }, {}); 655 | $selected[name](propsValue); 656 | }); 657 | }); 658 | } 659 | 660 | exports.Component = Component; 661 | exports.FunctionComponent = FunctionComponent; 662 | exports.VElement = VElement; 663 | exports.VNode = VNode; 664 | exports.VText = VText; 665 | exports.ValueComponent = ValueComponent; 666 | exports.createVNode = createVNode; 667 | exports.getKebabCase = getKebabCase; 668 | exports.isEmpty = isEmpty; 669 | exports.isVComponent = isVComponent; 670 | exports.isVDom = isVDom; 671 | exports.isVElement = isVElement; 672 | exports.isVFunction = isVFunction; 673 | exports.isVNode = isVNode; 674 | exports.isVText = isVText; 675 | exports.mountComponent = mountComponent; 676 | 677 | return exports; 678 | 679 | }({})); 680 | //# sourceMappingURL=jeact.iife.js.map 681 | --------------------------------------------------------------------------------