Browse Source
* refactor(icon): remove style dir (#6215) * refactor: rename locale * refactor: locale-provider * refactor: modal * refactor: menu * fix: custom class (#6217) * refactor: tooltip * refactor: grid (#6220) * refactor: grid * fix(grid): align & justify responsive * chore: update demo and snapshot * fix: row ts type not work * doc: update demo * refactor: ts * refactor: spin (#6222) * fix: typo (#6218) * fix: typo * docs<upload>: docs update * refactor: spin * refactor: spin * refactor: spin * refactor: spinnn * refactor: spin --------- Co-authored-by: lyn <76365499@qq.com> * fix: spin error #6222 * test: test case error (#6225) * fix: inject value maybe undefined * fix: tootip emit correct value * fix: rollback warning suffix avoid test break * doc(grid): remove unused type="flex" * refactor: skeleton (#6224) * refactor: skeleton * refactor: skeleton style * chore: modify skeleton demo style * fix(button): link and text should not have wave (#6226) * refactor: dropdown * refactor: popover & popconfirm * refactor(tag): less to cssinjs (#6227) * refactor(empty): less to cssinjs (#6230) * refactor(empty): less to cssinjs * chore: remove unuse code * fix: reactivity lose * fix: empty props #6230 * refactor: progress style (#6234) * refactor: progress * refactor: progress style * fix: progress attrs * refactor: progress #6234 * refactor: switch (#6236) * refactor: switch style * refactor: delete switch style * refactor:input (#6237) * refactor:input * fix inheritAttrs:false * fix attrs.class * feat: input add disabled * refactor:comment (#6238) * refactor:comment * fix inheritAttrs: false & attrs.class * refactor:pageheader (#6239) * refactor:pageheader * fix inheritAttrs: false & attrs.class * refactor:statistic (#6240) * refactor:statistic * fix inheritAttrs: false & attrs.class * refactor:list (#6241) * refactor:list * fix inheritAttrs: false & attrs.class * feat: update type * refactor(Space): less to cssinjs & add compact mode (#6229) * refactor(Space): less to cssinjs & add compact mode * chore(space): update md * chore(space): add demo * chore(space): add some demo * feat(button): add compact mode * fix: reactivity lose * docs: fix props version --------- Co-authored-by: tangjinzhou <415800467@qq.com> * perf: space compact * refactor:typography (#6244) * refactor:typography * fix return * fix import type * fix: typography #6244 * refactor:datepicker (#6245) * refactor: datepicker type * refactor: rate style (#6254) * refactor(layout): less to cssinjs (#6249) * doc: update layout cover * refactor(result): less to cssinjs (#6246) * refactor(result): less to cssinjs * fix: class name is overridden * docs: update result cover * refactor:slider (#6250) * feat: slider deprecated tooltipVisible * refactor(crad): less to cssinjs (#6258) * update * switch * Style adjustment * refactor(Card): less to cssinjs * Eliminate invalid code * optimization and adjustment css * Adjust the css * Optimize each item * adjustment css * refactor: card #6258 * refactor:carousel (#6262) * refactor:carousel * docs:update & refactor: carousel type --------- Co-authored-by: tangjinzhou <415800467@qq.com> * refactor:transfer (#6247) * refactor:transfer * merge v4 branch & fix theme interface conflict * docs:update & refactor: transfer type * perf: transfer * refactor:checkbox (#6248) * refactor:checkbox * docs:update & refactor: checkbox type * feat: checkbox add disabled context * refactor:pagination (#6251) * refactor:pagination * docs:update & refactor: pagination type * style: update pagination props type * refactor: mentions (#6255) * refactor: mentions * refactor: mentions menu provider * doc: update mentions demo * refcator:upload (#6261) * refcator:upload * docs:update & refactor: upload type * Update style.ts --------- Co-authored-by: tangjinzhou <415800467@qq.com> * perf: upload motion * refactor:timeline (#6263) * refactor:timeline * docs:update & refactor: timeline type * perf: timeline * refactor:steps (#6264) * refactor:steps * fix ...attrs * fix StepsToken error * docs:update & refactor: steps type * fix: steps icon clss error * refactor:collapse (#6266) * refactor:collapse * fix collapse props version * docs:update & refactor: collapse type & fix collapsible * feat: update collapse type * refactor:inputnumber (#6265) * refactor:inputnumber * docs:update & refactor: inputnumber type --------- Co-authored-by: tangjinzhou <415800467@qq.com> * feat: number add compactSize & disabledContext * refactor:table (#6267) * refactor:table * docs:update & refactor: table type --------- Co-authored-by: tangjinzhou <415800467@qq.com> * refactor: table * feat: table add expandColumnTitle slot * refactor:calendar (#6269) * refactor:calendar * docs:update * refactor:timepicker (#6270) * refactor:timepicker * docs:update & refactor: timepicker type * refactor:tree (#6276) * Feat v4 fix type errors (#6285) * fix compile type errors * fix menuprops type import * fix lint errors * fix lint errors * fix format error * fix node version * fix run dist error * fix run lint * fix as any * fix string type * refactor: rename locale file * feat: tree add leafIcon * [tabs] :less to cssinjs (#6288) * update * switch * Style adjustment * refactor(Card): less to cssinjs * tabs: less to cssinjs 开发ing * add function cssinjs * Eliminate irrelevant code * Eliminate irrelevant code 2 * update components * Eliminate irrelevant input code * refactor: tabs #6288 * feat: add segmented (#6286) * refactor: segmented #6286 * refactor:select (#6295) * refactor:select * update doc * delete useless * feat: select add context size * refactor: tree select (#6296) * feat: tree-select add context size * perf: table * docs: update doc toc * refactor: cascader * refactor: auto-complete * refactor: image * refactor: drawer * refactor:radio (#6299) * refactor:radio * fix attrs * feat: radio add disabled context * fix: some type & doc (#6292) * fix: typo (#6218) * fix: typo * docs<upload>: docs update * fix: type of minute in props disabledDateTime of DatePicker (#6233) * docs: typo (#6256) * feat: tooltip added overlayInnerStyle attribute * Update abstractTooltipProps.ts * Update Tooltip.tsx --------- Co-authored-by: lyn <76365499@qq.com> Co-authored-by: H1mple <35363759+baohangxing@users.noreply.github.com> Co-authored-by: tangjinzhou <415800467@qq.com> * refactor: form * fix: directive not work * fix: use open, remove visible * doc: update cover * refactor: remove not use code * chore: update build script * doc: update doc * doc: refactor doc * chore: update token error * chore: update style * refactor: rename _style to style * fix: tag warning * fix(dropdown): open invalid (#6316) * feat: add watermark (#6300) * feat: add watermark * feat: add watermark demo * feat: add mutationObserver * feat: add watermark demo * refactor: watermark type * doc: add theme-editor * fix: inject value maybe undefined && tag style invalid (#6320) * fix: inject value maybe undefined * fix(tag): style invalid * feat: add qrcode (#6315) * feat: add qrcode * fix: qrcode bug * fix: qrcode value required * refactor: props deconstruct * Feat v4 floatbutton (#6294) * feat: add float-button components * fix type & demo display * fix components entry * fix review bug * fix bug * fix .value * refactor: qrcode #6315 * refactor: float-button * fix: groupsize context error * fix: floatbutton animation not work * Feat v4 theme editor (#6348) * feat: add theme editor container * feat: add theme editor layout * add left panel * add vue-colorful & fix bug * 修复hue组件抖动问题 * fix bug && add demo * fix bug * fix demo preview * fix theme editor components demo * fix: token effect error * Feat v4 theme editor (#6349) * feat: add theme editor container * feat: add theme editor layout * add left panel * add vue-colorful & fix bug * 修复hue组件抖动问题 * fix bug && add demo * fix bug * fix demo preview * fix theme editor components demo * add theme editor token drawer * add theme editor token drawer * fix bug * open commment * fix error demo * fix theme editor bug * fix: cssinjs effect error * doc: format code * fix: tag click event not trigger * release 4.0.0-alpha.1 * fix: qrcode type * fix: remove not use file * doc: update doc site * doc: update site * doc: fix theme editor bgcolor (#6358) * fix: motion not work * release 4.0.0-alpha.2 * fix: qrcode ; error, close #6362 * fix docs dark theme & add docs coverDark (#6367) * fix docs dark theme & add docs coverDark * fix theme Editor edit * fix: dropdown divider disappear, close #6365 (#6369) * doc: update baner * fix: button wave not work * fix: ant-piker-cell-range-hover-end style error (#6373) * fix: ant-piker-cell-range-hover-end style error * feat: be consistent with antd * feat: be consistent with antd * fix: ConfigProvider error for style, close #6368 * release 4.0.0-alpha.4 * style: add dark style for `pre` and `code` (#6382) * docs: version menu (#6390) * Feat(DatePicker): increase presets prop (#6387) * feat(date-picker): add PresetDate type * feat(date-picker): add usePresets hook * feat(date-picker): add PresetPanel Component * feat(date-picker): add PresetPanel Component * feat(demo): update Preset Ranges Examples * feat(docs): add new prop presets * feat(docs): add new prop presets with english * fix(RangePicker): footer is not managed by panels * chore(Picker): prefixCls default rc-picker * chore(date-picker): update presetted-ranges demo * chore(date-picker): update rangePickerProps'presets * feat(date-picker): presets reactively processing * chore(date-picker): update type * refactor(RangePicker): deprecated ranges prop * chore(date-picker): update type * chore(PickerPanel): del notuse panelRef --------- Co-authored-by: tangjinzhou <415800467@qq.com> * fix: datepicker presets error #6387 * docs: update datepicker doc #6387 * feat(Steps): add items prop and variants (#6406) * refactor(steps): add items prop and variants * feat(steps): add Label Placement and Inline Steps demo * feat(steps): Label Placement and Inline Steps snap * test(steps): Steps demo snap * feat(Steps): update docs * fix(Step): progressDot * chore(useLegacyItems): change from warning to devWarning * refactor(Steps): Remove useLegacyItems * refactor(Steps): renderStep * test(Steps): update test snapshot * chore(Steps): filterEmpty * feat(Steps): update docs * docs: update site * refactor: steps #6406 * test: update steps * perf: shallowRef instead ref * fix(Modal): fix modal locale (#6423) * feat(StyleProvider): add StyleProvider handle cssinjs features (#6415) * feat(StyleProvider): StyleProvider * feat(StyleProvider): refactor to use context * chore(StyleProvider): update AStyleProviderProps type * chore(App): reback * chore(StyleProvider): export StyleProvider * feat(StyleProvider): update StyleProvider docs * feat(StyleProvider): update StyleProvider docs * feat(StyleProvider): add StyleProvider docs routes * chore(StyleProvider): with useStyleProvider * docs: update compatiple #6415 * feat(Progress): enhance size prop and add variants (#6409) * refactor(progress): Progress size and add variants * feat(progress): add `getsize` * refactor(progress): Progress size and add variants * chore(progress): update props type * chore(progress): update props type * feat(progress): update demo * feat(progress): update docs * test(progress): update test snap * fix(Circle): Merging classes * test(progress): update test snap * feat(progress): add size demo * test(progress): add size snapshot * chore(Progress): reback Circle svg class change * fix: progress borderRadius reactive #6409 * fix(defaultConfigProvider): add getPopupContainer (#6425), close #6419 * fix: qrcode size error, close #6418 * release 4.0.0-alpha.4 * fix: picker import error * test: add QRCode unit testing (#6441) * fix * fix compile type errors * fix menuprops type import * fix lint errors * fix lint errors * fix format error * fix node version * fix run dist error * fix run lint * fix as any * fix string type * fix steps error & fix docs version select option & fix theme editor error * fix(badge): badge props count default value error (#6433) * docs: update site responsive * fix: modal api method i18n not work, close #6438 * release 4.0.0-alpha.5 * chore(docs): update docs (#6446) * docs(space): update demo * docs(affix): update docs * fix: cssinjs compatibility (#6454) * feat: add convertLegacyToken * docs: v4 vuedocs (#6468) * fix introduce doc * fix getting-started doc * add migration-v4 doc * fix docs * Update migration-v4.zh-CN.md * Update migration-v4.zh-CN.md * Update migration-v4.en-US.md * Update migration-v4.zh-CN.md * Update getting-started.en-US.md * Update getting-started.zh-CN.md * Update introduce.en-US.md * Update introduce.zh-CN.md --------- Co-authored-by: tangjinzhou <415800467@qq.com> * feat: remove backtop * feat(anchor): add direction action (#6447) * refactor(anchor): direction show * refactor(anchor): update anchor css * feat(anchor): update demo * test(anchor): update demo test snap * feat(anchor): update docs * Update index.zh-CN.md * Update index.en-US.md --------- Co-authored-by: tangjinzhou <415800467@qq.com> * feat: anchor add customTitle slot #6447 * docs: update doc anchor * feat(menu): icon support function components with items and update demo (#6457) * fix(menu): icon do not show problem * fix(menu): icon do not show problem * feat(menu): update demo * test(menu): update demo snap * chore(Menu): update docs * test(Menu): update demo * Update MenuItem.tsx * Update SubMenu.tsx --------- Co-authored-by: tangjinzhou <415800467@qq.com> * doc: update menu icon * feat: menu items icon add arg * fix: antd.min error * release 4.0.0-alpha.6 * fix: table resizable not work && type error (#6514) * Refactor(demo): change options to composition api (#6499) * feat(demo): A-B * feat(demo): update B-checkbox * feat(demo): update CheckBox -DatePicker * feat(demo): update DatePicker - Form * feat(demo): update Form - List * feat(demo): update List-pagination * feat(demo): update List - skeleton * feat(demo): update skeleton - switch * feat(demo): update skeleton - switch * feat(demo): update switch - upload * feat(demo): update watermark * fix(demo): del hashId * fix: submenu type lose theme * fix: dropdown menu hide error * fix: dealing with switching topics modal, notification, message does not take effect close #6512 (#6518) * fix: resolve dark mode not support * fix: unified expression * feat(modal): add useModal (#6517) * feat(modal): add useModal hook * feat(modal): add HookModal demo * test(modal): update HookModal demo snap * feat(modal): update modal docs * chore(modal): update modal type * perf: useModal #6517 * release 4.0.0-beta.1 * docs: fix tab demo error * fix(config-provider): fix ConfigProvider.config is not function close #6528 (#6529) * Feat(use): add useMessage useNotification (#6527) * feat(Message): add useMessage hook * feat(Notification): add useNotification hook * feat(Message): add Hook demo * feat(Notification): add Hook demo * test(Message): update demo snap * test(Notification): update demo snap * docs(Message): update docs with FAQ * docs(Notification): update docs with FAQ * refactor: useMessage #6527 * refactor: useNotification #6527 * release 4.0.0-beta.2 * docs(button): update demo with space (#6536) * feat(button): demo space * test(button): update demo snap * chore(button): disabled demo Ghost space * test(button): update disabled demo snap * docs(introduce): update docs (#6539) * docs(introduce): update docs * docs(introduce): add Dollar * Update introduce.zh-CN.md * Update introduce.en-US.md --------- Co-authored-by: tangjinzhou <415800467@qq.com> * docs(customize-theme): update docs (#6540) * fix introduce doc * fix getting-started doc * add migration-v4 doc * fix docs * Update migration-v4.zh-CN.md * Update migration-v4.zh-CN.md * Update migration-v4.en-US.md * Update migration-v4.zh-CN.md * Update getting-started.en-US.md * Update getting-started.zh-CN.md * Update introduce.en-US.md * Update introduce.zh-CN.md * update customize-theme doc & fix migration-v4 error * update customize-theme doc * fix migration-v4 error * remove SSR & shadowDom * Update customize-theme.zh-CN.md * Update customize-theme.en-US.md --------- Co-authored-by: tangjinzhou <415800467@qq.com> * fix: getPopupContainer not work * release 4.0.0-beta.3 * release 4.0.0-beta.4 * docs: update grid docs (#6549) Co-authored-by: zhuzhengjian <zhuzhengjian@hoteamsoft.com> * test(alert): update demo with space (#6541) * docs(alert): update demo with space * docs(alert): update alert test snap --------- Co-authored-by: zhuzhengjian <zhuzhengjian@hoteamsoft.com> * fix: components bug & update docs (#6548) * fix bug * fix test case and update snapshot,fix space merge class * docs(grid): update migrate docs && delete xxxl in grid docs (#6562) * fix: segmentd disabled label is undefined (#6556) * fix: segmentd disabled label is undefined * fix: segmentd disabled label is undefined * fix: segmentd disabled label is undefined * fix(grid): remove grid xxxl attribute (#6572) * fix: remove grid xxxl attribute * docs: remove xxxl in grid docs * fix: tooltip custom color error * feat: remove Step __legacy * feat: add tour (#6332) * feat v4 add tour * fix type error * sync tour from antd5.4.6 & fix type error * fix error * refactor: tour #6332 * fix: tour center * fix: picker support v-show * test: update snap * test: update tour test * fix: tour-mask attrs pointer-events (#6577) * fix: tour animated * feat: support vue 3.3 slot type * release 4.0.0-rc.1 * release 4.0.0-rc.2, close #6588 * 4.0.0-rc.3 * chore: remove vue private api * fix: paginantion error, close #6590 * release 4.0.0-rc.4 * fix: checxbox style * fix: pagination mini size style * release 4.0.0-rc.5 * docs: update v4 tabs doc error(#6606) (#6607) * docs: add ant-design-vue nuxt module (#6620) * fix: layout-sider and menu transition style(#6637) (#6640) * docs: fixed the style error of online demo (#6630) * feat: ✨checkbox label slot support use option label (#6642) * docs: 📃change the default setting of "treeNodeFilterProp" from "value" to "label" * revert: ↩revert this config and create another pr to commit * feat: ✨checkbox label slot support use option label * test: 🧪update checkbox *.snap file --------- Co-authored-by: tangjinzhou <415800467@qq.com> * fix: add disabledContext override with form components (#6618) * fix: add disabledContext override with form components * test: update snap * fix: LabelWidth demo filename * fix: fontsize spelling mistake * fix(tour): target position (#6629) * style: format lint * docs(form): add form disabled demo (#6658) * fix: comment node error * release 4.0 * fix: portalWrapper add autoLock prop (#6687), close #6649 * fix: image animation & zindex, close #6675 * docs(QRCode): Synchronize QR code demonstration and add SVG (#6660) * fix: Synchronize QR code demonstration and add SVG * fix: responsive loss and invalid border style * docs: synchronize antd5.6.3 QRCode color in dark mode * feat: calendar select support info.source param (#6697) * docs: add ant-design-vue nuxt module * feat: calendar select support info.source param * docs: synchronous config-provider demo (#6706) * revert: #6706 * docs: export space-compact types (#6716) * release 4.0.0 --------- Co-authored-by: bqy_fe <1743369777@qq.com> Co-authored-by: zkwolf <chenhao5866@gmail.com> Co-authored-by: Zev Zhu <45655660+aibayanyu20@users.noreply.github.com> Co-authored-by: lyn <76365499@qq.com> Co-authored-by: 果冻橙 <shifeng199307@gmail.com> Co-authored-by: songsong0707 <74165917+songsong0707@users.noreply.github.com> Co-authored-by: yang <30883395+webvs2@users.noreply.github.com> Co-authored-by: selicens <1244620067@qq.com> Co-authored-by: 一堆菠萝 <53335668+JavanShen@users.noreply.github.com> Co-authored-by: H1mple <35363759+baohangxing@users.noreply.github.com> Co-authored-by: Cherry7 <79909910+CCherry07@users.noreply.github.com> Co-authored-by: Konv Suu <2583695112@qq.com> Co-authored-by: luoawai <32483950+luoawai@users.noreply.github.com> Co-authored-by: 鱼见 <657715602@qq.com> Co-authored-by: zhuzhengjian <zhuzhengjian@hoteamsoft.com> Co-authored-by: Cupid Valentine <53572196+valcosmos@users.noreply.github.com> Co-authored-by: 专业逮虾户aa <30494925+waldonUB@users.noreply.github.com> Co-authored-by: PanStar <PanStar@users.noreply.github.com>pull/6719/head 4.0.0
tangjinzhou
1 year ago
committed by
GitHub
2425 changed files with 203640 additions and 60975 deletions
@ -1,195 +1,36 @@
|
||||
const fs = require('fs'); |
||||
const path = require('path'); |
||||
const defaultVars = require('./scripts/default-vars'); |
||||
const darkVars = require('./scripts/dark-vars'); |
||||
const compactVars = require('./scripts/compact-vars'); |
||||
|
||||
function generateThemeFileContent(theme) { |
||||
return `const { ${theme}ThemeSingle } = require('./theme');\nconst defaultTheme = require('./default-theme');\n |
||||
module.exports = { |
||||
...defaultTheme, |
||||
...${theme}ThemeSingle |
||||
}`;
|
||||
} |
||||
const restCssPath = path.join(process.cwd(), 'components', 'style', 'reset.css'); |
||||
const tokenStatisticPath = path.join(process.cwd(), 'components', 'version', 'token.json'); |
||||
const tokenMetaPath = path.join(process.cwd(), 'components', 'version', 'token-meta.json'); |
||||
|
||||
// We need compile additional content for antd user
|
||||
function finalizeCompile() { |
||||
if (fs.existsSync(path.join(__dirname, './lib'))) { |
||||
// Build a entry less file to dist/antd.less
|
||||
const componentsPath = path.join(process.cwd(), 'components'); |
||||
let componentsLessContent = ''; |
||||
// Build components in one file: lib/style/components.less
|
||||
fs.readdir(componentsPath, (err, files) => { |
||||
files.forEach(file => { |
||||
if (fs.existsSync(path.join(componentsPath, file, 'style', 'index.less'))) { |
||||
componentsLessContent += `@import "../${path.posix.join( |
||||
file, |
||||
'style', |
||||
'index-pure.less', |
||||
)}";\n`;
|
||||
} |
||||
}); |
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'lib', 'style', 'components.less'), |
||||
componentsLessContent, |
||||
); |
||||
}); |
||||
if (fs.existsSync(path.join(__dirname, './es'))) { |
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'es', 'style', 'reset.css')); |
||||
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'es', 'version', 'token.json')); |
||||
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'es', 'version', 'token-meta.json')); |
||||
} |
||||
} |
||||
|
||||
function buildThemeFile(theme, vars) { |
||||
// Build less entry file: dist/antd.${theme}.less
|
||||
if (theme !== 'default') { |
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', `antd.${theme}.less`), |
||||
`@import "../lib/style/${theme}.less";\n@import "../lib/style/components.less";`, |
||||
); |
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Built a entry less file to dist/antd.${theme}.less`); |
||||
} else { |
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', `default-theme.js`), |
||||
`module.exports = ${JSON.stringify(vars, null, 2)};\n`, |
||||
); |
||||
return; |
||||
if (fs.existsSync(path.join(__dirname, './lib'))) { |
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'lib', 'style', 'reset.css')); |
||||
fs.copyFileSync(tokenStatisticPath, path.join(process.cwd(), 'lib', 'version', 'token.json')); |
||||
fs.copyFileSync(tokenMetaPath, path.join(process.cwd(), 'lib', 'version', 'token-meta.json')); |
||||
} |
||||
|
||||
// Build ${theme}.js: dist/${theme}-theme.js, for less-loader
|
||||
|
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', `theme.js`), |
||||
`const ${theme}ThemeSingle = ${JSON.stringify(vars, null, 2)};\n`, |
||||
{ |
||||
flag: 'a', |
||||
}, |
||||
); |
||||
|
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', `${theme}-theme.js`), |
||||
generateThemeFileContent(theme), |
||||
); |
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Built a ${theme} theme js file to dist/${theme}-theme.js`); |
||||
} |
||||
|
||||
function finalizeDist() { |
||||
if (fs.existsSync(path.join(__dirname, './dist'))) { |
||||
// Build less entry file: dist/antd.less
|
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', 'antd.less'), |
||||
'@import "../lib/style/default.less";\n@import "../lib/style/components.less";', |
||||
); |
||||
// eslint-disable-next-line no-console
|
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', 'theme.js'), |
||||
`const defaultTheme = require('./default-theme.js');\n`, |
||||
); |
||||
// eslint-disable-next-line no-console
|
||||
console.log('Built a entry less file to dist/antd.less'); |
||||
buildThemeFile('default', defaultVars); |
||||
buildThemeFile('dark', darkVars); |
||||
buildThemeFile('compact', compactVars); |
||||
buildThemeFile('variable', {}); |
||||
fs.writeFileSync( |
||||
path.join(process.cwd(), 'dist', `theme.js`), |
||||
` |
||||
function getThemeVariables(options = {}) { |
||||
let themeVar = { |
||||
'hack': \`true;@import "\${require.resolve('ant-design-vue/lib/style/color/colorPalette.less')}";\`,
|
||||
...defaultTheme |
||||
}; |
||||
if(options.dark) { |
||||
themeVar = { |
||||
...themeVar, |
||||
...darkThemeSingle |
||||
} |
||||
} |
||||
if(options.compact){ |
||||
themeVar = { |
||||
...themeVar, |
||||
...compactThemeSingle |
||||
} |
||||
fs.copyFileSync(restCssPath, path.join(process.cwd(), 'dist', 'reset.css')); |
||||
} |
||||
return themeVar; |
||||
} |
||||
|
||||
module.exports = { |
||||
darkThemeSingle, |
||||
compactThemeSingle, |
||||
getThemeVariables |
||||
}`,
|
||||
{ |
||||
flag: 'a', |
||||
}, |
||||
); |
||||
} |
||||
} |
||||
|
||||
function isComponentStyleEntry(file) { |
||||
return file.path.match(/style(\/|\\)index\.tsx/); |
||||
} |
||||
|
||||
function needTransformStyle(content) { |
||||
return content.includes('../../style/index.less') || content.includes('./index.less'); |
||||
} |
||||
|
||||
module.exports = { |
||||
compile: { |
||||
includeLessFile: [/(\/|\\)components(\/|\\)style(\/|\\)default.less$/], |
||||
transformTSFile(file) { |
||||
if (isComponentStyleEntry(file)) { |
||||
let content = file.contents.toString(); |
||||
|
||||
if (needTransformStyle(content)) { |
||||
const cloneFile = file.clone(); |
||||
|
||||
// Origin
|
||||
content = content.replace('../../style/index.less', '../../style/default.less'); |
||||
cloneFile.contents = Buffer.from(content); |
||||
|
||||
return cloneFile; |
||||
} |
||||
} |
||||
}, |
||||
transformFile(file) { |
||||
if (isComponentStyleEntry(file)) { |
||||
const indexLessFilePath = file.path.replace('index.tsx', 'index.less'); |
||||
|
||||
if (fs.existsSync(indexLessFilePath)) { |
||||
// We put origin `index.less` file to `index-pure.less`
|
||||
const pureFile = file.clone(); |
||||
pureFile.contents = Buffer.from(fs.readFileSync(indexLessFilePath, 'utf8')); |
||||
pureFile.path = pureFile.path.replace('index.tsx', 'index-pure.less'); |
||||
|
||||
// Rewrite `index.less` file with `root-entry-name`
|
||||
const indexLessFile = file.clone(); |
||||
indexLessFile.contents = Buffer.from( |
||||
[ |
||||
// Inject variable
|
||||
'@root-entry-name: default;', |
||||
// Point to origin file
|
||||
"@import './index-pure.less';", |
||||
].join('\n\n'), |
||||
); |
||||
indexLessFile.path = indexLessFile.path.replace('index.tsx', 'index.less'); |
||||
|
||||
return [indexLessFile, pureFile]; |
||||
} |
||||
} |
||||
|
||||
return []; |
||||
}, |
||||
lessConfig: { |
||||
modifyVars: { |
||||
'root-entry-name': 'default', |
||||
}, |
||||
}, |
||||
finalize: finalizeCompile, |
||||
}, |
||||
dist: { |
||||
finalize: finalizeDist, |
||||
}, |
||||
generateThemeFileContent, |
||||
bail: true, |
||||
}; |
||||
|
@ -1,27 +0,0 @@
|
||||
const less = require('less'); |
||||
const path = require('path'); |
||||
const postcss = require('postcss'); |
||||
const autoprefixer = require('autoprefixer'); |
||||
const NpmImportPlugin = require('less-plugin-npm-import'); |
||||
const { getConfig } = require('./utils/projectHelper'); |
||||
|
||||
function transformLess(lessContent, lessFilePath, config = {}) { |
||||
const { cwd = process.cwd() } = config; |
||||
const { compile: { lessConfig } = {} } = getConfig(); |
||||
const resolvedLessFile = path.resolve(cwd, lessFilePath); |
||||
|
||||
// Do less compile
|
||||
const lessOpts = { |
||||
paths: [path.dirname(resolvedLessFile)], |
||||
filename: resolvedLessFile, |
||||
plugins: [new NpmImportPlugin({ prefix: '~' })], |
||||
javascriptEnabled: true, |
||||
...lessConfig, |
||||
}; |
||||
return less |
||||
.render(lessContent, lessOpts) |
||||
.then(result => postcss([autoprefixer]).process(result.css, { from: undefined })) |
||||
.then(r => r.css); |
||||
} |
||||
|
||||
module.exports = transformLess; |
@ -1,11 +0,0 @@
|
||||
// We convert less import in es/lib to css file path
|
||||
function cssInjection(content) { |
||||
return content |
||||
.replace(/\/style\/?'/g, "/style/css'") |
||||
.replace(/\/style\/?"/g, '/style/css"') |
||||
.replace(/\.less/g, '.css'); |
||||
} |
||||
|
||||
module.exports = { |
||||
cssInjection, |
||||
}; |
@ -1,23 +1,34 @@
|
||||
import type { ElementOf } from './type'; |
||||
import { tuple } from './type'; |
||||
import type { PresetColorKey } from '../theme/interface'; |
||||
import { PresetColors } from '../theme/interface'; |
||||
|
||||
export const PresetStatusColorTypes = tuple('success', 'processing', 'error', 'default', 'warning'); |
||||
type InverseColor = `${PresetColorKey}-inverse`; |
||||
const inverseColors = PresetColors.map<InverseColor>(color => `${color}-inverse`); |
||||
|
||||
export const PresetColorTypes = tuple( |
||||
'pink', |
||||
'red', |
||||
'yellow', |
||||
'orange', |
||||
'cyan', |
||||
'green', |
||||
'blue', |
||||
'purple', |
||||
'geekblue', |
||||
'magenta', |
||||
'volcano', |
||||
'gold', |
||||
'lime', |
||||
); |
||||
export const PresetStatusColorTypes = [ |
||||
'success', |
||||
'processing', |
||||
'error', |
||||
'default', |
||||
'warning', |
||||
] as const; |
||||
|
||||
export type PresetColorType = ElementOf<typeof PresetColorTypes>; |
||||
export type PresetStatusColorType = ElementOf<typeof PresetStatusColorTypes>; |
||||
export type PresetColorType = PresetColorKey | InverseColor; |
||||
|
||||
export type PresetStatusColorType = (typeof PresetStatusColorTypes)[number]; |
||||
|
||||
/** |
||||
* determine if the color keyword belongs to the `Ant Design` {@link PresetColors}. |
||||
* @param color color to be judged |
||||
* @param includeInverse whether to include reversed colors |
||||
*/ |
||||
export function isPresetColor(color?: any, includeInverse = true) { |
||||
if (includeInverse) { |
||||
return [...inverseColors, ...PresetColors].includes(color); |
||||
} |
||||
|
||||
return PresetColors.includes(color); |
||||
} |
||||
|
||||
export function isPresetStatusColor(color?: any): color is PresetStatusColorType { |
||||
return PresetStatusColorTypes.includes(color); |
||||
} |
||||
|
@ -0,0 +1,22 @@
|
||||
import { inject, provide, reactive, watchEffect } from 'vue'; |
||||
|
||||
function createContext<T extends Record<string, any>>(defaultValue?: T) { |
||||
const contextKey = Symbol('contextKey'); |
||||
const useProvide = (props: T, newProps?: T) => { |
||||
const mergedProps = reactive<T>({} as T); |
||||
provide(contextKey, mergedProps); |
||||
watchEffect(() => { |
||||
Object.assign(mergedProps, props, newProps || {}); |
||||
}); |
||||
return mergedProps; |
||||
}; |
||||
const useInject = () => { |
||||
return inject(contextKey, defaultValue as T) || ({} as T); |
||||
}; |
||||
return { |
||||
useProvide, |
||||
useInject, |
||||
}; |
||||
} |
||||
|
||||
export default createContext; |
@ -0,0 +1,25 @@
|
||||
export type KeyType = string | number; |
||||
type ValueType = [number, any]; // [times, realValue]
|
||||
|
||||
class Entity { |
||||
/** @private Internal cache map. Do not access this directly */ |
||||
cache = new Map<string, ValueType>(); |
||||
|
||||
get(keys: KeyType[] | string): ValueType | null { |
||||
return this.cache.get(Array.isArray(keys) ? keys.join('%') : keys) || null; |
||||
} |
||||
|
||||
update(keys: KeyType[] | string, valueFn: (origin: ValueType | null) => ValueType | null) { |
||||
const path = Array.isArray(keys) ? keys.join('%') : keys; |
||||
const prevValue = this.cache.get(path)!; |
||||
const nextValue = valueFn(prevValue); |
||||
|
||||
if (nextValue === null) { |
||||
this.cache.delete(path); |
||||
} else { |
||||
this.cache.set(path, nextValue); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export default Entity; |
@ -0,0 +1,19 @@
|
||||
import type { CSSInterpolation } from './hooks/useStyleRegister'; |
||||
|
||||
class Keyframe { |
||||
private name: string; |
||||
style: CSSInterpolation; |
||||
|
||||
constructor(name: string, style: CSSInterpolation) { |
||||
this.name = name; |
||||
this.style = style; |
||||
} |
||||
|
||||
getName(hashId = ''): string { |
||||
return hashId ? `${hashId}-${this.name}` : this.name; |
||||
} |
||||
|
||||
_keyframe = true; |
||||
} |
||||
|
||||
export default Keyframe; |
@ -0,0 +1,157 @@
|
||||
import type { ShallowRef, ExtractPropTypes, InjectionKey, Ref } from 'vue'; |
||||
import { provide, defineComponent, unref, inject, watch, shallowRef } from 'vue'; |
||||
import CacheEntity from './Cache'; |
||||
import type { Linter } from './linters/interface'; |
||||
import type { Transformer } from './transformers/interface'; |
||||
import { arrayType, booleanType, objectType, someType, stringType, withInstall } from '../type'; |
||||
import initDefaultProps from '../props-util/initDefaultProps'; |
||||
export const ATTR_TOKEN = 'data-token-hash'; |
||||
export const ATTR_MARK = 'data-css-hash'; |
||||
export const ATTR_DEV_CACHE_PATH = 'data-dev-cache-path'; |
||||
|
||||
// Mark css-in-js instance in style element |
||||
export const CSS_IN_JS_INSTANCE = '__cssinjs_instance__'; |
||||
export const CSS_IN_JS_INSTANCE_ID = Math.random().toString(12).slice(2); |
||||
|
||||
export function createCache() { |
||||
if (typeof document !== 'undefined' && document.head && document.body) { |
||||
const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || []; |
||||
const { firstChild } = document.head; |
||||
|
||||
Array.from(styles).forEach(style => { |
||||
(style as any)[CSS_IN_JS_INSTANCE] = |
||||
(style as any)[CSS_IN_JS_INSTANCE] || CSS_IN_JS_INSTANCE_ID; |
||||
|
||||
// Not force move if no head |
||||
document.head.insertBefore(style, firstChild); |
||||
}); |
||||
|
||||
// Deduplicate of moved styles |
||||
const styleHash: Record<string, boolean> = {}; |
||||
Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(style => { |
||||
const hash = style.getAttribute(ATTR_MARK)!; |
||||
if (styleHash[hash]) { |
||||
if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { |
||||
style.parentNode?.removeChild(style); |
||||
} |
||||
} else { |
||||
styleHash[hash] = true; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return new CacheEntity(); |
||||
} |
||||
|
||||
export type HashPriority = 'low' | 'high'; |
||||
|
||||
export interface StyleContextProps { |
||||
autoClear?: boolean; |
||||
/** @private Test only. Not work in production. */ |
||||
mock?: 'server' | 'client'; |
||||
/** |
||||
* Only set when you need ssr to extract style on you own. |
||||
* If not provided, it will auto create <style /> on the end of Provider in server side. |
||||
*/ |
||||
cache: CacheEntity; |
||||
/** Tell children that this context is default generated context */ |
||||
defaultCache: boolean; |
||||
/** Use `:where` selector to reduce hashId css selector priority */ |
||||
hashPriority?: HashPriority; |
||||
/** Tell cssinjs where to inject style in */ |
||||
container?: Element | ShadowRoot; |
||||
/** Component wil render inline `<style />` for fallback in SSR. Not recommend. */ |
||||
ssrInline?: boolean; |
||||
/** Transform css before inject in document. Please note that `transformers` do not support dynamic update */ |
||||
transformers?: Transformer[]; |
||||
/** |
||||
* Linters to lint css before inject in document. |
||||
* Styles will be linted after transforming. |
||||
* Please note that `linters` do not support dynamic update. |
||||
*/ |
||||
linters?: Linter[]; |
||||
} |
||||
|
||||
const StyleContextKey: InjectionKey<ShallowRef<Partial<StyleContextProps>>> = |
||||
Symbol('StyleContextKey'); |
||||
|
||||
export type UseStyleProviderProps = Partial<StyleContextProps> | Ref<Partial<StyleContextProps>>; |
||||
const defaultStyleContext: StyleContextProps = { |
||||
cache: createCache(), |
||||
defaultCache: true, |
||||
hashPriority: 'low', |
||||
}; |
||||
export const useStyleInject = () => { |
||||
return inject(StyleContextKey, shallowRef({ ...defaultStyleContext })); |
||||
}; |
||||
export const useStyleProvider = (props: UseStyleProviderProps) => { |
||||
const parentContext = useStyleInject(); |
||||
const context = shallowRef<Partial<StyleContextProps>>({ ...defaultStyleContext }); |
||||
watch( |
||||
[props, parentContext], |
||||
() => { |
||||
const mergedContext: Partial<StyleContextProps> = { |
||||
...parentContext.value, |
||||
}; |
||||
const propsValue = unref(props); |
||||
Object.keys(propsValue).forEach(key => { |
||||
const value = propsValue[key]; |
||||
if (propsValue[key] !== undefined) { |
||||
mergedContext[key] = value; |
||||
} |
||||
}); |
||||
|
||||
const { cache } = propsValue; |
||||
mergedContext.cache = mergedContext.cache || createCache(); |
||||
mergedContext.defaultCache = !cache && parentContext.value.defaultCache; |
||||
context.value = mergedContext; |
||||
}, |
||||
{ immediate: true }, |
||||
); |
||||
provide(StyleContextKey, context); |
||||
return context; |
||||
}; |
||||
export const styleProviderProps = () => ({ |
||||
autoClear: booleanType(), |
||||
/** @private Test only. Not work in production. */ |
||||
mock: stringType<'server' | 'client'>(), |
||||
/** |
||||
* Only set when you need ssr to extract style on you own. |
||||
* If not provided, it will auto create <style /> on the end of Provider in server side. |
||||
*/ |
||||
cache: objectType<CacheEntity>(), |
||||
/** Tell children that this context is default generated context */ |
||||
defaultCache: booleanType(), |
||||
/** Use `:where` selector to reduce hashId css selector priority */ |
||||
hashPriority: stringType<HashPriority>(), |
||||
/** Tell cssinjs where to inject style in */ |
||||
container: someType<Element | ShadowRoot>(), |
||||
/** Component wil render inline `<style />` for fallback in SSR. Not recommend. */ |
||||
ssrInline: booleanType(), |
||||
/** Transform css before inject in document. Please note that `transformers` do not support dynamic update */ |
||||
transformers: arrayType<Transformer[]>(), |
||||
/** |
||||
* Linters to lint css before inject in document. |
||||
* Styles will be linted after transforming. |
||||
* Please note that `linters` do not support dynamic update. |
||||
*/ |
||||
linters: arrayType<Linter[]>(), |
||||
}); |
||||
export type StyleProviderProps = Partial<ExtractPropTypes<ReturnType<typeof styleProviderProps>>>; |
||||
export const StyleProvider = withInstall( |
||||
defineComponent({ |
||||
name: 'AStyleProvider', |
||||
inheritAttrs: false, |
||||
props: initDefaultProps(styleProviderProps(), defaultStyleContext), |
||||
setup(props, { slots }) { |
||||
useStyleProvider(props); |
||||
return () => slots.default?.(); |
||||
}, |
||||
}), |
||||
); |
||||
|
||||
export default { |
||||
useStyleInject, |
||||
useStyleProvider, |
||||
StyleProvider, |
||||
}; |
@ -0,0 +1,128 @@
|
||||
import hash from '@emotion/hash'; |
||||
import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, CSS_IN_JS_INSTANCE_ID } from '../StyleContext'; |
||||
import type Theme from '../theme/Theme'; |
||||
import useGlobalCache from './useGlobalCache'; |
||||
import { flattenToken, token2key } from '../util'; |
||||
import type { Ref } from 'vue'; |
||||
import { ref, computed } from 'vue'; |
||||
|
||||
const EMPTY_OVERRIDE = {}; |
||||
|
||||
// Generate different prefix to make user selector break in production env. |
||||
// This helps developer not to do style override directly on the hash id. |
||||
const hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css'; |
||||
|
||||
export interface Option<DerivativeToken> { |
||||
/** |
||||
* Generate token with salt. |
||||
* This is used to generate different hashId even same derivative token for different version. |
||||
*/ |
||||
salt?: string; |
||||
override?: object; |
||||
/** |
||||
* Format token as you need. Such as: |
||||
* |
||||
* - rename token |
||||
* - merge token |
||||
* - delete token |
||||
* |
||||
* This should always be the same since it's one time process. |
||||
* It's ok to useMemo outside but this has better cache strategy. |
||||
*/ |
||||
formatToken?: (mergedToken: any) => DerivativeToken; |
||||
} |
||||
|
||||
const tokenKeys = new Map<string, number>(); |
||||
function recordCleanToken(tokenKey: string) { |
||||
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) + 1); |
||||
} |
||||
|
||||
function removeStyleTags(key: string) { |
||||
if (typeof document !== 'undefined') { |
||||
const styles = document.querySelectorAll(`style[${ATTR_TOKEN}="${key}"]`); |
||||
|
||||
styles.forEach(style => { |
||||
if ((style as any)[CSS_IN_JS_INSTANCE] === CSS_IN_JS_INSTANCE_ID) { |
||||
style.parentNode?.removeChild(style); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// Remove will check current keys first |
||||
function cleanTokenStyle(tokenKey: string) { |
||||
tokenKeys.set(tokenKey, (tokenKeys.get(tokenKey) || 0) - 1); |
||||
|
||||
const tokenKeyList = Array.from(tokenKeys.keys()); |
||||
const cleanableKeyList = tokenKeyList.filter(key => { |
||||
const count = tokenKeys.get(key) || 0; |
||||
|
||||
return count <= 0; |
||||
}); |
||||
|
||||
if (cleanableKeyList.length < tokenKeyList.length) { |
||||
cleanableKeyList.forEach(key => { |
||||
removeStyleTags(key); |
||||
tokenKeys.delete(key); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Cache theme derivative token as global shared one |
||||
* @param theme Theme entity |
||||
* @param tokens List of tokens, used for cache. Please do not dynamic generate object directly |
||||
* @param option Additional config |
||||
* @returns Call Theme.getDerivativeToken(tokenObject) to get token |
||||
*/ |
||||
export default function useCacheToken<DerivativeToken = object, DesignToken = DerivativeToken>( |
||||
theme: Ref<Theme<any, any>>, |
||||
tokens: Ref<Partial<DesignToken>[]>, |
||||
option: Ref<Option<DerivativeToken>> = ref({}), |
||||
) { |
||||
// Basic - We do basic cache here |
||||
const mergedToken = computed(() => Object.assign({}, ...tokens.value)); |
||||
const tokenStr = computed(() => flattenToken(mergedToken.value)); |
||||
const overrideTokenStr = computed(() => flattenToken(option.value.override || EMPTY_OVERRIDE)); |
||||
|
||||
const cachedToken = useGlobalCache<[DerivativeToken & { _tokenKey: string }, string]>( |
||||
'token', |
||||
computed(() => [ |
||||
option.value.salt || '', |
||||
theme.value.id, |
||||
tokenStr.value, |
||||
overrideTokenStr.value, |
||||
]), |
||||
() => { |
||||
const { salt = '', override = EMPTY_OVERRIDE, formatToken } = option.value; |
||||
const derivativeToken = theme.value.getDerivativeToken(mergedToken.value); |
||||
|
||||
// Merge with override |
||||
let mergedDerivativeToken = { |
||||
...derivativeToken, |
||||
...override, |
||||
}; |
||||
|
||||
// Format if needed |
||||
if (formatToken) { |
||||
mergedDerivativeToken = formatToken(mergedDerivativeToken); |
||||
} |
||||
|
||||
// Optimize for `useStyleRegister` performance |
||||
const tokenKey = token2key(mergedDerivativeToken, salt); |
||||
mergedDerivativeToken._tokenKey = tokenKey; |
||||
recordCleanToken(tokenKey); |
||||
|
||||
const hashId = `${hashPrefix}-${hash(tokenKey)}`; |
||||
mergedDerivativeToken._hashId = hashId; // Not used |
||||
|
||||
return [mergedDerivativeToken, hashId]; |
||||
}, |
||||
cache => { |
||||
// Remove token will remove all related style |
||||
cleanTokenStyle(cache[0]._tokenKey); |
||||
}, |
||||
); |
||||
|
||||
return cachedToken; |
||||
} |
@ -0,0 +1,58 @@
|
||||
import { useStyleInject } from '../StyleContext'; |
||||
import type { KeyType } from '../Cache'; |
||||
import useHMR from './useHMR'; |
||||
import type { ShallowRef, Ref } from 'vue'; |
||||
import { onBeforeUnmount, watch, watchEffect, shallowRef } from 'vue'; |
||||
export default function useClientCache<CacheType>( |
||||
prefix: string, |
||||
keyPath: Ref<KeyType[]>, |
||||
cacheFn: () => CacheType, |
||||
onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void, |
||||
): ShallowRef<CacheType> { |
||||
const styleContext = useStyleInject(); |
||||
const fullPathStr = shallowRef(''); |
||||
const res = shallowRef<CacheType>(); |
||||
watchEffect(() => { |
||||
fullPathStr.value = [prefix, ...keyPath.value].join('%'); |
||||
}); |
||||
const HMRUpdate = useHMR(); |
||||
const clearCache = (pathStr: string) => { |
||||
styleContext.value.cache.update(pathStr, prevCache => { |
||||
const [times = 0, cache] = prevCache || []; |
||||
const nextCount = times - 1; |
||||
if (nextCount === 0) { |
||||
onCacheRemove?.(cache, false); |
||||
return null; |
||||
} |
||||
|
||||
return [times - 1, cache]; |
||||
}); |
||||
}; |
||||
|
||||
watch( |
||||
fullPathStr, |
||||
(newStr, oldStr) => { |
||||
if (oldStr) clearCache(oldStr); |
||||
// Create cache |
||||
styleContext.value.cache.update(newStr, prevCache => { |
||||
const [times = 0, cache] = prevCache || []; |
||||
|
||||
// HMR should always ignore cache since developer may change it |
||||
let tmpCache = cache; |
||||
if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) { |
||||
onCacheRemove?.(tmpCache, HMRUpdate); |
||||
tmpCache = null; |
||||
} |
||||
const mergedCache = tmpCache || cacheFn(); |
||||
|
||||
return [times + 1, mergedCache]; |
||||
}); |
||||
res.value = styleContext.value.cache.get(fullPathStr.value)![1]; |
||||
}, |
||||
{ immediate: true }, |
||||
); |
||||
onBeforeUnmount(() => { |
||||
clearCache(fullPathStr.value); |
||||
}); |
||||
return res; |
||||
} |
@ -0,0 +1,33 @@
|
||||
function useProdHMR() { |
||||
return false; |
||||
} |
||||
|
||||
let webpackHMR = false; |
||||
|
||||
function useDevHMR() { |
||||
return webpackHMR; |
||||
} |
||||
|
||||
export default process.env.NODE_ENV === 'production' ? useProdHMR : useDevHMR; |
||||
|
||||
// Webpack `module.hot.accept` do not support any deps update trigger
|
||||
// We have to hack handler to force mark as HRM
|
||||
if ( |
||||
process.env.NODE_ENV !== 'production' && |
||||
typeof module !== 'undefined' && |
||||
module && |
||||
(module as any).hot |
||||
) { |
||||
const win = window as any; |
||||
if (typeof win.webpackHotUpdate === 'function') { |
||||
const originWebpackHotUpdate = win.webpackHotUpdate; |
||||
|
||||
win.webpackHotUpdate = (...args: any[]) => { |
||||
webpackHMR = true; |
||||
setTimeout(() => { |
||||
webpackHMR = false; |
||||
}, 0); |
||||
return originWebpackHotUpdate(...args); |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,417 @@
|
||||
import hash from '@emotion/hash'; |
||||
import type * as CSS from 'csstype'; |
||||
// @ts-ignore |
||||
import unitless from '@emotion/unitless'; |
||||
import { compile, serialize, stringify } from 'stylis'; |
||||
import type { Theme, Transformer } from '..'; |
||||
import type Cache from '../Cache'; |
||||
import type Keyframes from '../Keyframes'; |
||||
import type { Linter } from '../linters'; |
||||
import { contentQuotesLinter, hashedAnimationLinter } from '../linters'; |
||||
import type { HashPriority } from '../StyleContext'; |
||||
import { |
||||
useStyleInject, |
||||
ATTR_DEV_CACHE_PATH, |
||||
ATTR_MARK, |
||||
ATTR_TOKEN, |
||||
CSS_IN_JS_INSTANCE, |
||||
CSS_IN_JS_INSTANCE_ID, |
||||
} from '../StyleContext'; |
||||
import { supportLayer } from '../util'; |
||||
import useGlobalCache from './useGlobalCache'; |
||||
import canUseDom from '../../canUseDom'; |
||||
import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS'; |
||||
import type { Ref } from 'vue'; |
||||
import { computed } from 'vue'; |
||||
import type { VueNode } from '../../type'; |
||||
|
||||
const isClientSide = canUseDom(); |
||||
|
||||
const SKIP_CHECK = '_skip_check_'; |
||||
|
||||
export type CSSProperties = Omit<CSS.PropertiesFallback<number | string>, 'animationName'> & { |
||||
animationName?: CSS.PropertiesFallback<number | string>['animationName'] | Keyframes; |
||||
}; |
||||
|
||||
export type CSSPropertiesWithMultiValues = { |
||||
[K in keyof CSSProperties]: |
||||
| CSSProperties[K] |
||||
| Extract<CSSProperties[K], string>[] |
||||
| { |
||||
[SKIP_CHECK]: boolean; |
||||
value: CSSProperties[K] | Extract<CSSProperties[K], string>[]; |
||||
}; |
||||
}; |
||||
|
||||
export type CSSPseudos = { [K in CSS.Pseudos]?: CSSObject }; |
||||
|
||||
type ArrayCSSInterpolation = CSSInterpolation[]; |
||||
|
||||
export type InterpolationPrimitive = null | undefined | boolean | number | string | CSSObject; |
||||
|
||||
export type CSSInterpolation = InterpolationPrimitive | ArrayCSSInterpolation | Keyframes; |
||||
|
||||
export type CSSOthersObject = Record<string, CSSInterpolation>; |
||||
|
||||
export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSSOthersObject {} |
||||
|
||||
// ============================================================================ |
||||
// == Parser == |
||||
// ============================================================================ |
||||
// Preprocessor style content to browser support one |
||||
export function normalizeStyle(styleStr: string) { |
||||
const serialized = serialize(compile(styleStr), stringify); |
||||
return serialized.replace(/\{%%%\:[^;];}/g, ';'); |
||||
} |
||||
|
||||
function isCompoundCSSProperty(value: CSSObject[string]) { |
||||
return typeof value === 'object' && value && SKIP_CHECK in value; |
||||
} |
||||
|
||||
// 注入 hash 值 |
||||
function injectSelectorHash(key: string, hashId: string, hashPriority?: HashPriority) { |
||||
if (!hashId) { |
||||
return key; |
||||
} |
||||
|
||||
const hashClassName = `.${hashId}`; |
||||
const hashSelector = hashPriority === 'low' ? `:where(${hashClassName})` : hashClassName; |
||||
|
||||
// 注入 hashId |
||||
const keys = key.split(',').map(k => { |
||||
const fullPath = k.trim().split(/\s+/); |
||||
|
||||
// 如果 Selector 第一个是 HTML Element,那我们就插到它的后面。反之,就插到最前面。 |
||||
let firstPath = fullPath[0] || ''; |
||||
const htmlElement = firstPath.match(/^\w+/)?.[0] || ''; |
||||
|
||||
firstPath = `${htmlElement}${hashSelector}${firstPath.slice(htmlElement.length)}`; |
||||
|
||||
return [firstPath, ...fullPath.slice(1)].join(' '); |
||||
}); |
||||
return keys.join(','); |
||||
} |
||||
|
||||
export interface ParseConfig { |
||||
hashId?: string; |
||||
hashPriority?: HashPriority; |
||||
layer?: string; |
||||
path?: string; |
||||
transformers?: Transformer[]; |
||||
linters?: Linter[]; |
||||
} |
||||
|
||||
export interface ParseInfo { |
||||
root?: boolean; |
||||
injectHash?: boolean; |
||||
parentSelectors: string[]; |
||||
} |
||||
|
||||
// Global effect style will mount once and not removed |
||||
// The effect will not save in SSR cache (e.g. keyframes) |
||||
const globalEffectStyleKeys = new Set(); |
||||
|
||||
/** |
||||
* @private Test only. Clear the global effect style keys. |
||||
*/ |
||||
export const _cf = |
||||
process.env.NODE_ENV !== 'production' ? () => globalEffectStyleKeys.clear() : undefined; |
||||
|
||||
// Parse CSSObject to style content |
||||
export const parseStyle = ( |
||||
interpolation: CSSInterpolation, |
||||
config: ParseConfig = {}, |
||||
{ root, injectHash, parentSelectors }: ParseInfo = { |
||||
root: true, |
||||
parentSelectors: [], |
||||
}, |
||||
): [ |
||||
parsedStr: string, |
||||
// Style content which should be unique on all of the style (e.g. Keyframes). |
||||
// Firefox will flick with same animation name when exist multiple same keyframes. |
||||
effectStyle: Record<string, string>, |
||||
] => { |
||||
const { hashId, layer, path, hashPriority, transformers = [], linters = [] } = config; |
||||
let styleStr = ''; |
||||
let effectStyle: Record<string, string> = {}; |
||||
|
||||
function parseKeyframes(keyframes: Keyframes) { |
||||
const animationName = keyframes.getName(hashId); |
||||
if (!effectStyle[animationName]) { |
||||
const [parsedStr] = parseStyle(keyframes.style, config, { |
||||
root: false, |
||||
parentSelectors, |
||||
}); |
||||
|
||||
effectStyle[animationName] = `@keyframes ${keyframes.getName(hashId)}${parsedStr}`; |
||||
} |
||||
} |
||||
|
||||
function flattenList(list: ArrayCSSInterpolation, fullList: CSSObject[] = []) { |
||||
list.forEach(item => { |
||||
if (Array.isArray(item)) { |
||||
flattenList(item, fullList); |
||||
} else if (item) { |
||||
fullList.push(item as CSSObject); |
||||
} |
||||
}); |
||||
|
||||
return fullList; |
||||
} |
||||
|
||||
const flattenStyleList = flattenList( |
||||
Array.isArray(interpolation) ? interpolation : [interpolation], |
||||
); |
||||
|
||||
flattenStyleList.forEach(originStyle => { |
||||
// Only root level can use raw string |
||||
const style: CSSObject = typeof originStyle === 'string' && !root ? {} : originStyle; |
||||
|
||||
if (typeof style === 'string') { |
||||
styleStr += `${style}\n`; |
||||
} else if ((style as any)._keyframe) { |
||||
// Keyframe |
||||
parseKeyframes(style as unknown as Keyframes); |
||||
} else { |
||||
const mergedStyle = transformers.reduce((prev, trans) => trans?.visit?.(prev) || prev, style); |
||||
|
||||
// Normal CSSObject |
||||
Object.keys(mergedStyle).forEach(key => { |
||||
const value = mergedStyle[key]; |
||||
|
||||
if ( |
||||
typeof value === 'object' && |
||||
value && |
||||
(key !== 'animationName' || !(value as Keyframes)._keyframe) && |
||||
!isCompoundCSSProperty(value) |
||||
) { |
||||
let subInjectHash = false; |
||||
|
||||
// 当成嵌套对象来处理 |
||||
let mergedKey = key.trim(); |
||||
// Whether treat child as root. In most case it is false. |
||||
let nextRoot = false; |
||||
|
||||
// 拆分多个选择器 |
||||
if ((root || injectHash) && hashId) { |
||||
if (mergedKey.startsWith('@')) { |
||||
// 略过媒体查询,交给子节点继续插入 hashId |
||||
subInjectHash = true; |
||||
} else { |
||||
// 注入 hashId |
||||
mergedKey = injectSelectorHash(key, hashId, hashPriority); |
||||
} |
||||
} else if (root && !hashId && (mergedKey === '&' || mergedKey === '')) { |
||||
// In case of `{ '&': { a: { color: 'red' } } }` or `{ '': { a: { color: 'red' } } }` without hashId, |
||||
// we will get `&{a:{color:red;}}` or `{a:{color:red;}}` string for stylis to compile. |
||||
// But it does not conform to stylis syntax, |
||||
// and finally we will get `{color:red;}` as css, which is wrong. |
||||
// So we need to remove key in root, and treat child `{ a: { color: 'red' } }` as root. |
||||
mergedKey = ''; |
||||
nextRoot = true; |
||||
} |
||||
|
||||
const [parsedStr, childEffectStyle] = parseStyle(value as any, config, { |
||||
root: nextRoot, |
||||
injectHash: subInjectHash, |
||||
parentSelectors: [...parentSelectors, mergedKey], |
||||
}); |
||||
|
||||
effectStyle = { |
||||
...effectStyle, |
||||
...childEffectStyle, |
||||
}; |
||||
|
||||
styleStr += `${mergedKey}${parsedStr}`; |
||||
} else { |
||||
const actualValue = (value as any)?.value ?? value; |
||||
if ( |
||||
process.env.NODE_ENV !== 'production' && |
||||
(typeof value !== 'object' || !(value as any)?.[SKIP_CHECK]) |
||||
) { |
||||
[contentQuotesLinter, hashedAnimationLinter, ...linters].forEach(linter => |
||||
linter(key, actualValue, { path, hashId, parentSelectors }), |
||||
); |
||||
} |
||||
|
||||
// 如果是样式则直接插入 |
||||
const styleName = key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); |
||||
|
||||
// Auto suffix with px |
||||
let formatValue = actualValue; |
||||
if (!unitless[key] && typeof formatValue === 'number' && formatValue !== 0) { |
||||
formatValue = `${formatValue}px`; |
||||
} |
||||
|
||||
// handle animationName & Keyframe value |
||||
if (key === 'animationName' && (value as Keyframes)?._keyframe) { |
||||
parseKeyframes(value as Keyframes); |
||||
formatValue = (value as Keyframes).getName(hashId); |
||||
} |
||||
|
||||
styleStr += `${styleName}:${formatValue};`; |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
if (!root) { |
||||
styleStr = `{${styleStr}}`; |
||||
} else if (layer && supportLayer()) { |
||||
const layerCells = layer.split(','); |
||||
const layerName = layerCells[layerCells.length - 1].trim(); |
||||
styleStr = `@layer ${layerName} {${styleStr}}`; |
||||
|
||||
// Order of layer if needed |
||||
if (layerCells.length > 1) { |
||||
// zombieJ: stylis do not support layer order, so we need to handle it manually. |
||||
styleStr = `@layer ${layer}{%%%:%}${styleStr}`; |
||||
} |
||||
} |
||||
|
||||
return [styleStr, effectStyle]; |
||||
}; |
||||
|
||||
// ============================================================================ |
||||
// == Register == |
||||
// ============================================================================ |
||||
function uniqueHash(path: (string | number)[], styleStr: string) { |
||||
return hash(`${path.join('%')}${styleStr}`); |
||||
} |
||||
|
||||
// function Empty() { |
||||
// return null; |
||||
// } |
||||
|
||||
/** |
||||
* Register a style to the global style sheet. |
||||
*/ |
||||
export default function useStyleRegister( |
||||
info: Ref<{ |
||||
theme: Theme<any, any>; |
||||
token: any; |
||||
path: string[]; |
||||
hashId?: string; |
||||
layer?: string; |
||||
}>, |
||||
styleFn: () => CSSInterpolation, |
||||
) { |
||||
const styleContext = useStyleInject(); |
||||
|
||||
const tokenKey = computed(() => info.value.token._tokenKey as string); |
||||
|
||||
const fullPath = computed(() => [tokenKey.value, ...info.value.path]); |
||||
|
||||
// Check if need insert style |
||||
let isMergedClientSide = isClientSide; |
||||
if (process.env.NODE_ENV !== 'production' && styleContext.value.mock !== undefined) { |
||||
isMergedClientSide = styleContext.value.mock === 'client'; |
||||
} |
||||
|
||||
// const [cacheStyle[0], cacheStyle[1], cacheStyle[2]] |
||||
useGlobalCache( |
||||
'style', |
||||
fullPath, |
||||
// Create cache if needed |
||||
() => { |
||||
const styleObj = styleFn(); |
||||
const { hashPriority, container, transformers, linters } = styleContext.value; |
||||
const { path, hashId, layer } = info.value; |
||||
const [parsedStyle, effectStyle] = parseStyle(styleObj, { |
||||
hashId, |
||||
hashPriority, |
||||
layer, |
||||
path: path.join('-'), |
||||
transformers, |
||||
linters, |
||||
}); |
||||
const styleStr = normalizeStyle(parsedStyle); |
||||
const styleId = uniqueHash(fullPath.value, styleStr); |
||||
|
||||
if (isMergedClientSide) { |
||||
const style = updateCSS(styleStr, styleId, { |
||||
mark: ATTR_MARK, |
||||
prepend: 'queue', |
||||
attachTo: container, |
||||
}); |
||||
|
||||
(style as any)[CSS_IN_JS_INSTANCE] = CSS_IN_JS_INSTANCE_ID; |
||||
|
||||
// Used for `useCacheToken` to remove on batch when token removed |
||||
style.setAttribute(ATTR_TOKEN, tokenKey.value); |
||||
|
||||
// Dev usage to find which cache path made this easily |
||||
if (process.env.NODE_ENV !== 'production') { |
||||
style.setAttribute(ATTR_DEV_CACHE_PATH, fullPath.value.join('|')); |
||||
} |
||||
|
||||
// Inject client side effect style |
||||
Object.keys(effectStyle).forEach(effectKey => { |
||||
if (!globalEffectStyleKeys.has(effectKey)) { |
||||
globalEffectStyleKeys.add(effectKey); |
||||
|
||||
// Inject |
||||
updateCSS(normalizeStyle(effectStyle[effectKey]), `_effect-${effectKey}`, { |
||||
mark: ATTR_MARK, |
||||
prepend: 'queue', |
||||
attachTo: container, |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return [styleStr, tokenKey.value, styleId]; |
||||
}, |
||||
// Remove cache if no need |
||||
([, , styleId], fromHMR) => { |
||||
if ((fromHMR || styleContext.value.autoClear) && isClientSide) { |
||||
removeCSS(styleId, { mark: ATTR_MARK }); |
||||
} |
||||
}, |
||||
); |
||||
|
||||
return (node: VueNode) => { |
||||
return node; |
||||
// let styleNode: VueNode; |
||||
// if (!styleContext.ssrInline || isMergedClientSide || !styleContext.defaultCache) { |
||||
// styleNode = <Empty />; |
||||
// } else { |
||||
// styleNode = ( |
||||
// <style |
||||
// {...{ |
||||
// [ATTR_TOKEN]: cacheStyle.value[1], |
||||
// [ATTR_MARK]: cacheStyle.value[2], |
||||
// }} |
||||
// innerHTML={cacheStyle.value[0]} |
||||
// /> |
||||
// ); |
||||
// } |
||||
|
||||
// return ( |
||||
// <> |
||||
// {styleNode} |
||||
// {node} |
||||
// </> |
||||
// ); |
||||
}; |
||||
} |
||||
|
||||
// ============================================================================ |
||||
// == SSR == |
||||
// ============================================================================ |
||||
export function extractStyle(cache: Cache) { |
||||
// prefix with `style` is used for `useStyleRegister` to cache style context |
||||
const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith('style%')); |
||||
|
||||
// const tokenStyles: Record<string, string[]> = {}; |
||||
|
||||
let styleText = ''; |
||||
|
||||
styleKeys.forEach(key => { |
||||
const [styleStr, tokenKey, styleId]: [string, string, string] = cache.cache.get(key)![1]; |
||||
|
||||
styleText += `<style ${ATTR_TOKEN}="${tokenKey}" ${ATTR_MARK}="${styleId}">${styleStr}</style>`; |
||||
}); |
||||
|
||||
return styleText; |
||||
} |
@ -0,0 +1,67 @@
|
||||
import useCacheToken from './hooks/useCacheToken'; |
||||
import type { CSSInterpolation, CSSObject } from './hooks/useStyleRegister'; |
||||
import useStyleRegister, { extractStyle } from './hooks/useStyleRegister'; |
||||
import Keyframes from './Keyframes'; |
||||
import type { Linter } from './linters'; |
||||
import { legacyNotSelectorLinter, logicalPropertiesLinter } from './linters'; |
||||
import type { StyleContextProps, StyleProviderProps } from './StyleContext'; |
||||
import { createCache, useStyleInject, useStyleProvider, StyleProvider } from './StyleContext'; |
||||
import type { DerivativeFunc, TokenType } from './theme'; |
||||
import { createTheme, Theme } from './theme'; |
||||
import type { Transformer } from './transformers/interface'; |
||||
import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; |
||||
|
||||
const cssinjs = { |
||||
Theme, |
||||
createTheme, |
||||
useStyleRegister, |
||||
useCacheToken, |
||||
createCache, |
||||
useStyleInject, |
||||
useStyleProvider, |
||||
Keyframes, |
||||
extractStyle, |
||||
|
||||
// Transformer
|
||||
legacyLogicalPropertiesTransformer, |
||||
|
||||
// Linters
|
||||
logicalPropertiesLinter, |
||||
legacyNotSelectorLinter, |
||||
|
||||
// cssinjs
|
||||
StyleProvider, |
||||
}; |
||||
export { |
||||
Theme, |
||||
createTheme, |
||||
useStyleRegister, |
||||
useCacheToken, |
||||
createCache, |
||||
useStyleInject, |
||||
useStyleProvider, |
||||
Keyframes, |
||||
extractStyle, |
||||
|
||||
// Transformer
|
||||
legacyLogicalPropertiesTransformer, |
||||
|
||||
// Linters
|
||||
logicalPropertiesLinter, |
||||
legacyNotSelectorLinter, |
||||
|
||||
// cssinjs
|
||||
StyleProvider, |
||||
}; |
||||
export type { |
||||
TokenType, |
||||
CSSObject, |
||||
CSSInterpolation, |
||||
DerivativeFunc, |
||||
Transformer, |
||||
Linter, |
||||
StyleContextProps, |
||||
StyleProviderProps, |
||||
}; |
||||
|
||||
export default cssinjs; |
@ -0,0 +1,25 @@
|
||||
import type { Linter } from './interface'; |
||||
import { lintWarning } from './utils'; |
||||
|
||||
const linter: Linter = (key, value, info) => { |
||||
if (key === 'content') { |
||||
// From emotion: https://github.com/emotion-js/emotion/blob/main/packages/serialize/src/index.js#L63
|
||||
const contentValuePattern = |
||||
/(attr|counters?|url|(((repeating-)?(linear|radial))|conic)-gradient)\(|(no-)?(open|close)-quote/; |
||||
const contentValues = ['normal', 'none', 'initial', 'inherit', 'unset']; |
||||
if ( |
||||
typeof value !== 'string' || |
||||
(contentValues.indexOf(value) === -1 && |
||||
!contentValuePattern.test(value) && |
||||
(value.charAt(0) !== value.charAt(value.length - 1) || |
||||
(value.charAt(0) !== '"' && value.charAt(0) !== "'"))) |
||||
) { |
||||
lintWarning( |
||||
`You seem to be using a value for 'content' without quotes, try replacing it with \`content: '"${value}"'\`.`, |
||||
info, |
||||
); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export default linter; |
@ -0,0 +1,15 @@
|
||||
import type { Linter } from './interface'; |
||||
import { lintWarning } from './utils'; |
||||
|
||||
const linter: Linter = (key, value, info) => { |
||||
if (key === 'animation') { |
||||
if (info.hashId && value !== 'none') { |
||||
lintWarning( |
||||
`You seem to be using hashed animation '${value}', in which case 'animationName' with Keyframe as value is recommended.`, |
||||
info, |
||||
); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
export default linter; |
@ -0,0 +1,5 @@
|
||||
export { default as contentQuotesLinter } from './contentQuotesLinter'; |
||||
export { default as hashedAnimationLinter } from './hashedAnimationLinter'; |
||||
export type { Linter } from './interface'; |
||||
export { default as legacyNotSelectorLinter } from './legacyNotSelectorLinter'; |
||||
export { default as logicalPropertiesLinter } from './logicalPropertiesLinter'; |
@ -0,0 +1,9 @@
|
||||
export interface LinterInfo { |
||||
path?: string; |
||||
hashId?: string; |
||||
parentSelectors: string[]; |
||||
} |
||||
|
||||
export interface Linter { |
||||
(key: string, value: string | number, info: LinterInfo): void; |
||||
} |
@ -0,0 +1,33 @@
|
||||
import type { Linter, LinterInfo } from './interface'; |
||||
import { lintWarning } from './utils'; |
||||
|
||||
function isConcatSelector(selector: string) { |
||||
const notContent = selector.match(/:not\(([^)]*)\)/)?.[1] || ''; |
||||
|
||||
// split selector. e.g.
|
||||
// `h1#a.b` => ['h1', #a', '.b']
|
||||
const splitCells = notContent.split(/(\[[^[]*])|(?=[.#])/).filter(str => str); |
||||
|
||||
return splitCells.length > 1; |
||||
} |
||||
|
||||
function parsePath(info: LinterInfo) { |
||||
return info.parentSelectors.reduce((prev, cur) => { |
||||
if (!prev) { |
||||
return cur; |
||||
} |
||||
|
||||
return cur.includes('&') ? cur.replace(/&/g, prev) : `${prev} ${cur}`; |
||||
}, ''); |
||||
} |
||||
|
||||
const linter: Linter = (_key, _value, info) => { |
||||
const parentSelectorPath = parsePath(info); |
||||
const notList = parentSelectorPath.match(/:not\([^)]*\)/g) || []; |
||||
|
||||
if (notList.length > 0 && notList.some(isConcatSelector)) { |
||||
lintWarning(`Concat ':not' selector not support in legacy browsers.`, info); |
||||
} |
||||
}; |
||||
|
||||
export default linter; |
@ -0,0 +1,88 @@
|
||||
import type { Linter } from './interface'; |
||||
import { lintWarning } from './utils'; |
||||
|
||||
const linter: Linter = (key, value, info) => { |
||||
switch (key) { |
||||
case 'marginLeft': |
||||
case 'marginRight': |
||||
case 'paddingLeft': |
||||
case 'paddingRight': |
||||
case 'left': |
||||
case 'right': |
||||
case 'borderLeft': |
||||
case 'borderLeftWidth': |
||||
case 'borderLeftStyle': |
||||
case 'borderLeftColor': |
||||
case 'borderRight': |
||||
case 'borderRightWidth': |
||||
case 'borderRightStyle': |
||||
case 'borderRightColor': |
||||
case 'borderTopLeftRadius': |
||||
case 'borderTopRightRadius': |
||||
case 'borderBottomLeftRadius': |
||||
case 'borderBottomRightRadius': |
||||
lintWarning( |
||||
`You seem to be using non-logical property '${key}' which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, |
||||
info, |
||||
); |
||||
return; |
||||
case 'margin': |
||||
case 'padding': |
||||
case 'borderWidth': |
||||
case 'borderStyle': |
||||
// case 'borderColor':
|
||||
if (typeof value === 'string') { |
||||
const valueArr = value.split(' ').map(item => item.trim()); |
||||
if (valueArr.length === 4 && valueArr[1] !== valueArr[3]) { |
||||
lintWarning( |
||||
`You seem to be using '${key}' property with different left ${key} and right ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, |
||||
info, |
||||
); |
||||
} |
||||
} |
||||
return; |
||||
case 'clear': |
||||
case 'textAlign': |
||||
if (value === 'left' || value === 'right') { |
||||
lintWarning( |
||||
`You seem to be using non-logical value '${value}' of ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, |
||||
info, |
||||
); |
||||
} |
||||
return; |
||||
case 'borderRadius': |
||||
if (typeof value === 'string') { |
||||
const radiusGroups = value.split('/').map(item => item.trim()); |
||||
const invalid = radiusGroups.reduce((result, group) => { |
||||
if (result) { |
||||
return result; |
||||
} |
||||
const radiusArr = group.split(' ').map(item => item.trim()); |
||||
// borderRadius: '2px 4px'
|
||||
if (radiusArr.length >= 2 && radiusArr[0] !== radiusArr[1]) { |
||||
return true; |
||||
} |
||||
// borderRadius: '4px 4px 2px'
|
||||
if (radiusArr.length === 3 && radiusArr[1] !== radiusArr[2]) { |
||||
return true; |
||||
} |
||||
// borderRadius: '4px 4px 2px 4px'
|
||||
if (radiusArr.length === 4 && radiusArr[2] !== radiusArr[3]) { |
||||
return true; |
||||
} |
||||
return result; |
||||
}, false); |
||||
|
||||
if (invalid) { |
||||
lintWarning( |
||||
`You seem to be using non-logical value '${value}' of ${key}, which is not compatible with RTL mode. Please use logical properties and values instead. For more information: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties.`, |
||||
info, |
||||
); |
||||
} |
||||
} |
||||
return; |
||||
default: |
||||
} |
||||
}; |
||||
|
||||
export default linter; |
@ -0,0 +1,13 @@
|
||||
import devWarning from '../../../vc-util/warning'; |
||||
import type { LinterInfo } from './interface'; |
||||
|
||||
export function lintWarning(message: string, info: LinterInfo) { |
||||
const { path, parentSelectors } = info; |
||||
|
||||
devWarning( |
||||
false, |
||||
`[Ant Design Vue CSS-in-JS] ${path ? `Error in '${path}': ` : ''}${message}${ |
||||
parentSelectors.length ? ` Selector info: ${parentSelectors.join(' -> ')}` : '' |
||||
}`,
|
||||
); |
||||
} |
@ -0,0 +1,38 @@
|
||||
import warning from '../../warning'; |
||||
import type { DerivativeFunc, TokenType } from './interface'; |
||||
|
||||
let uuid = 0; |
||||
|
||||
/** |
||||
* Theme with algorithms to derive tokens from design tokens. |
||||
* Use `createTheme` first which will help to manage the theme instance cache. |
||||
*/ |
||||
export default class Theme<DesignToken extends TokenType, DerivativeToken extends TokenType> { |
||||
private derivatives: DerivativeFunc<DesignToken, DerivativeToken>[]; |
||||
public readonly id: number; |
||||
|
||||
constructor( |
||||
derivatives: |
||||
| DerivativeFunc<DesignToken, DerivativeToken> |
||||
| DerivativeFunc<DesignToken, DerivativeToken>[], |
||||
) { |
||||
this.derivatives = Array.isArray(derivatives) ? derivatives : [derivatives]; |
||||
this.id = uuid; |
||||
|
||||
if (derivatives.length === 0) { |
||||
warning( |
||||
derivatives.length > 0, |
||||
'[Ant Design Vue CSS-in-JS] Theme should have at least one derivative function.', |
||||
); |
||||
} |
||||
|
||||
uuid += 1; |
||||
} |
||||
|
||||
getDerivativeToken(token: DesignToken): DerivativeToken { |
||||
return this.derivatives.reduce<DerivativeToken>( |
||||
(result, derivative) => derivative(token, result), |
||||
undefined as any, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,135 @@
|
||||
import type Theme from './Theme'; |
||||
import type { DerivativeFunc } from './interface'; |
||||
|
||||
// ================================== Cache ==================================
|
||||
type ThemeCacheMap = Map< |
||||
DerivativeFunc<any, any>, |
||||
{ |
||||
map?: ThemeCacheMap; |
||||
value?: [Theme<any, any>, number]; |
||||
} |
||||
>; |
||||
|
||||
type DerivativeOptions = DerivativeFunc<any, any>[]; |
||||
|
||||
export function sameDerivativeOption(left: DerivativeOptions, right: DerivativeOptions) { |
||||
if (left.length !== right.length) { |
||||
return false; |
||||
} |
||||
for (let i = 0; i < left.length; i++) { |
||||
if (left[i] !== right[i]) { |
||||
return false; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
export default class ThemeCache { |
||||
public static MAX_CACHE_SIZE = 20; |
||||
public static MAX_CACHE_OFFSET = 5; |
||||
|
||||
private readonly cache: ThemeCacheMap; |
||||
private keys: DerivativeOptions[]; |
||||
private cacheCallTimes: number; |
||||
|
||||
constructor() { |
||||
this.cache = new Map(); |
||||
this.keys = []; |
||||
this.cacheCallTimes = 0; |
||||
} |
||||
|
||||
public size(): number { |
||||
return this.keys.length; |
||||
} |
||||
|
||||
private internalGet( |
||||
derivativeOption: DerivativeOptions, |
||||
updateCallTimes = false, |
||||
): [Theme<any, any>, number] | undefined { |
||||
let cache: ReturnType<ThemeCacheMap['get']> = { map: this.cache }; |
||||
derivativeOption.forEach(derivative => { |
||||
if (!cache) { |
||||
cache = undefined; |
||||
} else { |
||||
cache = cache?.map?.get(derivative); |
||||
} |
||||
}); |
||||
if (cache?.value && updateCallTimes) { |
||||
cache.value[1] = this.cacheCallTimes++; |
||||
} |
||||
return cache?.value; |
||||
} |
||||
|
||||
public get(derivativeOption: DerivativeOptions): Theme<any, any> | undefined { |
||||
return this.internalGet(derivativeOption, true)?.[0]; |
||||
} |
||||
|
||||
public has(derivativeOption: DerivativeOptions): boolean { |
||||
return !!this.internalGet(derivativeOption); |
||||
} |
||||
|
||||
public set(derivativeOption: DerivativeOptions, value: Theme<any, any>): void { |
||||
// New cache
|
||||
if (!this.has(derivativeOption)) { |
||||
if (this.size() + 1 > ThemeCache.MAX_CACHE_SIZE + ThemeCache.MAX_CACHE_OFFSET) { |
||||
const [targetKey] = this.keys.reduce<[DerivativeOptions, number]>( |
||||
(result, key) => { |
||||
const [, callTimes] = result; |
||||
if (this.internalGet(key)![1] < callTimes) { |
||||
return [key, this.internalGet(key)![1]]; |
||||
} |
||||
return result; |
||||
}, |
||||
[this.keys[0], this.cacheCallTimes], |
||||
); |
||||
this.delete(targetKey); |
||||
} |
||||
|
||||
this.keys.push(derivativeOption); |
||||
} |
||||
|
||||
let cache = this.cache; |
||||
derivativeOption.forEach((derivative, index) => { |
||||
if (index === derivativeOption.length - 1) { |
||||
cache.set(derivative, { value: [value, this.cacheCallTimes++] }); |
||||
} else { |
||||
const cacheValue = cache.get(derivative); |
||||
if (!cacheValue) { |
||||
cache.set(derivative, { map: new Map() }); |
||||
} else if (!cacheValue.map) { |
||||
cacheValue.map = new Map(); |
||||
} |
||||
cache = cache.get(derivative)!.map!; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private deleteByPath( |
||||
currentCache: ThemeCacheMap, |
||||
derivatives: DerivativeFunc<any, any>[], |
||||
): Theme<any, any> | undefined { |
||||
const cache = currentCache.get(derivatives[0])!; |
||||
if (derivatives.length === 1) { |
||||
if (!cache.map) { |
||||
currentCache.delete(derivatives[0]); |
||||
} else { |
||||
currentCache.set(derivatives[0], { map: cache.map }); |
||||
} |
||||
return cache.value?.[0]; |
||||
} |
||||
const result = this.deleteByPath(cache.map!, derivatives.slice(1)); |
||||
if ((!cache.map || cache.map.size === 0) && !cache.value) { |
||||
currentCache.delete(derivatives[0]); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
public delete(derivativeOption: DerivativeOptions): Theme<any, any> | undefined { |
||||
// If cache exists
|
||||
if (this.has(derivativeOption)) { |
||||
this.keys = this.keys.filter(item => !sameDerivativeOption(item, derivativeOption)); |
||||
return this.deleteByPath(this.cache, derivativeOption); |
||||
} |
||||
return undefined; |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
import ThemeCache from './ThemeCache'; |
||||
import Theme from './Theme'; |
||||
import type { DerivativeFunc, TokenType } from './interface'; |
||||
|
||||
const cacheThemes = new ThemeCache(); |
||||
|
||||
/** |
||||
* Same as new Theme, but will always return same one if `derivative` not changed. |
||||
*/ |
||||
export default function createTheme< |
||||
DesignToken extends TokenType, |
||||
DerivativeToken extends TokenType, |
||||
>( |
||||
derivatives: |
||||
| DerivativeFunc<DesignToken, DerivativeToken>[] |
||||
| DerivativeFunc<DesignToken, DerivativeToken>, |
||||
) { |
||||
const derivativeArr = Array.isArray(derivatives) ? derivatives : [derivatives]; |
||||
// Create new theme if not exist
|
||||
if (!cacheThemes.has(derivativeArr)) { |
||||
cacheThemes.set(derivativeArr, new Theme(derivativeArr)); |
||||
} |
||||
|
||||
// Get theme from cache and return
|
||||
return cacheThemes.get(derivativeArr)!; |
||||
} |
@ -0,0 +1,4 @@
|
||||
export { default as createTheme } from './createTheme'; |
||||
export { default as Theme } from './Theme'; |
||||
export { default as ThemeCache } from './ThemeCache'; |
||||
export type { TokenType, DerivativeFunc } from './interface'; |
@ -0,0 +1,5 @@
|
||||
export type TokenType = object; |
||||
export type DerivativeFunc<DesignToken extends TokenType, DerivativeToken extends TokenType> = ( |
||||
designToken: DesignToken, |
||||
derivativeToken?: DerivativeToken, |
||||
) => DerivativeToken; |
@ -0,0 +1,5 @@
|
||||
import type { CSSObject } from '..'; |
||||
|
||||
export interface Transformer { |
||||
visit?: (cssObj: CSSObject) => CSSObject; |
||||
} |
@ -0,0 +1,162 @@
|
||||
import type { CSSObject } from '..'; |
||||
import type { Transformer } from './interface'; |
||||
|
||||
function splitValues(value: string | number) { |
||||
if (typeof value === 'number') { |
||||
return [value]; |
||||
} |
||||
|
||||
const splitStyle = String(value).split(/\s+/); |
||||
|
||||
// Combine styles split in brackets, like `calc(1px + 2px)`
|
||||
let temp = ''; |
||||
let brackets = 0; |
||||
return splitStyle.reduce<string[]>((list, item) => { |
||||
if (item.includes('(')) { |
||||
temp += item; |
||||
brackets += item.split('(').length - 1; |
||||
} else if (item.includes(')')) { |
||||
temp += ` ${item}`; |
||||
brackets -= item.split(')').length - 1; |
||||
if (brackets === 0) { |
||||
list.push(temp); |
||||
temp = ''; |
||||
} |
||||
} else if (brackets > 0) { |
||||
temp += ` ${item}`; |
||||
} else { |
||||
list.push(item); |
||||
} |
||||
return list; |
||||
}, []); |
||||
} |
||||
|
||||
type MatchValue = string[] & { |
||||
notSplit?: boolean; |
||||
}; |
||||
|
||||
function noSplit(list: MatchValue): MatchValue { |
||||
list.notSplit = true; |
||||
return list; |
||||
} |
||||
|
||||
const keyMap: Record<string, MatchValue> = { |
||||
// Inset
|
||||
inset: ['top', 'right', 'bottom', 'left'], |
||||
insetBlock: ['top', 'bottom'], |
||||
insetBlockStart: ['top'], |
||||
insetBlockEnd: ['bottom'], |
||||
insetInline: ['left', 'right'], |
||||
insetInlineStart: ['left'], |
||||
insetInlineEnd: ['right'], |
||||
|
||||
// Margin
|
||||
marginBlock: ['marginTop', 'marginBottom'], |
||||
marginBlockStart: ['marginTop'], |
||||
marginBlockEnd: ['marginBottom'], |
||||
marginInline: ['marginLeft', 'marginRight'], |
||||
marginInlineStart: ['marginLeft'], |
||||
marginInlineEnd: ['marginRight'], |
||||
|
||||
// Padding
|
||||
paddingBlock: ['paddingTop', 'paddingBottom'], |
||||
paddingBlockStart: ['paddingTop'], |
||||
paddingBlockEnd: ['paddingBottom'], |
||||
paddingInline: ['paddingLeft', 'paddingRight'], |
||||
paddingInlineStart: ['paddingLeft'], |
||||
paddingInlineEnd: ['paddingRight'], |
||||
|
||||
// Border
|
||||
borderBlock: noSplit(['borderTop', 'borderBottom']), |
||||
borderBlockStart: noSplit(['borderTop']), |
||||
borderBlockEnd: noSplit(['borderBottom']), |
||||
borderInline: noSplit(['borderLeft', 'borderRight']), |
||||
borderInlineStart: noSplit(['borderLeft']), |
||||
borderInlineEnd: noSplit(['borderRight']), |
||||
|
||||
// Border width
|
||||
borderBlockWidth: ['borderTopWidth', 'borderBottomWidth'], |
||||
borderBlockStartWidth: ['borderTopWidth'], |
||||
borderBlockEndWidth: ['borderBottomWidth'], |
||||
borderInlineWidth: ['borderLeftWidth', 'borderRightWidth'], |
||||
borderInlineStartWidth: ['borderLeftWidth'], |
||||
borderInlineEndWidth: ['borderRightWidth'], |
||||
|
||||
// Border style
|
||||
borderBlockStyle: ['borderTopStyle', 'borderBottomStyle'], |
||||
borderBlockStartStyle: ['borderTopStyle'], |
||||
borderBlockEndStyle: ['borderBottomStyle'], |
||||
borderInlineStyle: ['borderLeftStyle', 'borderRightStyle'], |
||||
borderInlineStartStyle: ['borderLeftStyle'], |
||||
borderInlineEndStyle: ['borderRightStyle'], |
||||
|
||||
// Border color
|
||||
borderBlockColor: ['borderTopColor', 'borderBottomColor'], |
||||
borderBlockStartColor: ['borderTopColor'], |
||||
borderBlockEndColor: ['borderBottomColor'], |
||||
borderInlineColor: ['borderLeftColor', 'borderRightColor'], |
||||
borderInlineStartColor: ['borderLeftColor'], |
||||
borderInlineEndColor: ['borderRightColor'], |
||||
|
||||
// Border radius
|
||||
borderStartStartRadius: ['borderTopLeftRadius'], |
||||
borderStartEndRadius: ['borderTopRightRadius'], |
||||
borderEndStartRadius: ['borderBottomLeftRadius'], |
||||
borderEndEndRadius: ['borderBottomRightRadius'], |
||||
}; |
||||
|
||||
function skipCheck(value: string | number) { |
||||
return { _skip_check_: true, value }; |
||||
} |
||||
|
||||
/** |
||||
* Convert css logical properties to legacy properties. |
||||
* Such as: `margin-block-start` to `margin-top`. |
||||
* Transform list: |
||||
* - inset |
||||
* - margin |
||||
* - padding |
||||
* - border |
||||
*/ |
||||
const transform: Transformer = { |
||||
visit: cssObj => { |
||||
const clone: CSSObject = {}; |
||||
|
||||
Object.keys(cssObj).forEach(key => { |
||||
const value = cssObj[key]; |
||||
const matchValue = keyMap[key]; |
||||
|
||||
if (matchValue && (typeof value === 'number' || typeof value === 'string')) { |
||||
const values = splitValues(value); |
||||
|
||||
if (matchValue.length && matchValue.notSplit) { |
||||
// not split means always give same value like border
|
||||
matchValue.forEach(matchKey => { |
||||
clone[matchKey] = skipCheck(value); |
||||
}); |
||||
} else if (matchValue.length === 1) { |
||||
// Handle like `marginBlockStart` => `marginTop`
|
||||
clone[matchValue[0]] = skipCheck(value); |
||||
} else if (matchValue.length === 2) { |
||||
// Handle like `marginBlock` => `marginTop` & `marginBottom`
|
||||
matchValue.forEach((matchKey, index) => { |
||||
clone[matchKey] = skipCheck(values[index] ?? values[0]); |
||||
}); |
||||
} else if (matchValue.length === 4) { |
||||
// Handle like `inset` => `top` & `right` & `bottom` & `left`
|
||||
matchValue.forEach((matchKey, index) => { |
||||
clone[matchKey] = skipCheck(values[index] ?? values[index - 2] ?? values[0]); |
||||
}); |
||||
} else { |
||||
clone[key] = value; |
||||
} |
||||
} else { |
||||
clone[key] = value; |
||||
} |
||||
}); |
||||
|
||||
return clone; |
||||
}, |
||||
}; |
||||
|
||||
export default transform; |
@ -0,0 +1,68 @@
|
||||
import hash from '@emotion/hash'; |
||||
import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; |
||||
import canUseDom from '../canUseDom'; |
||||
|
||||
export function flattenToken(token: any) { |
||||
let str = ''; |
||||
Object.keys(token).forEach(key => { |
||||
const value = token[key]; |
||||
str += key; |
||||
if (value && typeof value === 'object') { |
||||
str += flattenToken(value); |
||||
} else { |
||||
str += value; |
||||
} |
||||
}); |
||||
return str; |
||||
} |
||||
|
||||
/** |
||||
* Convert derivative token to key string |
||||
*/ |
||||
export function token2key(token: any, salt: string): string { |
||||
return hash(`${salt}_${flattenToken(token)}`); |
||||
} |
||||
|
||||
const layerKey = `layer-${Date.now()}-${Math.random()}`.replace(/\./g, ''); |
||||
const layerWidth = '903px'; |
||||
|
||||
function supportSelector(styleStr: string, handleElement?: (ele: HTMLElement) => void): boolean { |
||||
if (canUseDom()) { |
||||
updateCSS(styleStr, layerKey); |
||||
|
||||
const ele = document.createElement('div'); |
||||
ele.style.position = 'fixed'; |
||||
ele.style.left = '0'; |
||||
ele.style.top = '0'; |
||||
handleElement?.(ele); |
||||
document.body.appendChild(ele); |
||||
|
||||
if (process.env.NODE_ENV !== 'production') { |
||||
ele.innerHTML = 'Test'; |
||||
ele.style.zIndex = '9999999'; |
||||
} |
||||
|
||||
const support = getComputedStyle(ele).width === layerWidth; |
||||
|
||||
ele.parentNode?.removeChild(ele); |
||||
removeCSS(layerKey); |
||||
|
||||
return support; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
let canLayer: boolean | undefined = undefined; |
||||
export function supportLayer(): boolean { |
||||
if (canLayer === undefined) { |
||||
canLayer = supportSelector( |
||||
`@layer ${layerKey} { .${layerKey} { width: ${layerWidth}!important; } }`, |
||||
ele => { |
||||
ele.className = layerKey; |
||||
}, |
||||
); |
||||
} |
||||
|
||||
return canLayer!; |
||||
} |
@ -0,0 +1,21 @@
|
||||
type RecordType = Record<string, any>; |
||||
|
||||
function extendsObject<T extends RecordType>(...list: T[]) { |
||||
const result: RecordType = { ...list[0] }; |
||||
|
||||
for (let i = 1; i < list.length; i++) { |
||||
const obj = list[i]; |
||||
if (obj) { |
||||
Object.keys(obj).forEach(key => { |
||||
const val = obj[key]; |
||||
if (val !== undefined) { |
||||
result[key] = val; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
export default extendsObject; |
@ -1,30 +0,0 @@
|
||||
export function getComponentLocale(props, context, componentName, getDefaultLocale) { |
||||
let locale = {}; |
||||
if (context && context.antLocale && context.antLocale[componentName]) { |
||||
locale = context.antLocale[componentName]; |
||||
} else { |
||||
const defaultLocale = getDefaultLocale(); |
||||
// TODO: make default lang of antd be English
|
||||
// https://github.com/ant-design/ant-design/issues/6334
|
||||
locale = defaultLocale.default || defaultLocale; |
||||
} |
||||
|
||||
const result = { |
||||
...locale, |
||||
...props.locale, |
||||
}; |
||||
result.lang = { |
||||
...locale.lang, |
||||
...props.locale.lang, |
||||
}; |
||||
return result; |
||||
} |
||||
|
||||
export function getLocaleCode(context) { |
||||
const localeCode = context.antLocale && context.antLocale.locale; |
||||
// Had use LocaleProvide but didn't set locale
|
||||
if (context.antLocale && context.antLocale.exist && !localeCode) { |
||||
return 'zh-cn'; |
||||
} |
||||
return localeCode; |
||||
} |
@ -0,0 +1,62 @@
|
||||
import { tryOnScopeDispose } from './tryOnScopeDispose'; |
||||
import { watch } from 'vue'; |
||||
import type { MaybeElementRef } from './unrefElement'; |
||||
import { unrefElement } from './unrefElement'; |
||||
import { useSupported } from './useSupported'; |
||||
import type { ConfigurableWindow } from './_configurable'; |
||||
import { defaultWindow } from './_configurable'; |
||||
|
||||
export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {} |
||||
|
||||
/** |
||||
* Watch for changes being made to the DOM tree. |
||||
* |
||||
* @see https://vueuse.org/useMutationObserver
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN
|
||||
* @param target |
||||
* @param callback |
||||
* @param options |
||||
*/ |
||||
export function useMutationObserver( |
||||
target: MaybeElementRef, |
||||
callback: MutationCallback, |
||||
options: UseMutationObserverOptions = {}, |
||||
) { |
||||
const { window = defaultWindow, ...mutationOptions } = options; |
||||
let observer: MutationObserver | undefined; |
||||
const isSupported = useSupported(() => window && 'MutationObserver' in window); |
||||
|
||||
const cleanup = () => { |
||||
if (observer) { |
||||
observer.disconnect(); |
||||
observer = undefined; |
||||
} |
||||
}; |
||||
|
||||
const stopWatch = watch( |
||||
() => unrefElement(target), |
||||
el => { |
||||
cleanup(); |
||||
|
||||
if (isSupported.value && window && el) { |
||||
observer = new MutationObserver(callback); |
||||
observer!.observe(el, mutationOptions); |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
); |
||||
|
||||
const stop = () => { |
||||
cleanup(); |
||||
stopWatch(); |
||||
}; |
||||
|
||||
tryOnScopeDispose(stop); |
||||
|
||||
return { |
||||
isSupported, |
||||
stop, |
||||
}; |
||||
} |
||||
|
||||
export type UseMutationObserverReturn = ReturnType<typeof useMutationObserver>; |
@ -1,84 +0,0 @@
|
||||
import type { RequiredMark } from '../../form/Form'; |
||||
import type { ComputedRef, UnwrapRef } from 'vue'; |
||||
import { computed, inject } from 'vue'; |
||||
import type { ConfigProviderProps, CSPConfig, Direction, SizeType } from '../../config-provider'; |
||||
import { defaultConfigProvider } from '../../config-provider'; |
||||
import type { VueNode } from '../type'; |
||||
import type { ValidateMessages } from '../../form/interface'; |
||||
|
||||
export default ( |
||||
name: string, |
||||
props: Record<any, any>, |
||||
): { |
||||
configProvider: UnwrapRef<ConfigProviderProps>; |
||||
prefixCls: ComputedRef<string>; |
||||
rootPrefixCls: ComputedRef<string>; |
||||
direction: ComputedRef<Direction>; |
||||
size: ComputedRef<SizeType>; |
||||
getTargetContainer: ComputedRef<() => HTMLElement>; |
||||
space: ComputedRef<{ size: SizeType | number }>; |
||||
pageHeader: ComputedRef<{ ghost: boolean }>; |
||||
form?: ComputedRef<{ |
||||
requiredMark?: RequiredMark; |
||||
colon?: boolean; |
||||
validateMessages?: ValidateMessages; |
||||
}>; |
||||
autoInsertSpaceInButton: ComputedRef<boolean>; |
||||
renderEmpty?: ComputedRef<(componentName?: string) => VueNode>; |
||||
virtual: ComputedRef<boolean>; |
||||
dropdownMatchSelectWidth: ComputedRef<boolean | number>; |
||||
getPopupContainer: ComputedRef<ConfigProviderProps['getPopupContainer']>; |
||||
getPrefixCls: ConfigProviderProps['getPrefixCls']; |
||||
autocomplete: ComputedRef<string>; |
||||
csp: ComputedRef<CSPConfig>; |
||||
} => { |
||||
const configProvider = inject<UnwrapRef<ConfigProviderProps>>( |
||||
'configProvider', |
||||
defaultConfigProvider, |
||||
); |
||||
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); |
||||
const direction = computed(() => props.direction ?? configProvider.direction); |
||||
const rootPrefixCls = computed(() => configProvider.getPrefixCls()); |
||||
const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton); |
||||
const renderEmpty = computed(() => configProvider.renderEmpty); |
||||
const space = computed(() => configProvider.space); |
||||
const pageHeader = computed(() => configProvider.pageHeader); |
||||
const form = computed(() => configProvider.form); |
||||
const getTargetContainer = computed( |
||||
() => props.getTargetContainer || configProvider.getTargetContainer, |
||||
); |
||||
const getPopupContainer = computed( |
||||
() => props.getPopupContainer || configProvider.getPopupContainer, |
||||
); |
||||
|
||||
const dropdownMatchSelectWidth = computed<boolean | number>( |
||||
() => props.dropdownMatchSelectWidth ?? configProvider.dropdownMatchSelectWidth, |
||||
); |
||||
const virtual = computed( |
||||
() => |
||||
(props.virtual === undefined ? configProvider.virtual !== false : props.virtual !== false) && |
||||
dropdownMatchSelectWidth.value !== false, |
||||
); |
||||
const size = computed(() => props.size || configProvider.componentSize); |
||||
const autocomplete = computed(() => props.autocomplete || configProvider.input?.autocomplete); |
||||
const csp = computed(() => configProvider.csp); |
||||
return { |
||||
configProvider, |
||||
prefixCls, |
||||
direction, |
||||
size, |
||||
getTargetContainer, |
||||
getPopupContainer, |
||||
space, |
||||
pageHeader, |
||||
form, |
||||
autoInsertSpaceInButton, |
||||
renderEmpty, |
||||
virtual, |
||||
dropdownMatchSelectWidth, |
||||
rootPrefixCls, |
||||
getPrefixCls: configProvider.getPrefixCls, |
||||
autocomplete, |
||||
csp, |
||||
}; |
||||
}; |
@ -0,0 +1,30 @@
|
||||
import { ref } from 'vue'; |
||||
import canUseDom from '../../_util/canUseDom'; |
||||
|
||||
let uuid = 0; |
||||
|
||||
/** Is client side and not jsdom */ |
||||
export const isBrowserClient = process.env.NODE_ENV !== 'test' && canUseDom(); |
||||
|
||||
/** Get unique id for accessibility usage */ |
||||
export function getUUID(): number | string { |
||||
let retId: string | number; |
||||
|
||||
// Test never reach
|
||||
/* istanbul ignore if */ |
||||
if (isBrowserClient) { |
||||
retId = uuid; |
||||
uuid += 1; |
||||
} else { |
||||
retId = 'TEST_OR_SSR'; |
||||
} |
||||
|
||||
return retId; |
||||
} |
||||
|
||||
export default function useId(id = ref('')) { |
||||
// Inner id for accessibility usage. Only work in client side
|
||||
const innerId = `vc_unique_${getUUID()}`; |
||||
|
||||
return id.value || innerId; |
||||
} |
@ -0,0 +1,48 @@
|
||||
import type { Ref } from 'vue'; |
||||
import { computed, watchEffect } from 'vue'; |
||||
import { updateCSS, removeCSS } from '../../vc-util/Dom/dynamicCSS'; |
||||
import getScrollBarSize from '../../_util/getScrollBarSize'; |
||||
|
||||
const UNIQUE_ID = `vc-util-locker-${Date.now()}`; |
||||
|
||||
let uuid = 0; |
||||
|
||||
/**../vc-util/Dom/dynam |
||||
* Test usage export. Do not use in your production |
||||
*/ |
||||
export function isBodyOverflowing() { |
||||
return ( |
||||
document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && |
||||
window.innerWidth > document.body.offsetWidth |
||||
); |
||||
} |
||||
|
||||
export default function useScrollLocker(lock?: Ref<boolean>) { |
||||
const mergedLock = computed(() => !!lock && !!lock.value); |
||||
uuid += 1; |
||||
const id = `${UNIQUE_ID}_${uuid}`; |
||||
|
||||
watchEffect( |
||||
onClear => { |
||||
if (mergedLock.value) { |
||||
const scrollbarSize = getScrollBarSize(); |
||||
const isOverflow = isBodyOverflowing(); |
||||
|
||||
updateCSS( |
||||
` |
||||
html body { |
||||
overflow-y: hidden; |
||||
${isOverflow ? `width: calc(100% - ${scrollbarSize}px);` : ''} |
||||
}`,
|
||||
id, |
||||
); |
||||
} else { |
||||
removeCSS(id); |
||||
} |
||||
onClear(() => { |
||||
removeCSS(id); |
||||
}); |
||||
}, |
||||
{ flush: 'post' }, |
||||
); |
||||
} |
@ -1,30 +0,0 @@
|
||||
import type { ComputedRef, UnwrapRef } from 'vue'; |
||||
import { computed, inject, provide } from 'vue'; |
||||
import type { ConfigProviderProps, SizeType } from '../../config-provider'; |
||||
import { defaultConfigProvider } from '../../config-provider'; |
||||
|
||||
const sizeProvider = Symbol('SizeProvider'); |
||||
|
||||
const useProvideSize = <T = SizeType>(props: Record<any, any>): ComputedRef<T> => { |
||||
const configProvider = inject<UnwrapRef<ConfigProviderProps>>( |
||||
'configProvider', |
||||
defaultConfigProvider, |
||||
); |
||||
const size = computed<T>(() => props.size || configProvider.componentSize); |
||||
provide(sizeProvider, size); |
||||
return size; |
||||
}; |
||||
|
||||
const useInjectSize = <T = SizeType>(props?: Record<any, any>): ComputedRef<T> => { |
||||
const size: ComputedRef<T> = props |
||||
? computed(() => props.size) |
||||
: inject( |
||||
sizeProvider, |
||||
computed(() => 'default' as unknown as T), |
||||
); |
||||
return size; |
||||
}; |
||||
|
||||
export { useInjectSize, sizeProvider, useProvideSize }; |
||||
|
||||
export default useProvideSize; |
@ -1,110 +0,0 @@
|
||||
// MIT License from https://github.com/kaimallea/isMobile
|
||||
|
||||
const applePhone = /iPhone/i; |
||||
const appleIpod = /iPod/i; |
||||
const appleTablet = /iPad/i; |
||||
const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile'
|
||||
const androidTablet = /Android/i; |
||||
const amazonPhone = /\bAndroid(?:.+)SD4930UR\b/i; |
||||
const amazonTablet = /\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i; |
||||
const windowsPhone = /Windows Phone/i; |
||||
const windowsTablet = /\bWindows(?:.+)ARM\b/i; // Match 'Windows' AND 'ARM'
|
||||
const otherBlackberry = /BlackBerry/i; |
||||
const otherBlackberry10 = /BB10/i; |
||||
const otherOpera = /Opera Mini/i; |
||||
const otherChrome = /\b(CriOS|Chrome)(?:.+)Mobile/i; |
||||
const otherFirefox = /Mobile(?:.+)Firefox\b/i; // Match 'Mobile' AND 'Firefox'
|
||||
|
||||
function match(regex, userAgent) { |
||||
return regex.test(userAgent); |
||||
} |
||||
|
||||
function isMobile(userAgent) { |
||||
let ua = userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : ''); |
||||
|
||||
// Facebook mobile app's integrated browser adds a bunch of strings that
|
||||
// match everything. Strip it out if it exists.
|
||||
let tmp = ua.split('[FBAN'); |
||||
if (typeof tmp[1] !== 'undefined') { |
||||
[ua] = tmp; |
||||
} |
||||
|
||||
// Twitter mobile app's integrated browser on iPad adds a "Twitter for
|
||||
// iPhone" string. Same probably happens on other tablet platforms.
|
||||
// This will confuse detection so strip it out if it exists.
|
||||
tmp = ua.split('Twitter'); |
||||
if (typeof tmp[1] !== 'undefined') { |
||||
[ua] = tmp; |
||||
} |
||||
|
||||
const result = { |
||||
apple: { |
||||
phone: match(applePhone, ua) && !match(windowsPhone, ua), |
||||
ipod: match(appleIpod, ua), |
||||
tablet: !match(applePhone, ua) && match(appleTablet, ua) && !match(windowsPhone, ua), |
||||
device: |
||||
(match(applePhone, ua) || match(appleIpod, ua) || match(appleTablet, ua)) && |
||||
!match(windowsPhone, ua), |
||||
}, |
||||
amazon: { |
||||
phone: match(amazonPhone, ua), |
||||
tablet: !match(amazonPhone, ua) && match(amazonTablet, ua), |
||||
device: match(amazonPhone, ua) || match(amazonTablet, ua), |
||||
}, |
||||
android: { |
||||
phone: |
||||
(!match(windowsPhone, ua) && match(amazonPhone, ua)) || |
||||
(!match(windowsPhone, ua) && match(androidPhone, ua)), |
||||
tablet: |
||||
!match(windowsPhone, ua) && |
||||
!match(amazonPhone, ua) && |
||||
!match(androidPhone, ua) && |
||||
(match(amazonTablet, ua) || match(androidTablet, ua)), |
||||
device: |
||||
(!match(windowsPhone, ua) && |
||||
(match(amazonPhone, ua) || |
||||
match(amazonTablet, ua) || |
||||
match(androidPhone, ua) || |
||||
match(androidTablet, ua))) || |
||||
match(/\bokhttp\b/i, ua), |
||||
}, |
||||
windows: { |
||||
phone: match(windowsPhone, ua), |
||||
tablet: match(windowsTablet, ua), |
||||
device: match(windowsPhone, ua) || match(windowsTablet, ua), |
||||
}, |
||||
other: { |
||||
blackberry: match(otherBlackberry, ua), |
||||
blackberry10: match(otherBlackberry10, ua), |
||||
opera: match(otherOpera, ua), |
||||
firefox: match(otherFirefox, ua), |
||||
chrome: match(otherChrome, ua), |
||||
device: |
||||
match(otherBlackberry, ua) || |
||||
match(otherBlackberry10, ua) || |
||||
match(otherOpera, ua) || |
||||
match(otherFirefox, ua) || |
||||
match(otherChrome, ua), |
||||
}, |
||||
|
||||
// Additional
|
||||
any: null, |
||||
phone: null, |
||||
tablet: null, |
||||
}; |
||||
result.any = |
||||
result.apple.device || result.android.device || result.windows.device || result.other.device; |
||||
|
||||
// excludes 'other' devices and ipods, targeting touchscreen phones
|
||||
result.phone = result.apple.phone || result.android.phone || result.windows.phone; |
||||
result.tablet = result.apple.tablet || result.android.tablet || result.windows.tablet; |
||||
|
||||
return result; |
||||
} |
||||
|
||||
const defaultResult = { |
||||
...isMobile(), |
||||
isMobile, |
||||
}; |
||||
|
||||
export default defaultResult; |
@ -1,75 +1,85 @@
|
||||
import { computed } from 'vue'; |
||||
import type { GlobalToken } from '../theme/interface'; |
||||
import { useToken } from '../theme/internal'; |
||||
|
||||
export type Breakpoint = 'xxxl' | 'xxl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs'; |
||||
export type BreakpointMap = Record<Breakpoint, string>; |
||||
export type ScreenMap = Partial<Record<Breakpoint, boolean>>; |
||||
export type ScreenSizeMap = Partial<Record<Breakpoint, number>>; |
||||
|
||||
export const responsiveArray: Breakpoint[] = ['xxxl', 'xxl', 'xl', 'lg', 'md', 'sm', 'xs']; |
||||
type SubscribeFunc = (screens: ScreenMap) => void; |
||||
|
||||
export const responsiveMap: BreakpointMap = { |
||||
xs: '(max-width: 575px)', |
||||
sm: '(min-width: 576px)', |
||||
md: '(min-width: 768px)', |
||||
lg: '(min-width: 992px)', |
||||
xl: '(min-width: 1200px)', |
||||
xxl: '(min-width: 1600px)', |
||||
xxxl: '(min-width: 2000px)', |
||||
}; |
||||
const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({ |
||||
xs: `(max-width: ${token.screenXSMax}px)`, |
||||
sm: `(min-width: ${token.screenSM}px)`, |
||||
md: `(min-width: ${token.screenMD}px)`, |
||||
lg: `(min-width: ${token.screenLG}px)`, |
||||
xl: `(min-width: ${token.screenXL}px)`, |
||||
xxl: `(min-width: ${token.screenXXL}px)`, |
||||
xxxl: `{min-width: ${token.screenXXXL}px}`, |
||||
}); |
||||
|
||||
type SubscribeFunc = (screens: ScreenMap) => void; |
||||
const subscribers = new Map<Number, SubscribeFunc>(); |
||||
let subUid = -1; |
||||
let screens = {}; |
||||
export default function useResponsiveObserver() { |
||||
const [, token] = useToken(); |
||||
|
||||
const responsiveObserve = { |
||||
matchHandlers: {} as { |
||||
[prop: string]: { |
||||
mql: MediaQueryList; |
||||
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; |
||||
}; |
||||
}, |
||||
dispatch(pointMap: ScreenMap) { |
||||
screens = pointMap; |
||||
subscribers.forEach(func => func(screens)); |
||||
return subscribers.size >= 1; |
||||
}, |
||||
subscribe(func: SubscribeFunc): number { |
||||
if (!subscribers.size) this.register(); |
||||
subUid += 1; |
||||
subscribers.set(subUid, func); |
||||
func(screens); |
||||
return subUid; |
||||
}, |
||||
unsubscribe(token: number) { |
||||
subscribers.delete(token); |
||||
if (!subscribers.size) this.unregister(); |
||||
}, |
||||
unregister() { |
||||
Object.keys(responsiveMap).forEach((screen: string) => { |
||||
const matchMediaQuery = responsiveMap[screen]; |
||||
const handler = this.matchHandlers[matchMediaQuery]; |
||||
handler?.mql.removeListener(handler?.listener); |
||||
}); |
||||
subscribers.clear(); |
||||
}, |
||||
register() { |
||||
Object.keys(responsiveMap).forEach((screen: string) => { |
||||
const matchMediaQuery = responsiveMap[screen]; |
||||
const listener = ({ matches }: { matches: boolean }) => { |
||||
this.dispatch({ |
||||
...screens, |
||||
[screen]: matches, |
||||
}); |
||||
}; |
||||
const mql = window.matchMedia(matchMediaQuery); |
||||
mql.addListener(listener); |
||||
this.matchHandlers[matchMediaQuery] = { |
||||
mql, |
||||
listener, |
||||
}; |
||||
return computed(() => { |
||||
const responsiveMap: BreakpointMap = getResponsiveMap(token.value); |
||||
const subscribers = new Map<Number, SubscribeFunc>(); |
||||
let subUid = -1; |
||||
let screens = {}; |
||||
|
||||
listener(mql); |
||||
}); |
||||
}, |
||||
}; |
||||
return { |
||||
matchHandlers: {} as { |
||||
[prop: string]: { |
||||
mql: MediaQueryList; |
||||
listener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null; |
||||
}; |
||||
}, |
||||
dispatch(pointMap: ScreenMap) { |
||||
screens = pointMap; |
||||
subscribers.forEach(func => func(screens)); |
||||
return subscribers.size >= 1; |
||||
}, |
||||
subscribe(func: SubscribeFunc): number { |
||||
if (!subscribers.size) this.register(); |
||||
subUid += 1; |
||||
subscribers.set(subUid, func); |
||||
func(screens); |
||||
return subUid; |
||||
}, |
||||
unsubscribe(paramToken: number) { |
||||
subscribers.delete(paramToken); |
||||
if (!subscribers.size) this.unregister(); |
||||
}, |
||||
unregister() { |
||||
Object.keys(responsiveMap).forEach((screen: string) => { |
||||
const matchMediaQuery = responsiveMap[screen]; |
||||
const handler = this.matchHandlers[matchMediaQuery]; |
||||
handler?.mql.removeListener(handler?.listener); |
||||
}); |
||||
subscribers.clear(); |
||||
}, |
||||
register() { |
||||
Object.keys(responsiveMap).forEach((screen: string) => { |
||||
const matchMediaQuery = responsiveMap[screen]; |
||||
const listener = ({ matches }: { matches: boolean }) => { |
||||
this.dispatch({ |
||||
...screens, |
||||
[screen]: matches, |
||||
}); |
||||
}; |
||||
const mql = window.matchMedia(matchMediaQuery); |
||||
mql.addListener(listener); |
||||
this.matchHandlers[matchMediaQuery] = { |
||||
mql, |
||||
listener, |
||||
}; |
||||
|
||||
export default responsiveObserve; |
||||
listener(mql); |
||||
}); |
||||
}, |
||||
responsiveMap, |
||||
}; |
||||
}); |
||||
} |
||||
|
@ -0,0 +1,23 @@
|
||||
import type { ValidateStatus } from '../form/FormItem'; |
||||
import classNames from './classNames'; |
||||
|
||||
const InputStatuses = ['warning', 'error', ''] as const; |
||||
|
||||
export type InputStatus = (typeof InputStatuses)[number]; |
||||
|
||||
export function getStatusClassNames( |
||||
prefixCls: string, |
||||
status?: ValidateStatus, |
||||
hasFeedback?: boolean, |
||||
) { |
||||
return classNames({ |
||||
[`${prefixCls}-status-success`]: status === 'success', |
||||
[`${prefixCls}-status-warning`]: status === 'warning', |
||||
[`${prefixCls}-status-error`]: status === 'error', |
||||
[`${prefixCls}-status-validating`]: status === 'validating', |
||||
[`${prefixCls}-has-feedback`]: hasFeedback, |
||||
}); |
||||
} |
||||
|
||||
export const getMergedStatus = (contextStatus?: ValidateStatus, customStatus?: InputStatus) => |
||||
customStatus || contextStatus; |
@ -1,42 +0,0 @@
|
||||
import getScrollBarSize from './getScrollBarSize'; |
||||
import setStyle from './setStyle'; |
||||
|
||||
function isBodyOverflowing() { |
||||
return ( |
||||
document.body.scrollHeight > (window.innerHeight || document.documentElement.clientHeight) && |
||||
window.innerWidth > document.body.offsetWidth |
||||
); |
||||
} |
||||
|
||||
let cacheStyle = {}; |
||||
|
||||
export default (close?: boolean) => { |
||||
if (!isBodyOverflowing() && !close) { |
||||
return; |
||||
} |
||||
|
||||
// https://github.com/ant-design/ant-design/issues/19729
|
||||
const scrollingEffectClassName = 'ant-scrolling-effect'; |
||||
const scrollingEffectClassNameReg = new RegExp(`${scrollingEffectClassName}`, 'g'); |
||||
const bodyClassName = document.body.className; |
||||
|
||||
if (close) { |
||||
if (!scrollingEffectClassNameReg.test(bodyClassName)) return; |
||||
setStyle(cacheStyle); |
||||
cacheStyle = {}; |
||||
document.body.className = bodyClassName.replace(scrollingEffectClassNameReg, '').trim(); |
||||
return; |
||||
} |
||||
|
||||
const scrollBarSize = getScrollBarSize(); |
||||
if (scrollBarSize) { |
||||
cacheStyle = setStyle({ |
||||
position: 'relative', |
||||
width: `calc(100% - ${scrollBarSize}px)`, |
||||
}); |
||||
if (!scrollingEffectClassNameReg.test(bodyClassName)) { |
||||
const addClassName = `${bodyClassName} ${scrollingEffectClassName}`; |
||||
document.body.className = addClassName.trim(); |
||||
} |
||||
} |
||||
}; |
@ -1,47 +1,29 @@
|
||||
import raf from './raf'; |
||||
|
||||
export default function throttleByAnimationFrame(fn: (...args: any[]) => void) { |
||||
let requestId: number; |
||||
type throttledFn = (...args: any[]) => void; |
||||
|
||||
const later = (args: any[]) => () => { |
||||
type throttledCancelFn = { cancel: () => void }; |
||||
|
||||
function throttleByAnimationFrame<T extends any[]>(fn: (...args: T) => void) { |
||||
let requestId: number | null; |
||||
|
||||
const later = (args: T) => () => { |
||||
requestId = null; |
||||
fn(...args); |
||||
}; |
||||
|
||||
const throttled = (...args: any[]) => { |
||||
const throttled: throttledFn & throttledCancelFn = (...args: T) => { |
||||
if (requestId == null) { |
||||
requestId = raf(later(args)); |
||||
} |
||||
}; |
||||
|
||||
(throttled as any).cancel = () => raf.cancel(requestId!); |
||||
throttled.cancel = () => { |
||||
raf.cancel(requestId!); |
||||
requestId = null; |
||||
}; |
||||
|
||||
return throttled; |
||||
} |
||||
|
||||
export function throttleByAnimationFrameDecorator() { |
||||
// eslint-disable-next-line func-names
|
||||
return function (target: any, key: string, descriptor: any) { |
||||
const fn = descriptor.value; |
||||
let definingProperty = false; |
||||
return { |
||||
configurable: true, |
||||
get() { |
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { |
||||
return fn; |
||||
} |
||||
|
||||
const boundFn = throttleByAnimationFrame(fn.bind(this)); |
||||
definingProperty = true; |
||||
Object.defineProperty(this, key, { |
||||
value: boundFn, |
||||
configurable: true, |
||||
writable: true, |
||||
}); |
||||
definingProperty = false; |
||||
return boundFn; |
||||
}, |
||||
}; |
||||
}; |
||||
} |
||||
export default throttleByAnimationFrame; |
||||
|
@ -0,0 +1,17 @@
|
||||
export const groupKeysMap = (keys: string[]) => { |
||||
const map = new Map<string, number>(); |
||||
keys.forEach((key, index) => { |
||||
map.set(key, index); |
||||
}); |
||||
return map; |
||||
}; |
||||
|
||||
export const groupDisabledKeysMap = <RecordType extends any[]>(dataSource: RecordType) => { |
||||
const map = new Map<string, number>(); |
||||
dataSource.forEach(({ disabled, key }, index) => { |
||||
if (disabled) { |
||||
map.set(key, index); |
||||
} |
||||
}); |
||||
return map; |
||||
}; |
@ -1,7 +0,0 @@
|
||||
import warning, { resetWarned } from '../vc-util/warning'; |
||||
|
||||
export { resetWarned }; |
||||
|
||||
export default (valid, component, message = '') => { |
||||
warning(valid, `[antdv: ${component}] ${message}`); |
||||
}; |
@ -0,0 +1,21 @@
|
||||
import vcWarning, { resetWarned } from '../vc-util/warning'; |
||||
|
||||
export { resetWarned }; |
||||
export function noop() {} |
||||
|
||||
type Warning = (valid: boolean, component: string, message?: string) => void; |
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
let warning: Warning = noop; |
||||
if (process.env.NODE_ENV !== 'production') { |
||||
warning = (valid, component, message) => { |
||||
vcWarning(valid, `[ant-design-vue: ${component}] ${message}`); |
||||
|
||||
// StrictMode will inject console which will not throw warning in React 17.
|
||||
if (process.env.NODE_ENV === 'test') { |
||||
resetWarned(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
export default warning; |
@ -1,178 +0,0 @@
|
||||
import { nextTick, defineComponent, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'; |
||||
import TransitionEvents from './css-animation/Event'; |
||||
import raf from './raf'; |
||||
import { findDOMNode } from './props-util'; |
||||
import useConfigInject from './hooks/useConfigInject'; |
||||
let styleForPesudo: HTMLStyleElement; |
||||
|
||||
// Where el is the DOM element you'd like to test for visibility |
||||
function isHidden(element: HTMLElement) { |
||||
if (process.env.NODE_ENV === 'test') { |
||||
return false; |
||||
} |
||||
return !element || element.offsetParent === null; |
||||
} |
||||
function isNotGrey(color: string) { |
||||
// eslint-disable-next-line no-useless-escape |
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/); |
||||
if (match && match[1] && match[2] && match[3]) { |
||||
return !(match[1] === match[2] && match[2] === match[3]); |
||||
} |
||||
return true; |
||||
} |
||||
export default defineComponent({ |
||||
compatConfig: { MODE: 3 }, |
||||
name: 'Wave', |
||||
props: { |
||||
insertExtraNode: Boolean, |
||||
disabled: Boolean, |
||||
}, |
||||
setup(props, { slots, expose }) { |
||||
const instance = getCurrentInstance(); |
||||
const { csp, prefixCls } = useConfigInject('', props); |
||||
expose({ |
||||
csp, |
||||
}); |
||||
let eventIns = null; |
||||
let clickWaveTimeoutId = null; |
||||
let animationStartId = null; |
||||
let animationStart = false; |
||||
let extraNode = null; |
||||
let isUnmounted = false; |
||||
const onTransitionStart = e => { |
||||
if (isUnmounted) return; |
||||
|
||||
const node = findDOMNode(instance); |
||||
if (!e || e.target !== node) { |
||||
return; |
||||
} |
||||
|
||||
if (!animationStart) { |
||||
resetEffect(node); |
||||
} |
||||
}; |
||||
const onTransitionEnd = (e: any) => { |
||||
if (!e || e.animationName !== 'fadeEffect') { |
||||
return; |
||||
} |
||||
resetEffect(e.target); |
||||
}; |
||||
const getAttributeName = () => { |
||||
const { insertExtraNode } = props; |
||||
return insertExtraNode |
||||
? `${prefixCls.value}-click-animating` |
||||
: `${prefixCls.value}-click-animating-without-extra-node`; |
||||
}; |
||||
const onClick = (node: HTMLElement, waveColor: string) => { |
||||
const { insertExtraNode, disabled } = props; |
||||
if (disabled || !node || isHidden(node) || node.className.indexOf('-leave') >= 0) { |
||||
return; |
||||
} |
||||
|
||||
extraNode = document.createElement('div'); |
||||
extraNode.className = `${prefixCls.value}-click-animating-node`; |
||||
const attributeName = getAttributeName(); |
||||
node.removeAttribute(attributeName); |
||||
node.setAttribute(attributeName, 'true'); |
||||
// Not white or transparent or grey |
||||
styleForPesudo = styleForPesudo || document.createElement('style'); |
||||
if ( |
||||
waveColor && |
||||
waveColor !== '#ffffff' && |
||||
waveColor !== 'rgb(255, 255, 255)' && |
||||
isNotGrey(waveColor) && |
||||
!/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color |
||||
waveColor !== 'transparent' |
||||
) { |
||||
// Add nonce if CSP exist |
||||
if (csp.value?.nonce) { |
||||
styleForPesudo.nonce = csp.value.nonce; |
||||
} |
||||
extraNode.style.borderColor = waveColor; |
||||
styleForPesudo.innerHTML = ` |
||||
[${prefixCls.value}-click-animating-without-extra-node='true']::after, .${prefixCls.value}-click-animating-node { |
||||
--antd-wave-shadow-color: ${waveColor}; |
||||
}`; |
||||
if (!document.body.contains(styleForPesudo)) { |
||||
document.body.appendChild(styleForPesudo); |
||||
} |
||||
} |
||||
if (insertExtraNode) { |
||||
node.appendChild(extraNode); |
||||
} |
||||
TransitionEvents.addStartEventListener(node, onTransitionStart); |
||||
TransitionEvents.addEndEventListener(node, onTransitionEnd); |
||||
}; |
||||
const resetEffect = (node: HTMLElement) => { |
||||
if (!node || node === extraNode || !(node instanceof Element)) { |
||||
return; |
||||
} |
||||
const { insertExtraNode } = props; |
||||
const attributeName = getAttributeName(); |
||||
node.setAttribute(attributeName, 'false'); // edge has bug on `removeAttribute` #14466 |
||||
if (styleForPesudo) { |
||||
styleForPesudo.innerHTML = ''; |
||||
} |
||||
if (insertExtraNode && extraNode && node.contains(extraNode)) { |
||||
node.removeChild(extraNode); |
||||
} |
||||
TransitionEvents.removeStartEventListener(node, onTransitionStart); |
||||
TransitionEvents.removeEndEventListener(node, onTransitionEnd); |
||||
}; |
||||
const bindAnimationEvent = (node: HTMLElement) => { |
||||
if ( |
||||
!node || |
||||
!node.getAttribute || |
||||
node.getAttribute('disabled') || |
||||
node.className.indexOf('disabled') >= 0 |
||||
) { |
||||
return; |
||||
} |
||||
const newClick = (e: MouseEvent) => { |
||||
// Fix radio button click twice |
||||
if ((e.target as any).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) { |
||||
return; |
||||
} |
||||
resetEffect(node); |
||||
// Get wave color from target |
||||
const waveColor = |
||||
getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible |
||||
getComputedStyle(node).getPropertyValue('border-color') || |
||||
getComputedStyle(node).getPropertyValue('background-color'); |
||||
clickWaveTimeoutId = setTimeout(() => onClick(node, waveColor), 0); |
||||
raf.cancel(animationStartId); |
||||
animationStart = true; |
||||
|
||||
// Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this. |
||||
animationStartId = raf(() => { |
||||
animationStart = false; |
||||
}, 10); |
||||
}; |
||||
node.addEventListener('click', newClick, true); |
||||
return { |
||||
cancel: () => { |
||||
node.removeEventListener('click', newClick, true); |
||||
}, |
||||
}; |
||||
}; |
||||
onMounted(() => { |
||||
nextTick(() => { |
||||
const node = findDOMNode(instance); |
||||
if (node.nodeType !== 1) { |
||||
return; |
||||
} |
||||
eventIns = bindAnimationEvent(node); |
||||
}); |
||||
}); |
||||
onBeforeUnmount(() => { |
||||
if (eventIns) { |
||||
eventIns.cancel(); |
||||
} |
||||
clearTimeout(clickWaveTimeoutId); |
||||
isUnmounted = true; |
||||
}); |
||||
return () => { |
||||
return slots.default?.()[0]; |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1,164 @@
|
||||
import type { CSSProperties } from 'vue'; |
||||
import { onBeforeUnmount, onMounted, Transition, render, defineComponent, shallowRef } from 'vue'; |
||||
import useState from '../hooks/useState'; |
||||
import { objectType } from '../type'; |
||||
import { getTargetWaveColor } from './util'; |
||||
import wrapperRaf from '../raf'; |
||||
function validateNum(value: number) { |
||||
return Number.isNaN(value) ? 0 : value; |
||||
} |
||||
|
||||
export interface WaveEffectProps { |
||||
className: string; |
||||
target: HTMLElement; |
||||
} |
||||
|
||||
const WaveEffect = defineComponent({ |
||||
props: { |
||||
target: objectType<HTMLElement>(), |
||||
className: String, |
||||
}, |
||||
setup(props) { |
||||
const divRef = shallowRef<HTMLDivElement | null>(null); |
||||
|
||||
const [color, setWaveColor] = useState<string | null>(null); |
||||
const [borderRadius, setBorderRadius] = useState<number[]>([]); |
||||
const [left, setLeft] = useState(0); |
||||
const [top, setTop] = useState(0); |
||||
const [width, setWidth] = useState(0); |
||||
const [height, setHeight] = useState(0); |
||||
const [enabled, setEnabled] = useState(false); |
||||
|
||||
function syncPos() { |
||||
const { target } = props; |
||||
const nodeStyle = getComputedStyle(target); |
||||
|
||||
// Get wave color from target |
||||
setWaveColor(getTargetWaveColor(target)); |
||||
|
||||
const isStatic = nodeStyle.position === 'static'; |
||||
|
||||
// Rect |
||||
const { borderLeftWidth, borderTopWidth } = nodeStyle; |
||||
setLeft(isStatic ? target.offsetLeft : validateNum(-parseFloat(borderLeftWidth))); |
||||
setTop(isStatic ? target.offsetTop : validateNum(-parseFloat(borderTopWidth))); |
||||
setWidth(target.offsetWidth); |
||||
setHeight(target.offsetHeight); |
||||
|
||||
// Get border radius |
||||
const { |
||||
borderTopLeftRadius, |
||||
borderTopRightRadius, |
||||
borderBottomLeftRadius, |
||||
borderBottomRightRadius, |
||||
} = nodeStyle; |
||||
|
||||
setBorderRadius( |
||||
[ |
||||
borderTopLeftRadius, |
||||
borderTopRightRadius, |
||||
borderBottomRightRadius, |
||||
borderBottomLeftRadius, |
||||
].map(radius => validateNum(parseFloat(radius))), |
||||
); |
||||
} |
||||
// Add resize observer to follow size |
||||
let resizeObserver: ResizeObserver; |
||||
let rafId: number; |
||||
let timeoutId: any; |
||||
const clear = () => { |
||||
clearTimeout(timeoutId); |
||||
wrapperRaf.cancel(rafId); |
||||
resizeObserver?.disconnect(); |
||||
}; |
||||
const removeDom = () => { |
||||
const holder = divRef.value?.parentElement; |
||||
if (holder) { |
||||
render(null, holder); |
||||
if (holder.parentElement) { |
||||
holder.parentElement.removeChild(holder); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
onMounted(() => { |
||||
clear(); |
||||
timeoutId = setTimeout(() => { |
||||
removeDom(); |
||||
}, 5000); |
||||
const { target } = props; |
||||
if (target) { |
||||
// We need delay to check position here |
||||
// since UI may change after click |
||||
rafId = wrapperRaf(() => { |
||||
syncPos(); |
||||
|
||||
setEnabled(true); |
||||
}); |
||||
|
||||
if (typeof ResizeObserver !== 'undefined') { |
||||
resizeObserver = new ResizeObserver(syncPos); |
||||
|
||||
resizeObserver.observe(target); |
||||
} |
||||
} |
||||
}); |
||||
onBeforeUnmount(() => { |
||||
clear(); |
||||
}); |
||||
|
||||
const onTransitionend = (e: TransitionEvent) => { |
||||
if (e.propertyName === 'opacity') { |
||||
removeDom(); |
||||
} |
||||
}; |
||||
return () => { |
||||
if (!enabled.value) { |
||||
return null; |
||||
} |
||||
const waveStyle = { |
||||
left: `${left.value}px`, |
||||
top: `${top.value}px`, |
||||
width: `${width.value}px`, |
||||
height: `${height.value}px`, |
||||
borderRadius: borderRadius.value.map(radius => `${radius}px`).join(' '), |
||||
} as CSSProperties & { |
||||
[name: string]: number | string; |
||||
}; |
||||
|
||||
if (color) { |
||||
waveStyle['--wave-color'] = color.value as string; |
||||
} |
||||
|
||||
return ( |
||||
<Transition |
||||
appear |
||||
name="wave-motion" |
||||
appearFromClass="wave-motion-appear" |
||||
appearActiveClass="wave-motion-appear" |
||||
appearToClass="wave-motion-appear wave-motion-appear-active" |
||||
> |
||||
<div |
||||
ref={divRef} |
||||
class={props.className} |
||||
style={waveStyle} |
||||
onTransitionend={onTransitionend} |
||||
/> |
||||
</Transition> |
||||
); |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
function showWaveEffect(node: HTMLElement, className: string) { |
||||
// Create holder |
||||
const holder = document.createElement('div'); |
||||
holder.style.position = 'absolute'; |
||||
holder.style.left = `0px`; |
||||
holder.style.top = `0px`; |
||||
node?.insertBefore(holder, node?.firstChild); |
||||
|
||||
render(<WaveEffect target={node} className={className} />, holder); |
||||
} |
||||
|
||||
export default showWaveEffect; |
@ -0,0 +1,96 @@
|
||||
import { |
||||
computed, |
||||
defineComponent, |
||||
getCurrentInstance, |
||||
nextTick, |
||||
onBeforeUnmount, |
||||
onMounted, |
||||
watch, |
||||
} from 'vue'; |
||||
import useConfigInject from '../../config-provider/hooks/useConfigInject'; |
||||
import isVisible from '../../vc-util/Dom/isVisible'; |
||||
import classNames from '../classNames'; |
||||
import { findDOMNode } from '../props-util'; |
||||
import useStyle from './style'; |
||||
import useWave from './useWave'; |
||||
|
||||
export interface WaveProps { |
||||
disabled?: boolean; |
||||
} |
||||
|
||||
export default defineComponent({ |
||||
compatConfig: { MODE: 3 }, |
||||
name: 'Wave', |
||||
props: { |
||||
disabled: Boolean, |
||||
}, |
||||
setup(props, { slots }) { |
||||
const instance = getCurrentInstance(); |
||||
const { prefixCls } = useConfigInject('wave', props); |
||||
|
||||
// ============================== Style =============================== |
||||
const [, hashId] = useStyle(prefixCls); |
||||
|
||||
// =============================== Wave =============================== |
||||
const showWave = useWave( |
||||
instance, |
||||
computed(() => classNames(prefixCls.value, hashId.value)), |
||||
); |
||||
let onClick: (e: MouseEvent) => void; |
||||
const clear = () => { |
||||
const node = findDOMNode(instance); |
||||
node.removeEventListener('click', onClick, true); |
||||
}; |
||||
|
||||
onMounted(() => { |
||||
watch( |
||||
() => props.disabled, |
||||
() => { |
||||
clear(); |
||||
nextTick(() => { |
||||
const node = findDOMNode(instance); |
||||
|
||||
if (!node || node.nodeType !== 1 || props.disabled) { |
||||
return; |
||||
} |
||||
|
||||
// Click handler |
||||
const onClick = (e: MouseEvent) => { |
||||
// Fix radio button click twice |
||||
if ( |
||||
(e.target as HTMLElement).tagName === 'INPUT' || |
||||
!isVisible(e.target as HTMLElement) || |
||||
// No need wave |
||||
!node.getAttribute || |
||||
node.getAttribute('disabled') || |
||||
(node as HTMLInputElement).disabled || |
||||
node.className.includes('disabled') || |
||||
node.className.includes('-leave') |
||||
) { |
||||
return; |
||||
} |
||||
|
||||
showWave(); |
||||
}; |
||||
|
||||
// Bind events |
||||
node.addEventListener('click', onClick, true); |
||||
}); |
||||
}, |
||||
{ |
||||
immediate: true, |
||||
flush: 'post', |
||||
}, |
||||
); |
||||
}); |
||||
onBeforeUnmount(() => { |
||||
clear(); |
||||
}); |
||||
|
||||
return () => { |
||||
// ============================== Render ============================== |
||||
const children = slots.default?.()[0]; |
||||
return children; |
||||
}; |
||||
}, |
||||
}); |
@ -0,0 +1,38 @@
|
||||
import { genComponentStyleHook } from '../../theme/internal'; |
||||
import type { FullToken, GenerateStyle } from '../../theme/internal'; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ComponentToken {} |
||||
|
||||
export type WaveToken = FullToken<'Wave'>; |
||||
|
||||
const genWaveStyle: GenerateStyle<WaveToken> = token => { |
||||
const { componentCls, colorPrimary } = token; |
||||
return { |
||||
[componentCls]: { |
||||
position: 'absolute', |
||||
background: 'transparent', |
||||
pointerEvents: 'none', |
||||
boxSizing: 'border-box', |
||||
color: `var(--wave-color, ${colorPrimary})`, |
||||
|
||||
boxShadow: `0 0 0 0 currentcolor`, |
||||
opacity: 0.2, |
||||
|
||||
// =================== Motion ===================
|
||||
'&.wave-motion-appear': { |
||||
transition: [ |
||||
`box-shadow 0.4s ${token.motionEaseOutCirc}`, |
||||
`opacity 2s ${token.motionEaseOutCirc}`, |
||||
].join(','), |
||||
|
||||
'&-active': { |
||||
boxShadow: `0 0 0 6px currentcolor`, |
||||
opacity: 0, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
}; |
||||
|
||||
export default genComponentStyleHook('Wave', token => [genWaveStyle(token)]); |
@ -0,0 +1,16 @@
|
||||
import type { ComponentInternalInstance, Ref } from 'vue'; |
||||
import { findDOMNode } from '../props-util'; |
||||
import showWaveEffect from './WaveEffect'; |
||||
|
||||
export default function useWave( |
||||
instance: ComponentInternalInstance | null, |
||||
className: Ref<string>, |
||||
): VoidFunction { |
||||
function showWave() { |
||||
const node = findDOMNode(instance); |
||||
|
||||
showWaveEffect(node, className.value); |
||||
} |
||||
|
||||
return showWave; |
||||
} |
@ -0,0 +1,35 @@
|
||||
export function isNotGrey(color: string) { |
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\d.]*)?\)/); |
||||
if (match && match[1] && match[2] && match[3]) { |
||||
return !(match[1] === match[2] && match[2] === match[3]); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
export function isValidWaveColor(color: string) { |
||||
return ( |
||||
color && |
||||
color !== '#fff' && |
||||
color !== '#ffffff' && |
||||
color !== 'rgb(255, 255, 255)' && |
||||
color !== 'rgba(255, 255, 255, 1)' && |
||||
isNotGrey(color) && |
||||
!/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color
|
||||
color !== 'transparent' |
||||
); |
||||
} |
||||
|
||||
export function getTargetWaveColor(node: HTMLElement) { |
||||
const { borderTopColor, borderColor, backgroundColor } = getComputedStyle(node); |
||||
if (isValidWaveColor(borderTopColor)) { |
||||
return borderTopColor; |
||||
} |
||||
if (isValidWaveColor(borderColor)) { |
||||
return borderColor; |
||||
} |
||||
if (isValidWaveColor(backgroundColor)) { |
||||
return backgroundColor; |
||||
} |
||||
return null; |
||||
} |
@ -1,6 +0,0 @@
|
||||
@import '../../style/themes/index'; |
||||
|
||||
.@{ant-prefix}-affix { |
||||
position: fixed; |
||||
z-index: @zindex-affix; |
||||
} |
@ -0,0 +1,27 @@
|
||||
import type { CSSObject } from '../../_util/cssinjs'; |
||||
import type { FullToken, GenerateStyle } from '../../theme/internal'; |
||||
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; |
||||
|
||||
interface AffixToken extends FullToken<'Affix'> { |
||||
zIndexPopup: number; |
||||
} |
||||
|
||||
// ============================== Shared ==============================
|
||||
const genSharedAffixStyle: GenerateStyle<AffixToken> = (token): CSSObject => { |
||||
const { componentCls } = token; |
||||
|
||||
return { |
||||
[componentCls]: { |
||||
position: 'fixed', |
||||
zIndex: token.zIndexPopup, |
||||
}, |
||||
}; |
||||
}; |
||||
|
||||
// ============================== Export ==============================
|
||||
export default genComponentStyleHook('Affix', token => { |
||||
const affixToken = mergeToken<AffixToken>(token, { |
||||
zIndexPopup: token.zIndexBase + 10, |
||||
}); |
||||
return [genSharedAffixStyle(affixToken)]; |
||||
}); |
@ -1,2 +0,0 @@
|
||||
import '../../style/index.less'; |
||||
import './index.less'; |
@ -0,0 +1,57 @@
|
||||
<docs> |
||||
--- |
||||
order: 0 |
||||
title: |
||||
zh-CN: 操作 |
||||
en-US: Action |
||||
--- |
||||
|
||||
## zh-CN |
||||
|
||||
可以在右上角自定义操作项。 |
||||
|
||||
## en-US |
||||
|
||||
Custom action. |
||||
|
||||
</docs> |
||||
|
||||
<template> |
||||
<a-space direction="vertical" style="width: 100%"> |
||||
<a-alert message="Success Tips" type="success" show-icon closable> |
||||
<template #action> |
||||
<a-button size="small" type="text">UNDO</a-button> |
||||
</template> |
||||
</a-alert> |
||||
<a-alert |
||||
message="Error Text" |
||||
show-icon |
||||
description="Error Description Error Description Error Description Error Description" |
||||
type="error" |
||||
> |
||||
<template #action> |
||||
<a-button size="small" danger>Detail</a-button> |
||||
</template> |
||||
</a-alert> |
||||
<a-alert message="Warning Text" type="warning" closable> |
||||
<template #action> |
||||
<a-space> |
||||
<a-button size="small" type="ghost">Done</a-button> |
||||
</a-space> |
||||
</template> |
||||
</a-alert> |
||||
<a-alert |
||||
message="Info Text" |
||||
description="Info Description Info Description Info Description Info Description" |
||||
type="info" |
||||
closable |
||||
> |
||||
<template #action> |
||||
<a-space direction="vertical"> |
||||
<a-button size="small" type="primary">Accept</a-button> |
||||
<a-button size="small" danger type="ghost">Decline</a-button> |
||||
</a-space> |
||||
</template> |
||||
</a-alert> |
||||
</a-space> |
||||
</template> |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue