From c73c665e25d4e9136c441572a59be29b783d4f57 Mon Sep 17 00:00:00 2001 From: "Daybrush (Younkue Choi)" Date: Fri, 28 Jun 2024 13:01:51 +0900 Subject: [PATCH] feat: add `stretch` option (#107) * feat: add stretchRange option in JustifiedGrid * feat: add passUnstretchRow * fix: fix stretch cost * feat: add index property in GridItem * feat: add stretch, stretchRange, passUnstretchRow * demo: add stretch demo * test: test `stretch` options * fix: fix FrameGrid useless code --- package.json | 2 +- .../lib/grids/ngx-justified-grid.component.ts | 3 + .../0-JustifiedGrid.stories.ts | 1 + .../5-StretchedJustifiedGrid.stories.ts | 52 +++ .../NgxJustifiedGridApp/app.component.html | 4 + .../apps/NgxJustifiedGridApp/app.component.ts | 3 + .../app.component.html | 3 + .../app.component.ts | 3 + .../app.component.html | 3 + .../app.component.ts | 3 + .../0-JustifiedGrid.stories.tsx | 1 + .../5-StretchedJustifiedGrid.stories.ts | 26 ++ .../apps/ReactJustifiedGridApp.tsx | 3 + .../ReactKeepRatioWithMaintainedTargetApp.tsx | 3 + .../apps/ReactKeepRatioWithOffsetApp.tsx | 3 + .../0-JustifiedGrid.stories.tsx | 1 + .../5-StretchedJustifiedGrid.stories.ts | 28 ++ .../apps/SvelteJustifiedGridApp.svelte | 6 + ...lteKeepRatioWithMaintainedTargetApp.svelte | 6 + .../apps/SvelteKeepRatioWithOffsetApp.svelte | 6 + .../0-JustifiedGrid.stories.tsx | 1 + .../5-StretchedJustifiedGrid.stories.ts | 28 ++ .../apps/VueJustifiedGridApp.vue | 3 + .../VueKeepRatioWithMaintainedTargetApp.vue | 6 + .../apps/VueKeepRatioWithOffsetApp.vue | 6 + src/Grid.ts | 3 + src/GridItem.ts | 4 + src/ItemRenderer.ts | 5 +- src/ResizeWatcher.ts | 12 +- src/grids/FrameGrid.ts | 1 - src/grids/JustifiedGrid.ts | 416 ++++++++++++++++-- src/types.ts | 5 + src/utils.ts | 15 + .../0-JustifiedGrid.stories.tsx | 1 + .../5-StretchedJustifiedGrid.stories.tsx | 80 ++++ .../apps/VanillaJustifiedGridApp.tsx | 3 + stories/templates/controls.ts | 15 + stories/templates/default.css | 3 + test/e2e/manual/index.html | 3 + test/manual/justifiedgrid-stretch.html | 117 +++++ .../manual/justifiedinfinitegrid-stretch.html | 125 ++++++ test/unit/JustifiedGrid.spec.ts | 184 ++++++++ 42 files changed, 1146 insertions(+), 50 deletions(-) create mode 100644 packages/ngx-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts create mode 100644 packages/react-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts create mode 100644 packages/svelte-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts create mode 100644 packages/vue-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts create mode 100644 stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.tsx create mode 100644 test/manual/justifiedgrid-stretch.html create mode 100644 test/manual/justifiedinfinitegrid-stretch.html diff --git a/package.json b/package.json index ac66b18..c04f2c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@egjs/grid", - "version": "1.15.0", + "version": "1.16.0-beta.7", "description": "A component that can arrange items according to the type of grids", "main": "dist/grid.cjs.js", "module": "dist/grid.esm.js", diff --git a/packages/ngx-grid/projects/ngx-grid/src/lib/grids/ngx-justified-grid.component.ts b/packages/ngx-grid/projects/ngx-grid/src/lib/grids/ngx-justified-grid.component.ts index b09ae3f..fe21f67 100644 --- a/packages/ngx-grid/projects/ngx-grid/src/lib/grids/ngx-justified-grid.component.ts +++ b/packages/ngx-grid/projects/ngx-grid/src/lib/grids/ngx-justified-grid.component.ts @@ -21,4 +21,7 @@ export class NgxJustifiedGridComponent @Input() sizeRange!: Required['sizeRange']; @Input() isCroppedSize!: Required['isCroppedSize']; @Input() displayedRow!: Required['displayedRow']; + @Input() stretch!: Required['stretch']; + @Input() stretchRange!: Required['stretchRange']; + @Input() passUnstretchRow!: Required['passUnstretchRow']; } diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.ts b/packages/ngx-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.ts index f56869b..88f0b3e 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.ts +++ b/packages/ngx-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.ts @@ -15,3 +15,4 @@ export * from "./1-JustifiedGrid.stories"; export * from "./2-CroppedJustifiedGrid.stories"; export * from "./3-KeepRatioWithOffset.stories"; export * from "./4-KeepRatioWithMaintainedTarget.stories"; +export * from "./5-StretchedJustifiedGrid.stories"; diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts b/packages/ngx-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts new file mode 100644 index 0000000..a571c91 --- /dev/null +++ b/packages/ngx-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts @@ -0,0 +1,52 @@ +import { AppComponent } from './apps/NgxKeepRatioWithMaintainedTargetApp/app.component'; +import { JUSTIFIED_GRID_CONTROLS } from '../../../../stories/templates/controls'; +import { convertPath, convertAngularTemplate, makeArgs } from '../../../../stories/utils'; +import HTML_TEMPLATE from '!!raw-loader!./apps/NgxKeepRatioWithMaintainedTargetApp/app.component.html'; +import CSS_TEMPLATE from '!!raw-loader!../../../../stories/templates/default.css'; +import RawApp from '!!raw-loader!./apps/NgxKeepRatioWithMaintainedTargetApp/app.component.ts'; +import MODULE_TEMPLATE from '!!raw-loader!../apps/default/app.module.ts'; + +export const StretchedJustifiedGridTemplate = (props: any) => ({ + component: AppComponent, + props: { + ...props, + key: JSON.stringify(props), + }, +}); +StretchedJustifiedGridTemplate.storyName = "Stretched Items with JustifiedGrid"; + + +StretchedJustifiedGridTemplate.argTypes = JUSTIFIED_GRID_CONTROLS; +StretchedJustifiedGridTemplate.args = { + ...makeArgs(StretchedJustifiedGridTemplate.argTypes), + stretch: true, + sizeRange: [200, 300], +}; + +StretchedJustifiedGridTemplate.parameters = { + preview: [ + { + tab: "CSS", + template: CSS_TEMPLATE, + language: "css", + }, + { + tab: "Angular", + template: HTML_TEMPLATE, + language: "html", + description: "app.component.html", + }, + { + tab: "Angular", + template: convertAngularTemplate(convertPath(RawApp, "projects", "@egjs/ngx-grid")), + language: "ts", + description: "app.component.ts", + }, + { + tab: "Angular", + template: convertPath(MODULE_TEMPLATE, "projects", "@egjs/ngx-grid"), + language: "ts", + description: "app.module.ts", + }, + ], +}; diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.html b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.html index 83f1cb1..c246660 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.html +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.html @@ -6,6 +6,10 @@ [sizeRange]="sizeRange" [isCroppedSize]="isCroppedSize" [displayedRow]="displayedRow" + [stretch]="stretch" + [stretchRange]="stretchRange" + [passUnstretchRow]="passUnstretchRow" + *ngFor="let item of [0]; trackBy: trackBy;" >
1
diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.ts b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.ts index 1ca9cf5..89492c4 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.ts +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxJustifiedGridApp/app.component.ts @@ -13,6 +13,9 @@ export class AppComponent { @Input() sizeRange: any; @Input() isCroppedSize: any; @Input() displayedRow: any; + @Input() stretch!: any; + @Input() stretchRange!: any; + @Input() passUnstretchRow!: any; @Input() key: any; trackBy = () => this.key; } diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.html b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.html index 602f392..2fff58c 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.html +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.html @@ -7,6 +7,9 @@ [sizeRange]="sizeRange" [isCroppedSize]="isCroppedSize" [displayedRow]="displayedRow" + [stretch]="stretch" + [stretchRange]="stretchRange" + [passUnstretchRow]="passUnstretchRow" *ngFor="let item of [0]; trackBy: trackBy;" >
diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.ts b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.ts index 1ca9cf5..89492c4 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.ts +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithMaintainedTargetApp/app.component.ts @@ -13,6 +13,9 @@ export class AppComponent { @Input() sizeRange: any; @Input() isCroppedSize: any; @Input() displayedRow: any; + @Input() stretch!: any; + @Input() stretchRange!: any; + @Input() passUnstretchRow!: any; @Input() key: any; trackBy = () => this.key; } diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.html b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.html index 683de95..f68432c 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.html +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.html @@ -7,6 +7,9 @@ [sizeRange]="sizeRange" [isCroppedSize]="isCroppedSize" [displayedRow]="displayedRow" + [stretch]="stretch" + [stretchRange]="stretchRange" + [passUnstretchRow]="passUnstretchRow" *ngFor="let item of [0]; trackBy: trackBy;" >
diff --git a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.ts b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.ts index 1ca9cf5..89492c4 100644 --- a/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.ts +++ b/packages/ngx-grid/stories/2-JustifiedGrid/apps/NgxKeepRatioWithOffsetApp/app.component.ts @@ -13,6 +13,9 @@ export class AppComponent { @Input() sizeRange: any; @Input() isCroppedSize: any; @Input() displayedRow: any; + @Input() stretch!: any; + @Input() stretchRange!: any; + @Input() passUnstretchRow!: any; @Input() key: any; trackBy = () => this.key; } diff --git a/packages/react-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx b/packages/react-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx index c057df1..f27e8c1 100644 --- a/packages/react-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx +++ b/packages/react-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx @@ -7,3 +7,4 @@ export * from "./1-JustifiedGrid.stories"; export * from "./2-CroppedJustifiedGrid.stories"; export * from "./3-KeepRatioWithOffset.stories"; export * from "./4-KeepRatioWithMaintainedTarget.stories"; +export * from "./5-StretchedJustifiedGrid.stories"; diff --git a/packages/react-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts b/packages/react-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts new file mode 100644 index 0000000..9e9cc5b --- /dev/null +++ b/packages/react-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts @@ -0,0 +1,26 @@ +import App from "./apps/ReactKeepRatioWithMaintainedTargetApp"; +import RawApp from "!!raw-loader!./apps/ReactKeepRatioWithMaintainedTargetApp"; +import { JUSTIFIED_GRID_CONTROLS } from "../../../../stories/templates/controls"; +import { makeArgs, convertReactTemplate, convertPath } from "../../../../stories/utils"; +import "../../../../stories/templates/default.css"; + +export const StretchedJustifiedGridTemplate = App.bind({}) as any; + + +StretchedJustifiedGridTemplate.storyName = "Stretched Items with JustifiedGrid"; +StretchedJustifiedGridTemplate.argTypes = JUSTIFIED_GRID_CONTROLS; +StretchedJustifiedGridTemplate.args = { + ...makeArgs(StretchedJustifiedGridTemplate.argTypes), + stretch: true, + sizeRange: [200, 300], +}; + +StretchedJustifiedGridTemplate.parameters = { + preview: [ + { + tab: "React", + template: convertReactTemplate(convertPath(RawApp, "react-grid", "@egjs/react-grid")), + language: "tsx", + }, + ], +}; diff --git a/packages/react-grid/stories/2-JustifiedGrid/apps/ReactJustifiedGridApp.tsx b/packages/react-grid/stories/2-JustifiedGrid/apps/ReactJustifiedGridApp.tsx index cc2cb25..39a505c 100644 --- a/packages/react-grid/stories/2-JustifiedGrid/apps/ReactJustifiedGridApp.tsx +++ b/packages/react-grid/stories/2-JustifiedGrid/apps/ReactJustifiedGridApp.tsx @@ -12,6 +12,9 @@ export default function App(props: Record) { sizeRange={props.sizeRange} isCroppedSize={props.isCroppedSize} displayedRow={props.displayedRow} + stretch={props.stretch} + stretchRange={props.stretchRange} + passUnstretchRow={props.passUnstretchRow} >
1
2
diff --git a/packages/react-grid/stories/2-JustifiedGrid/apps/ReactKeepRatioWithMaintainedTargetApp.tsx b/packages/react-grid/stories/2-JustifiedGrid/apps/ReactKeepRatioWithMaintainedTargetApp.tsx index b616b96..2d4fb58 100644 --- a/packages/react-grid/stories/2-JustifiedGrid/apps/ReactKeepRatioWithMaintainedTargetApp.tsx +++ b/packages/react-grid/stories/2-JustifiedGrid/apps/ReactKeepRatioWithMaintainedTargetApp.tsx @@ -12,6 +12,9 @@ export default function App(props: Record) { sizeRange={props.sizeRange} isCroppedSize={props.isCroppedSize} displayedRow={props.displayedRow} + stretch={props.stretch} + stretchRange={props.stretchRange} + passUnstretchRow={props.passUnstretchRow} >
) { sizeRange={props.sizeRange} isCroppedSize={props.isCroppedSize} displayedRow={props.displayedRow} + stretch={props.stretch} + stretchRange={props.stretchRange} + passUnstretchRow={props.passUnstretchRow} >
image1 diff --git a/packages/svelte-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx b/packages/svelte-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx index 1708de4..dfe4d55 100644 --- a/packages/svelte-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx +++ b/packages/svelte-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx @@ -5,3 +5,4 @@ export * from "./1-JustifiedGrid.stories"; export * from "./2-CroppedJustifiedGrid.stories"; export * from "./3-KeepRatioWithOffset.stories"; export * from "./4-KeepRatioWithMaintainedTarget.stories"; +export * from "./5-StretchedJustifiedGrid.stories"; diff --git a/packages/svelte-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts b/packages/svelte-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts new file mode 100644 index 0000000..302a274 --- /dev/null +++ b/packages/svelte-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts @@ -0,0 +1,28 @@ +import JustifiedGridApp from "./apps/SvelteKeepRatioWithMaintainedTargetApp.svelte"; +import RawJustifiedGridApp from "!!raw-loader!./apps/SvelteKeepRatioWithMaintainedTargetApp.svelte"; +import { JUSTIFIED_GRID_CONTROLS } from "../../../../stories/templates/controls"; +import { convertPath, convertSvelteTemplate, makeArgs } from "../../../../stories/utils"; +import "../../../../stories/templates/default.css"; + +export const StretchedJustifiedGridTemplate = (props) => ({ + Component: JustifiedGridApp, + props, +}); + +StretchedJustifiedGridTemplate.storyName = "Stretched Items with JustifiedGrid"; +StretchedJustifiedGridTemplate.argTypes = JUSTIFIED_GRID_CONTROLS; +StretchedJustifiedGridTemplate.args = { + ...makeArgs(StretchedJustifiedGridTemplate.argTypes), + stretch: true, + sizeRange: [200, 300], +}; + +StretchedJustifiedGridTemplate.parameters = { + preview: [ + { + tab: "Svelte", + template: convertSvelteTemplate(convertPath(RawJustifiedGridApp, "src", "@egjs/svelte-grid")), + language: "html", + }, + ], +}; diff --git a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteJustifiedGridApp.svelte b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteJustifiedGridApp.svelte index e7ba2a4..e1c1764 100644 --- a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteJustifiedGridApp.svelte +++ b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteJustifiedGridApp.svelte @@ -8,6 +8,9 @@ export let sizeRange; export let isCroppedSize; export let displayedRow; + export let stretch; + export let stretchRange; + export let passUnstretchRow;
1
2
diff --git a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithMaintainedTargetApp.svelte b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithMaintainedTargetApp.svelte index 1a0cdeb..afaf8c6 100644 --- a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithMaintainedTargetApp.svelte +++ b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithMaintainedTargetApp.svelte @@ -8,6 +8,9 @@ export let sizeRange; export let isCroppedSize; export let displayedRow; + export let stretch; + export let stretchRange; + export let passUnstretchRow;
image1 diff --git a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithOffsetApp.svelte b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithOffsetApp.svelte index cc35972..d2ce16d 100644 --- a/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithOffsetApp.svelte +++ b/packages/svelte-grid/stories/2-JustifiedGrid/apps/SvelteKeepRatioWithOffsetApp.svelte @@ -8,6 +8,9 @@ export let sizeRange; export let isCroppedSize; export let displayedRow; + export let stretch; + export let stretchRange; + export let passUnstretchRow;
image1 diff --git a/packages/vue-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx b/packages/vue-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx index 5c1de0c..6b323ea 100644 --- a/packages/vue-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx +++ b/packages/vue-grid/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx @@ -6,3 +6,4 @@ export * from "./1-JustifiedGrid.stories"; export * from "./2-CroppedJustifiedGrid.stories"; export * from "./3-KeepRatioWithOffset.stories"; export * from "./4-KeepRatioWithMaintainedTarget.stories"; +export * from "./5-StretchedJustifiedGrid.stories"; diff --git a/packages/vue-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts b/packages/vue-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts new file mode 100644 index 0000000..c37c77b --- /dev/null +++ b/packages/vue-grid/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.ts @@ -0,0 +1,28 @@ +/* eslint-disable import/no-webpack-loader-syntax */ +import App from "./apps/VueKeepRatioWithMaintainedTargetApp.vue"; +import RawApp from "!!raw-loader!./apps/VueKeepRatioWithMaintainedTargetApp.vue"; +import { JUSTIFIED_GRID_CONTROLS } from "../../../../stories/templates/controls"; +import { convertPath, convertVueTemplate, makeArgs } from "../../../../stories/utils"; +import "../../../../stories/templates/default.css"; +import { makeVueApp } from "../utils"; + +export const StretchedJustifiedGridTemplate = makeVueApp(App); + + +StretchedJustifiedGridTemplate.storyName = "Stretched Items with JustifiedGrid"; +StretchedJustifiedGridTemplate.argTypes = JUSTIFIED_GRID_CONTROLS; +StretchedJustifiedGridTemplate.args = { + ...makeArgs(StretchedJustifiedGridTemplate.argTypes), + stretch: true, + sizeRange: [200, 300], +}; + +StretchedJustifiedGridTemplate.parameters = { + preview: [ + { + tab: "Vue", + template: convertVueTemplate(convertPath(RawApp, "src", "@egjs/vue-grid")), + language: "html", + }, + ], +}; diff --git a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueJustifiedGridApp.vue b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueJustifiedGridApp.vue index 9e47d61..e10fb0e 100644 --- a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueJustifiedGridApp.vue +++ b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueJustifiedGridApp.vue @@ -8,6 +8,9 @@ v-bind:sizeRange="sizeRange" v-bind:isCroppedSize="isCroppedSize" v-bind:displayedRow="displayedRow" + v-bind:stretch="stretch" + v-bind:stretchRange="stretchRange" + v-bind:passUnstretchRow="passUnstretchRow" >
1
2
diff --git a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithMaintainedTargetApp.vue b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithMaintainedTargetApp.vue index 807f255..4f60431 100644 --- a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithMaintainedTargetApp.vue +++ b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithMaintainedTargetApp.vue @@ -8,6 +8,9 @@ v-bind:sizeRange="sizeRange" v-bind:isCroppedSize="isCroppedSize" v-bind:displayedRow="displayedRow" + v-bind:stretch="stretch" + v-bind:stretchRange="stretchRange" + v-bind:passUnstretchRow="passUnstretchRow" >
image1 @@ -124,6 +127,9 @@ body { } .image img { width: 100%; + object-fit: contain; + height: calc(100% - 40px); + background: #ddd; } .title { diff --git a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithOffsetApp.vue b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithOffsetApp.vue index d71c447..744fa82 100644 --- a/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithOffsetApp.vue +++ b/packages/vue-grid/stories/2-JustifiedGrid/apps/VueKeepRatioWithOffsetApp.vue @@ -8,6 +8,9 @@ v-bind:sizeRange="sizeRange" v-bind:isCroppedSize="isCroppedSize" v-bind:displayedRow="displayedRow" + v-bind:stretch="stretch" + v-bind:stretchRange="stretchRange" + v-bind:passUnstretchRow="passUnstretchRow" >
image1 @@ -124,6 +127,9 @@ body { } .image img { width: 100%; + object-fit: contain; + height: calc(100% - 40px); + background: #ddd; } .title { diff --git a/src/Grid.ts b/src/Grid.ts index a6204ea..48bde43 100644 --- a/src/Grid.ts +++ b/src/Grid.ts @@ -126,6 +126,9 @@ abstract class Grid extends Component * @param items - The items to set. 설정할 아이템들 */ public setItems(items: GridItem[]): this { + items.forEach((item, i) => { + item.index = i; + }); const options = this.options; if (options.useResizeObserver && options.observeChildren) { diff --git a/src/GridItem.ts b/src/GridItem.ts index daaff88..e51dc9a 100644 --- a/src/GridItem.ts +++ b/src/GridItem.ts @@ -10,6 +10,7 @@ import { MOUNT_STATE, RECT_NAMES, UPDATE_STATE } from "./consts"; * @typedef * @memberof Grid.GridItem * @property - The item key. 아이템 키. + * @property - The item index. 아이템 index. * @property - The element for the item. 아이템에 있는 엘리먼트. * @property - State of whether the element has been added to the container. element가 container에 추가되었는지 상태. * @property - The update state of the element's rect. element의 rect의 업데이트 상태. @@ -24,6 +25,7 @@ import { MOUNT_STATE, RECT_NAMES, UPDATE_STATE } from "./consts"; */ export interface GridItemStatus { key?: string | number; + index?: number; element?: HTMLElement | null; mountState?: MOUNT_STATE; updateState?: UPDATE_STATE; @@ -68,6 +70,7 @@ class GridItem { const element = itemStatus.element; const status: Required = { key: "", + index: 0, orgRect: { left: 0, top: 0, width: 0, height: 0 }, rect: { left: 0, top: 0, width: 0, height: 0 }, cssRect: {}, @@ -227,6 +230,7 @@ class GridItem { */ public getStatus(): Required { return { + index: this.index, mountState: this.mountState, updateState: this.updateState, attributes: this.attributes, diff --git a/src/ItemRenderer.ts b/src/ItemRenderer.ts index 77f94a7..8222252 100644 --- a/src/ItemRenderer.ts +++ b/src/ItemRenderer.ts @@ -192,6 +192,7 @@ export class ItemRenderer { } = RECT_NAMES[horizontal ? "horizontal" : "vertical"]; const inlineSize = this.getInlineSize(); let keys = getKeys(cssRect); + const hasRectProperties = keys.length > 0; if (useTransform) { keys = keys.filter((key) => key !== "top" && key !== "left"); @@ -212,6 +213,8 @@ export class ItemRenderer { return `${name}: ${value}px;`; })); - element.style.cssText += cssTexts.join(""); + if (hasRectProperties) { + element.style.cssText += cssTexts.join(""); + } } } diff --git a/src/ResizeWatcher.ts b/src/ResizeWatcher.ts index bc03e17..e7d39d2 100644 --- a/src/ResizeWatcher.ts +++ b/src/ResizeWatcher.ts @@ -76,9 +76,11 @@ export class ResizeWatcher { const box = this._options.childrenRectBox; children.forEach((element) => { - observer.observe(element, { - box, - }); + if (element) { + observer.observe(element, { + box, + }); + } }); } public unobserveChildren(children: Element[]) { @@ -88,7 +90,9 @@ export class ResizeWatcher { return; } children.forEach((element) => { - observer.unobserve(element); + if (element) { + observer.unobserve(element); + } }); } public listen(callback: (e: ResizeWatcherResizeEvent) => void) { diff --git a/src/grids/FrameGrid.ts b/src/grids/FrameGrid.ts index 290c1da..1b1b396 100644 --- a/src/grids/FrameGrid.ts +++ b/src/grids/FrameGrid.ts @@ -147,7 +147,6 @@ export class FrameGrid extends Grid { rects: frameRects, } = frame; const { - gap, useFrameFill, } = this.options; diff --git a/src/grids/JustifiedGrid.ts b/src/grids/JustifiedGrid.ts index 1d1680f..7251c2a 100644 --- a/src/grids/JustifiedGrid.ts +++ b/src/grids/JustifiedGrid.ts @@ -5,8 +5,8 @@ */ import Grid from "../Grid"; import { MOUNT_STATE, PROPERTY_TYPE } from "../consts"; -import { GridOptions, Properties, GridOutlines } from "../types"; -import { getRangeCost, GetterSetter, isObject } from "../utils"; +import { GridOptions, GridOutlines, Properties } from "../types"; +import { between, getRangeCost, GetterSetter, isNumber, isObject, sum, throttle } from "../utils"; import { find_path } from "./lib/dijkstra"; import { GridItem } from "../GridItem"; @@ -31,15 +31,34 @@ function splitItems(items: GridItem[], path: string[]) { } return groups; } -function getExpectedColumnSize(item: GridItem, rowSize: number) { + +function parseStretchSize(inlineSize: number, size: number | string) { + if (isNumber(size)) { + return size; + } + const signText = size.charAt(0); + const sign = signText === "+" ? 1 : (signText === "-" ? -1 : 0); + let nextSize = parseFloat(size); + + if (size.match(/%$/g)) { + nextSize *= inlineSize / 100; + } + if (sign) { + return inlineSize + nextSize; + } + return nextSize; +} + +function getExpectedItemInlineSize(item: GridItem, rowSize: number) { const inlineSize = item.orgInlineSize; const contentSize = item.orgContentSize; + const inlineOffset = item.gridData.inlineOffset || 0; + const contentOffset = item.gridData.contentOffset || 0; if (!inlineSize || !contentSize) { return rowSize; } - const inlineOffset = parseFloat(item.gridData.inlineOffset) || 0; - const contentOffset = parseFloat(item.gridData.contentOffset) || 0; + const ratio = contentSize <= contentOffset ? 1 : (inlineSize - inlineOffset) / (contentSize - contentOffset); return ratio * (rowSize - contentOffset) + inlineOffset; @@ -81,6 +100,39 @@ export interface JustifiedGridOptions extends GridOptions { * @default false */ isCroppedSize?: boolean; + /** + * The ratio is maintained except for the offset value in the inline direction. If 'data-grid-inline-offset' is set in the element of each item, it will be applied first. + * inline 방향의 offset 수치 만큼 제외하고 비율을 유지한다. 각 아이템의 element에 'data-grid-inline-offset' 을 설정하면 우선 적용한다. + * @default 0 + */ + inlineOffset?: number; + /** + * The ratio is maintained except for the offset value in the content direction. If 'data-grid-content-offset' is set in the element or JSX of each item, it will be applied first. + * content 방향의 offset 수치 만큼 제외하고 비율을 유지한다. 각 아이템의 Element 또는 JSX에 'data-grid-content-offset' 을 설정하면 우선 적용한다. + * @default 0 + */ + contentOffset?: number; + /** + * it is possible to basically break the proportion of the item and stretch the inline size to fill the container. + * If you set the `sizeRange` range narrowly, you can stretch well. + * 기본적으로 아이템의 비율을 깨서 inline size를 stretch하여 container를 꽉 채우게 가능하다. sizeRange의 범위를 좁게 설정하면 stretch가 잘 될 수 있다. + * @default false + */ + stretch?: boolean; + /** + * If `-`, `+`, or `%` are added as a string value, it is a relative value to the original size. If it is a number value, the stretch range can be set as an absolute value. + * If `data-grid-min-stretch` and `data-grid-max-stretch` are set in the Element or JSX of each item, they will be applied first. + * string 값으로 `-`, `+`, `%`이 붙으면 원본 크기에 대한 상대값이며 number 값으로 들어오면 절대 값으로 stretch 범위를 설정할 수 있습니다. + * 각 아이템의 Element 또는 JSX에 `data-grid-min-stretch`, `data-grid-max-stretch`을 설정하면 우선 적용한다. + * @ + * @default ["-10%", "+10%"] + */ + stretchRange?: Array; + /** + * Items placed in the last row are not stretched and are drawn maintaining their proportions. When using InfiniteGrid, it is calculated and re-rendered as follows: + * 마지막 row에 배치되는 아이템들 경우 stretch되지 않고 비율유지한채로 그려진다. InfiniteGrid를 사용하는 경우 다음 그룹과 같이 계산되어 재렌더링한다. + */ + passUnstretchRow?: boolean; } /** @@ -103,6 +155,13 @@ export class JustifiedGrid extends Grid { sizeRange: PROPERTY_TYPE.RENDER_PROPERTY, isCroppedSize: PROPERTY_TYPE.RENDER_PROPERTY, displayedRow: PROPERTY_TYPE.RENDER_PROPERTY, + stretch: PROPERTY_TYPE.RENDER_PROPERTY, + stretchRange: PROPERTY_TYPE.RENDER_PROPERTY, + passUnstretchRow: PROPERTY_TYPE.RENDER_PROPERTY, + inlineMargin: PROPERTY_TYPE.RENDER_PROPERTY, + contentMargin: PROPERTY_TYPE.RENDER_PROPERTY, + inlineOffset: PROPERTY_TYPE.RENDER_PROPERTY, + contentOffset: PROPERTY_TYPE.RENDER_PROPERTY, }; public static defaultOptions: Required = { ...Grid.defaultOptions, @@ -111,6 +170,11 @@ export class JustifiedGrid extends Grid { sizeRange: [0, Infinity], displayedRow: -1, isCroppedSize: false, + stretch: false, + passUnstretchRow: true, + stretchRange: ["-20%", "+20%"], + inlineOffset: 0, + contentOffset: 0, }; public applyGrid(items: GridItem[], direction: "start" | "end", outline: number[]): GridOutlines { const { @@ -125,14 +189,25 @@ export class JustifiedGrid extends Grid { const element = item.element; const attributes = item.attributes; const gridData = item.gridData; - let inlineOffset = parseFloat(attributes.inlineOffset) || gridData.inlineOffset || 0; - let contentOffset = parseFloat(attributes.contentOffset) || gridData.contentOffset | 0; + let inlineOffset = parseFloat(attributes.inlineOffset); + let contentOffset = parseFloat(attributes.contentOffset); + // let contentMargin = parseFloat(attributes.contentMargin); + + if (isNaN(inlineOffset)) { + inlineOffset = this.inlineOffset || gridData.inlineOffset || 0; + } + if (isNaN(contentOffset)) { + contentOffset = this.contentOffset || gridData.contentOffset | 0; + } + // if (isNaN(contentMargin)) { + // contentMargin = this.contentMargin || gridData.contentMargin | 0; + // } if ( element && !("inlineOffset" in attributes) && !("contentOffset" in attributes) && item.mountState === MOUNT_STATE.MOUNTED ) { - const maintainedTarget = element.querySelector(`[${attributePrefix}maintained-target]`); + const maintainedTarget = element.querySelector(`[${attributePrefix}maintained-target]`); if (maintainedTarget) { const widthOffset = element.offsetWidth - element.clientWidth @@ -151,17 +226,20 @@ export class JustifiedGrid extends Grid { } gridData.inlineOffset = inlineOffset; gridData.contentOffset = contentOffset; + // gridData.contentMargin = contentMargin; }); const rowRange = this.options.rowRange; let path: string[] = []; + const isEndDirection = direction === "end"; + if (items.length) { - path = rowRange ? this._getRowPath(items) : this._getPath(items); + path = rowRange ? this._getRowPath(items, isEndDirection) : this._getPath(items, isEndDirection); } return this._setStyle(items, path, outline, direction === "end"); } - private _getRowPath(items: GridItem[]) { + private _getRowPath(items: GridItem[], isEndDirection: boolean) { const columnRange = this._getColumnRange(); const rowRange = this._getRowRange(); @@ -170,7 +248,7 @@ export class JustifiedGrid extends Grid { cost: 0, length: 0, currentNode: 0, - }, columnRange, rowRange); + }, columnRange, rowRange, isEndDirection); return pathLink?.path.map((node) => `${node}`) ?? []; } @@ -178,7 +256,8 @@ export class JustifiedGrid extends Grid { items: GridItem[], currentLink: Link, columnRange: number[], - rowRange: number[] + rowRange: number[], + isEndDirection: boolean, ): Link { const [minColumn] = columnRange; const [minRow, maxRow] = rowRange; @@ -193,7 +272,7 @@ export class JustifiedGrid extends Grid { // not reached lastNode but path is exceed or the number of remaining nodes is less than minColumn. if (currentNode < lastNode && (maxRow <= pathLength || currentNode + minColumn > lastNode)) { const rangeCost = getRangeCost(lastNode - currentNode, columnRange); - const lastCost = rangeCost * Math.abs(this._getCost(items, currentNode, lastNode)); + const lastCost = rangeCost * Math.abs(this._getCost(items, currentNode, lastNode, isEndDirection)); return { ...currentLink, @@ -210,7 +289,7 @@ export class JustifiedGrid extends Grid { isOver: minRow > pathLength || maxRow < pathLength, }; } else { - return this._searchRowLink(items, currentLink, lastNode, columnRange, rowRange); + return this._searchRowLink(items, currentLink, lastNode, columnRange, rowRange, isEndDirection); } } @@ -219,7 +298,8 @@ export class JustifiedGrid extends Grid { currentLink: Link, lastNode: number, columnRange: number[], - rowRange: number[] + rowRange: number[], + isEndDirection: boolean, ) { const [minColumn, maxColumn] = columnRange; const { @@ -235,13 +315,13 @@ export class JustifiedGrid extends Grid { if (nextNode === currentNode) { continue; } - const nextCost = Math.abs(this._getCost(items, currentNode, nextNode)); + const nextCost = Math.abs(this._getCost(items, currentNode, nextNode, isEndDirection)); const nextLink = this._getRowLink(items, { path: [...path, nextNode], length: pathLength + 1, cost: cost + nextCost, currentNode: nextNode, - }, columnRange, rowRange); + }, columnRange, rowRange, isEndDirection); if (nextLink) { links.push(nextLink); @@ -264,9 +344,9 @@ export class JustifiedGrid extends Grid { // It returns the lowest cost link. return links[0]; } - private _getExpectedRowSize(items: GridItem[]) { - const inlineGap = this.getInlineGap(); - let containerInlineSize = this.getContainerInlineSize()! - inlineGap * (items.length - 1); + private _getExpectedRowSize(items: GridItem[], forceStretch?: boolean) { + const containerInlineSize = this.getContainerInlineSize()! - this.getInlineGap() * (items.length - 1); + let fixedContainerInsize = containerInlineSize; let ratioSum = 0; let inlineSum = 0; @@ -279,33 +359,190 @@ export class JustifiedGrid extends Grid { return; } // sum((expect - offset) * ratio) = container inline size - const inlineOffset = parseFloat(item.gridData.inlineOffset) || 0; - const contentOffset = parseFloat(item.gridData.contentOffset) || 0; + const inlineOffset = item.gridData.inlineOffset || 0; + const contentOffset = item.gridData.contentOffset || 0; + // const contentMargin = item.gridData.contentMargin || 0; + const maintainedRatio = contentSize <= contentOffset ? 1 : (inlineSize - inlineOffset) / (contentSize - contentOffset); ratioSum += maintainedRatio; + // inlineSum += (contentOffset + contentMargin) * maintainedRatio; inlineSum += contentOffset * maintainedRatio; - containerInlineSize -= inlineOffset; + fixedContainerInsize -= inlineOffset; }); - return ratioSum ? (containerInlineSize + inlineSum) / ratioSum : 0; + if (ratioSum) { + const nextRowSize = (fixedContainerInsize + inlineSum) / ratioSum; + + if (this.stretch) { + const [minRowSize, maxRowSize] = this._getSizeRange(); + const stretchRowSize = between(nextRowSize, minRowSize, maxRowSize); + + if (forceStretch) { + return stretchRowSize; + } + const stretchRange = this.stretchRange; + const inlineSizes = items.map((item) => { + return getExpectedItemInlineSize(item, stretchRowSize); + }); + const minInlineSize = inlineSizes.reduce((prev, itemInlineSize, i) => { + return prev + parseStretchSize(itemInlineSize, items[i].attributes.minStretch || stretchRange[0]); + }, 0); + const maxInlineSize = inlineSizes.reduce((prev, itemInlineSize, i) => { + return prev + parseStretchSize(itemInlineSize, items[i].attributes.maxStretch || stretchRange[1]); + }, 0); + + // for stretch + if (minInlineSize <= containerInlineSize && containerInlineSize <= maxInlineSize) { + return stretchRowSize; + } + } + + return nextRowSize; + } + return 0; + } + + private _getExpectedInlineSizes(items: GridItem[], rowSize: number) { + const { + stretch, + stretchRange, + } = this.options; + return items.map((item) => { + const minInlineSize = stretch + ? parseStretchSize(item.orgInlineSize, item.attributes.minStretch || stretchRange[0]) + : -Infinity; + const maxInlineSize = stretch + ? parseStretchSize(item.orgInlineSize, item.attributes.maxStretch || stretchRange[1]) + : Infinity; + + const itemInlineSize = getExpectedItemInlineSize(item, rowSize); + let isMax = false; + let isMin = false; + if (itemInlineSize >= maxInlineSize) { + isMax = true; + } else if (itemInlineSize <= minInlineSize) { + isMin = true; + } + + return { + minSize: minInlineSize, + maxSize: maxInlineSize, + size: between(itemInlineSize, minInlineSize, maxInlineSize), + originalSize: itemInlineSize, + isMax, + isMin, + }; + }); + } + private _getStretchItemInfos(items: GridItem[], rowSize: number) { + const itemsLength = items.length; + const containerInlineSize = this.getContainerInlineSize() - this.getInlineGap() * (Math.max(1, itemsLength) - 1); + const itemInfos = this._getExpectedInlineSizes(items, rowSize); + const firstItemsSize = sum(itemInfos.map((info) => info.size)); + const distSize = containerInlineSize - firstItemsSize; + const firstScale = containerInlineSize / sum(itemInfos.map((info) => info.originalSize)); + const costInfos = itemInfos.map((info) => { + return { + ...info, + passed: false, + size: info.originalSize * firstScale, + }; + }); + + if (distSize === 0) { + return { + infos: costInfos, + cost: 0, + }; + } + // increase + const isIncrease = distSize > 0; + const costInfosLength = costInfos.length; + + for (let i = 0; i < costInfosLength; ++i) { + const passedItemsSize = sum(costInfos.map((info) => info.passed ? info.size : 0)); + const restItemsSize = sum(costInfos.map((info) => info.passed ? 0 : info.originalSize)); + let distScale = (containerInlineSize - passedItemsSize) / restItemsSize; + // minimize or maximize + costInfos.forEach((info) => { + if (info.passed) { + return; + } + + if (isIncrease) { + if (info.size > info.maxSize) { + distScale = Math.min(distScale, info.maxSize / info.originalSize); + } + } else { + if (info.size < info.minSize) { + distScale = Math.max(distScale, info.minSize / info.originalSize); + } + } + }); + + costInfos.forEach((info) => { + if (!info.passed) { + info.size = between(info.originalSize * distScale, info.minSize, info.maxSize); + + if ( + (isIncrease && !throttle(info.size - info.maxSize, 0.001)) + || (!isIncrease && !throttle(info.size - info.minSize, 0.001)) + ) { + info.passed = true; + } + } + }); + + if (costInfos.every((info) => info.passed)) { + break; + } + } + const lastDistScale = containerInlineSize / sum(costInfos.map((info) => info.size)); + + // last + if (throttle(lastDistScale - 1, 0.001)) { + costInfos.forEach((info) => { + info.size *= lastDistScale; + }); + } + + + return { + infos: costInfos, + cost: sum(costInfos.map((info) => { + let costRatio = 1; + + if (info.size > info.maxSize || info.size < info.minSize) { + costRatio = 2; + } + let originalSize = info.originalSize; + + if (isIncrease) { + originalSize = Math.max(originalSize, info.minSize); + } else { + originalSize = Math.min(originalSize, info.maxSize); + } + return Math.abs(info.size - originalSize) * costRatio; + })), + }; } private _getExpectedInlineSize(items: GridItem[], rowSize: number) { const inlineGap = this.getInlineGap(); - const size = items.reduce((sum, item) => { - return sum + getExpectedColumnSize(item, rowSize); - }, 0); + const itemInfos = this._getExpectedInlineSizes(items, rowSize); - return size ? size + inlineGap * (items.length - 1) : 0; + return itemInfos.length ? sum(itemInfos.map((info) => info.size)) + inlineGap * (items.length - 1) : 0; } private _getCost( items: GridItem[], i: number, j: number, + isEndDirection: boolean, ) { const lineItems = items.slice(i, j); - const rowSize = this._getExpectedRowSize(lineItems); + const containerInlineSize = this.getContainerInlineSize(); + let rowSize = this._getExpectedRowSize(lineItems); const [minSize, maxSize] = this._getSizeRange(); if (this.isCroppedSize) { @@ -317,23 +554,50 @@ export class JustifiedGrid extends Grid { rowSize < minSize ? minSize : maxSize, ); - return Math.pow(expectedInlineSize - this.getContainerInlineSize(), 2); + return Math.pow(expectedInlineSize - containerInlineSize, 2); + } + let extraCost = 0; + + if (this.stretch) { + if (rowSize < minSize) { + rowSize = minSize; + } else if (rowSize > maxSize) { + rowSize = maxSize; + } + const sizeCost = Math.abs(rowSize - minSize); + + const expectedInlineSize = this._getExpectedInlineSize( + lineItems, + rowSize, + ); + + if ( + !this.passUnstretchRow + || (isEndDirection ? j !== items.length : i !== 0) + || expectedInlineSize >= containerInlineSize + ) { + const res = this._getStretchItemInfos(lineItems, rowSize); + + extraCost = res.cost; + } + + return extraCost + sizeCost; } if (isFinite(maxSize)) { // if this size is not in range, the cost increases sharply. if (rowSize < minSize) { - return Math.pow(rowSize - minSize, 2) + Math.pow(maxSize, 2); + return Math.pow(rowSize - minSize, 2) + Math.pow(maxSize, 2) + extraCost; } else if (rowSize > maxSize) { - return Math.pow(rowSize - maxSize, 2) + Math.pow(maxSize, 2); + return Math.pow(rowSize - maxSize, 2) + Math.pow(maxSize, 2) + extraCost; } } else if (rowSize < minSize) { - return Math.max(Math.pow(minSize, 2), Math.pow(rowSize, 2)) + Math.pow(maxSize, 2); + return Math.max(Math.pow(minSize, 2), Math.pow(rowSize, 2)) + Math.pow(maxSize, 2) + extraCost; } // if this size in range, the cost is row - return rowSize - minSize; + return rowSize - minSize + extraCost; } - private _getPath(items: GridItem[]) { + private _getPath(items: GridItem[], isEndDirection: boolean) { const lastNode = items.length; const columnRangeOption = this.options.columnRange; const [minColumn, maxColumn]: number[] = isObject(columnRangeOption) @@ -352,6 +616,7 @@ export class JustifiedGrid extends Grid { items, currentNode, nextNode, + isEndDirection, ); if (cost < 0 && nextNode === lastNode) { @@ -373,42 +638,103 @@ export class JustifiedGrid extends Grid { const { isCroppedSize, displayedRow, + stretch, + passUnstretchRow, } = this.options; + const itemsLength = items.length; const sizeRange = this._getSizeRange(); const startPoint = outline[0] || 0; const containerInlineSize = this.getContainerInlineSize(); const inlineGap = this.getInlineGap(); const contentGap = this.getContentGap(); const groups = splitItems(items, path); + let passedItems!: number[]; + const groupsLength = groups.length; let contentPos = startPoint; let displayedSize = 0; + let passedPoint!: number[]; groups.forEach((groupItems, rowIndex) => { - const length = groupItems.length; - let rowSize = this._getExpectedRowSize(groupItems); + const groupItemslength = groupItems.length; + let rowSize = this._getExpectedRowSize(groupItems, true); + if (isCroppedSize) { rowSize = Math.max(sizeRange[0], Math.min(rowSize, sizeRange[1])); } - const expectedInlineSize = this._getExpectedInlineSize(groupItems, rowSize); - const allGap = inlineGap * (length - 1); + const itemInfos = groupItems.map((item, index) => { + const itemInlineSize = getExpectedItemInlineSize(item, rowSize); + + return { + index, + item, + inlineSize: itemInlineSize, + orgInlineSize: itemInlineSize, + maxInlineSize: itemInlineSize, + minInlineSize: itemInlineSize, + }; + }); + const expectedInlineSize = this._getExpectedInlineSize(groupItems, rowSize); const scale = (containerInlineSize - allGap) / (expectedInlineSize - allGap); + const noGapExpectedContainerInlineSize = expectedInlineSize - allGap; + const noGapContainerInlineSize = containerInlineSize - allGap; + + if (stretch && expectedInlineSize && noGapContainerInlineSize !== noGapExpectedContainerInlineSize) { + // passed이고 마지막 그룹의 경우 stretchSize가 containerSize보다 작으면 pass! + if ( + passUnstretchRow && noGapExpectedContainerInlineSize < noGapContainerInlineSize + && (isEndDirection ? rowIndex === groupsLength - 1 : rowIndex === 0) + ) { + passedPoint = [contentPos]; + passedItems = groupItems.map((_, i) => itemsLength - groupItemslength + i); + + const inlineSizes = this._getExpectedInlineSizes(groupItems, rowSize); + + itemInfos.forEach((info, i) => { + info.minInlineSize = inlineSizes[i].minSize; + info.maxInlineSize = inlineSizes[i].maxSize; + info.inlineSize = between(info.inlineSize, info.minInlineSize, info.maxInlineSize); + + }); + } else { + const { infos } = this._getStretchItemInfos(groupItems, rowSize); + + itemInfos.forEach((info, i) => { + info.inlineSize = infos[i].size; + info.minInlineSize = infos[i].minSize; + info.maxInlineSize = infos[i].maxSize; + }); + } + } - groupItems.forEach((item, i) => { - let columnSize = getExpectedColumnSize(item, rowSize); + itemInfos.forEach((info, i) => { + const { + item, + inlineSize, + } = info; + let nextInlineSize = inlineSize; const prevItem = groupItems[i - 1]; const inlinePos = prevItem ? prevItem.cssInlinePos! + prevItem.cssInlineSize! + inlineGap : 0; if (isCroppedSize) { - columnSize *= scale; + nextInlineSize *= scale; } + + + const gridData = item.gridData; + + gridData.orgInlineSize = info.orgInlineSize; + gridData.orgContentSize = rowSize; + gridData.minInlineSize = info.minInlineSize; + gridData.maxInlineSize = info.maxInlineSize; + item.setCSSGridRect({ inlinePos, contentPos, - inlineSize: columnSize, + inlineSize: nextInlineSize, contentSize: rowSize, }); }); @@ -423,6 +749,8 @@ export class JustifiedGrid extends Grid { return { start: [startPoint], end: [displayedSize], + passedItems, + passed: passedPoint, }; } // always start is lower than end. @@ -433,6 +761,8 @@ export class JustifiedGrid extends Grid { item.cssContentPos! -= height; }); return { + passedItems, + passed: passedPoint ? [passedPoint[0] - height] : null, start: [startPoint - height], end: [startPoint], // endPoint - height = startPoint }; diff --git a/src/types.ts b/src/types.ts index 4164eb7..c113eab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -143,6 +143,7 @@ export interface GridOptions { externalContainerManager?: ContainerManager | null; } + /** * @typedef * @memberof Grid @@ -150,7 +151,11 @@ export interface GridOptions { export interface GridOutlines { start: number[]; end: number[]; + passed?: number[] | null; + passedItems?: number[] | null; } + + /** * @typedef * @memberof Grid diff --git a/src/utils.ts b/src/utils.ts index 311c97a..e10dfc5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,6 +45,9 @@ export function isNumber(val: any): val is number { export function camelize(str: string) { return str.replace(/[\s-_]([a-z])/g, (all, letter) => letter.toUpperCase()); } +export function sum(arr: number[]) { + return arr.reduce((a, b) => a + b, 0); +} export function getDataAttributes(element: HTMLElement, attributePrefix: string) { const dataAttributes: Record = {}; @@ -135,6 +138,18 @@ export function getRangeCost(value: number, valueRange: number[]) { return Math.max(value - valueRange[1], valueRange[0] - value, 0) + 1; } +export function between(value: number, min: number, max: number) { + return Math.min(max, Math.max(value, min)); +} + +export function throttle(num: number, unit?: number) { + if (!unit) { + return num; + } + const reverseUnit = 1 / unit; + return Math.round(num / unit) / reverseUnit; +} + /** * Decorator that makes the method of grid available in the framework. * @ko 프레임워크에서 그리드의 메소드를 사용할 수 있게 하는 데코레이터. diff --git a/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx b/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx index c057df1..f27e8c1 100644 --- a/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx +++ b/stories/2-JustifiedGrid/0-JustifiedGrid.stories.tsx @@ -7,3 +7,4 @@ export * from "./1-JustifiedGrid.stories"; export * from "./2-CroppedJustifiedGrid.stories"; export * from "./3-KeepRatioWithOffset.stories"; export * from "./4-KeepRatioWithMaintainedTarget.stories"; +export * from "./5-StretchedJustifiedGrid.stories"; diff --git a/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.tsx b/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.tsx new file mode 100644 index 0000000..5e52b16 --- /dev/null +++ b/stories/2-JustifiedGrid/5-StretchedJustifiedGrid.stories.tsx @@ -0,0 +1,80 @@ +/* eslint-disable import/no-webpack-loader-syntax */ +import * as React from "react"; +import { makeArgs } from "../utils"; +import JustifiedGridApp from "./apps/VanillaJustifiedGridApp"; +import { JUSTIFIED_GRID_CONTROLS } from "../templates/controls"; +import { JustifiedGrid } from "../../src"; +import { getApp } from "../templates/ReactJSX"; +import "../templates/default.css"; +import { getPreview } from "../templates/preview"; + +export const StretchedJustifiedGridTemplate = getApp(JustifiedGrid, JustifiedGridApp, () => { + return
+
+ image1 +
Item 1
+
+
+ image2 +
Item 2
+
+
+ image3 +
Item 3
+
+
+ image4 +
Item 4
+
+
+ image5 +
Item 5
+
+
+ image6 +
Item 6
+
+
+ image7 +
Item 7
+
+
+ image8 +
Item 8
+
+
+ image9 +
Item 9
+
+
+ image10 +
Item 10
+
+
; +}); + + +StretchedJustifiedGridTemplate.storyName = "Stretched Items with JustifiedGrid"; +StretchedJustifiedGridTemplate.argTypes = JUSTIFIED_GRID_CONTROLS; +StretchedJustifiedGridTemplate.args = { + ...makeArgs(StretchedJustifiedGridTemplate.argTypes), + stretch: true, + sizeRange: [200, 300], +}; + +StretchedJustifiedGridTemplate.parameters = { + preview: getPreview("2-JustifiedGrid", "KeepRatioWithMaintainedTarget", { + vanillaCode: require("!!raw-loader!./apps/VanillaJustifiedGridApp.tsx").default, + htmlCode: require("!!raw-loader!./templates/VanillaKeepRatioWithMaintainedTarget.html").default, + }), +}; diff --git a/stories/2-JustifiedGrid/apps/VanillaJustifiedGridApp.tsx b/stories/2-JustifiedGrid/apps/VanillaJustifiedGridApp.tsx index 4df3171..accbeef 100644 --- a/stories/2-JustifiedGrid/apps/VanillaJustifiedGridApp.tsx +++ b/stories/2-JustifiedGrid/apps/VanillaJustifiedGridApp.tsx @@ -9,6 +9,9 @@ export default function App(props: Record) { sizeRange: props.sizeRange, isCroppedSize: props.isCroppedSize, displayedRow: props.displayedRow, + stretch: props.stretch, + stretchRange: props.stretchRange, + passUnstretchRow: props.passUnstretchRow, }); grid.renderItems(); diff --git a/stories/templates/controls.ts b/stories/templates/controls.ts index 63a73b7..68bdd9d 100644 --- a/stories/templates/controls.ts +++ b/stories/templates/controls.ts @@ -71,6 +71,21 @@ export const JUSTIFIED_GRID_CONTROLS = { description: makeLink("JustifiedGrid", "displayedRow"), defaultValue: -1, }), + stretch: makeArgType({ + type: "boolean", + description: makeLink("JustifiedGrid", "stretch"), + defaultValue: false, + }), + stretchRange: makeArgType({ + type: "object", + description: makeLink("JustifiedGrid", "stretchRange"), + defaultValue: [144, 320], + }), + passUnstretchRow: makeArgType({ + type: "boolean", + description: makeLink("JustifiedGrid", "passUnstretchRow"), + defaultValue: true, + }), }; export const FRAME_GRID_CONTROLS = { diff --git a/stories/templates/default.css b/stories/templates/default.css index e6e24e0..4abeeb5 100644 --- a/stories/templates/default.css +++ b/stories/templates/default.css @@ -77,6 +77,9 @@ html, body { .image img { width: 100%; + object-fit: contain; + height: calc(100% - 40px); + background: #ddd; } .title { diff --git a/test/e2e/manual/index.html b/test/e2e/manual/index.html index 4b7e0d6..9aff8a3 100644 --- a/test/e2e/manual/index.html +++ b/test/e2e/manual/index.html @@ -80,6 +80,9 @@ .image img { width: 100%; + object-fit: contain; + height: calc(100% - 40px); + background: #ddd; } .title { diff --git a/test/manual/justifiedgrid-stretch.html b/test/manual/justifiedgrid-stretch.html new file mode 100644 index 0000000..d81b88f --- /dev/null +++ b/test/manual/justifiedgrid-stretch.html @@ -0,0 +1,117 @@ + +
+ +
+ + diff --git a/test/manual/justifiedinfinitegrid-stretch.html b/test/manual/justifiedinfinitegrid-stretch.html new file mode 100644 index 0000000..04f2fbc --- /dev/null +++ b/test/manual/justifiedinfinitegrid-stretch.html @@ -0,0 +1,125 @@ +

+ JustifiedInfiniteGrid +

+
+
+
+
+ + + + + diff --git a/test/unit/JustifiedGrid.spec.ts b/test/unit/JustifiedGrid.spec.ts index 15eff0b..b4ed1aa 100644 --- a/test/unit/JustifiedGrid.spec.ts +++ b/test/unit/JustifiedGrid.spec.ts @@ -1,5 +1,6 @@ import { GridItem } from "../../src"; import { JustifiedGrid } from "../../src/grids/JustifiedGrid"; +import { SIZES } from "./utils/consts"; import { appendElements, cleanup, expectItemsPosition, getRowCount, getRowPoses, sandbox, waitEvent, @@ -505,4 +506,187 @@ describe("test JustifiedGrid", () => { }); }); }); + describe("test stretch option", () => { + it(`should check whether cost is reflected in stretch (no stretch)`, async () => { + // Given + container!.style.cssText = "width: 1000px;"; + + grid = new JustifiedGrid(container!, { + gap: 5, + horizontal: false, + sizeRange: [150, 300], + stretch: true, + stretchRange: [144, 320], + passUnstretchRow: false, + }); + + appendElements(container!, 5); + + // When + grid.renderItems(); + + await waitEvent(grid, "renderComplete"); + + const items = grid.getItems(); + const rowCount = getRowCount(items); + // [518, 517], + // [550, 825], + // [640, 640], + // [364, 520], + // [710, 1020], + // [600, 819], + // [486, 729], + // [544, 784], + // [720, 720], + // [381, 555], + // [521, 775], + + // Then + expect(rowCount).to.be.equal(1); + + // NO STRETCH + items.forEach((item, i) => { + expect(item.cssInlineSize).to.be.closeTo(SIZES[i][0] / SIZES[i][1] * 241.1, 0.1); + expect(item.cssContentSize).to.be.closeTo(241.1, 0.1); + }); + }); + it(`should check whether cost is reflected in stretch (stretch)`, async () => { + // Given + container!.style.cssText = "width: 1000px;"; + + grid = new JustifiedGrid(container!, { + gap: 0, + horizontal: false, + sizeRange: [150, 220], + stretch: true, + stretchRange: [144, 300], + passUnstretchRow: false, + }); + + appendElements(container!, 5); + + // When + grid.renderItems(); + + await waitEvent(grid, "renderComplete"); + + const items = grid.getItems(); + const rowCount = getRowCount(items); + + // Then + expect(rowCount).to.be.equal(1); + + // NO STRETCH + const expectedHeight = 220; + const sum = SIZES.slice(0, 5).reduce((v1, v2) => v1 + v2[0] / v2[1] * expectedHeight, 0); + const scale = grid.getContainerInlineSize() / sum; + + items.forEach((item, i) => { + expect(item.cssContentSize).to.be.closeTo(expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.not.closeTo(SIZES[i][0] / SIZES[i][1] * expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.closeTo(SIZES[i][0] / SIZES[i][1] * scale * expectedHeight, 0.1, `expect ${i}`); + }); + }); + it(`should check whether cost is reflected in stretch (stretch, 2line)`, async () => { + // Given + container!.style.cssText = "width: 1000px;"; + + grid = new JustifiedGrid(container!, { + gap: 0, + horizontal: false, + sizeRange: [220, 220], + stretch: true, + stretchRange: [144, 300], + passUnstretchRow: false, + }); + + appendElements(container!, 8); + + // When + grid.renderItems(); + + await waitEvent(grid, "renderComplete"); + + + const items = grid.getItems(); + const rowCount = getRowCount(items); + + // Then + expect(rowCount).to.be.equal(2); + + // NO STRETCH + const expectedHeight = 220; + + + // 0 4 + // 4 8 + const scale1 = grid.getContainerInlineSize() + / SIZES.slice(0, 4).reduce((v1, v2) => v1 + v2[0] / v2[1] * expectedHeight, 0); + const scale2 = grid.getContainerInlineSize() + / SIZES.slice(4, 8).reduce((v1, v2) => v1 + v2[0] / v2[1] * expectedHeight, 0); + + items.slice(0, 4).forEach((item, i) => { + expect(item.cssContentSize).to.be.closeTo(expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.not.closeTo(SIZES[i][0] / SIZES[i][1] * expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.closeTo(SIZES[i][0] / SIZES[i][1] * scale1 * expectedHeight, 0.1, `expect ${i}`); + }); + items.slice(4, 8).forEach((item, i) => { + const size = SIZES[i + 4]; + expect(item.cssContentSize).to.be.closeTo(expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.not.closeTo(size[0] / size[1] * expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.closeTo(size[0] / size[1] * scale2 * expectedHeight, 0.1, `expect ${i + 4}`); + }); + }); + it(`should check whether cost is reflected in stretch (stretch, 2line, pass last line)`, async () => { + // Given + container!.style.cssText = "width: 1000px;"; + + grid = new JustifiedGrid(container!, { + gap: 0, + horizontal: false, + sizeRange: [220, 220], + stretch: true, + stretchRange: [130, 300], + passUnstretchRow: true, + }); + + appendElements(container!, 8); + + // When + grid.renderItems(); + + await waitEvent(grid, "renderComplete"); + + const passedItems = grid.getOutlines().passedItems; + const items = grid.getItems(); + const rowCount = getRowCount(items); + + // Then + + expect(passedItems?.length).to.be.equal(2); + expect(rowCount).to.be.equal(2); + + // NO STRETCH + const expectedHeight = 220; + + // 0 4 + // 4 8 + const scale1 = grid.getContainerInlineSize() + / SIZES.slice(0, 6).reduce((v1, v2) => v1 + v2[0] / v2[1] * expectedHeight, 0); + + items.slice(0, 6).forEach((item, i) => { + const size = SIZES[i]; + expect(item.cssContentPos).to.be.equals(0); + expect(item.cssContentSize).to.be.closeTo(expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.not.closeTo(size[0] / size[1] * expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.closeTo(size[0] / size[1] * scale1 * expectedHeight, 0.1, `expect ${i}`); + }); + items.slice(6, 8).forEach((item, i) => { + const size = SIZES[i + 4]; + + expect(item.cssContentSize).to.be.closeTo(expectedHeight, 0.1); + expect(item.cssInlineSize).to.be.not.closeTo(size[0] / size[1] * expectedHeight, 0.1); + }); + }); + }); });