@thinkpixellab-public/px-vue 3.0.4 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,7 @@ export default {
18
18
  name: 'px-aspect',
19
19
  mixins: [PxBaseResize],
20
20
  props: {
21
- // the aspect ration (can be express as a number or a ration string like '16:9')
21
+ // the aspect ration (can be express as a number or a ratio string like '16:9')
22
22
  aspect: { default: 16 / 9 },
23
23
 
24
24
  // the horizontal positioning when there is leftover space
@@ -2,8 +2,8 @@
2
2
 
3
3
  The grid will be created to contain cells (like a spreadsheet). A space represents an area that
4
4
  spans a certain number of cells and can contain a certain number of cards. Keeping all of this
5
- straight can get a little confusing because the terms are highly related but different, so here
6
- is the breakdown in some detail:
5
+ straight can get a little confusing because the terms are highly related but different, so here is
6
+ the breakdown in some detail:
7
7
 
8
8
  ## Cell
9
9
  A single unit in the grid that spans 1 row and 1 column.
@@ -16,6 +16,11 @@ spaces can only span columns (not rows). We support three kinds of spaces:
16
16
  - **stacked** space ('==') This space is also (1x1) but contains two cards stacked vertically.
17
17
  - **wide** space ('-----') This space is 2x1 cells and contains one card.
18
18
 
19
+ You might be wondering why we don't just make space and cell the same thing and the largest cards
20
+ would be 2x2 instead of 1x2. It has to do with the way we're using css grid for layout. Things are a
21
+ lot cleaner if our max card height is a single row in the grid. It also makes it so that the
22
+ notation matches the rows array which keeps things easier to understand.`
23
+
19
24
  ## Card
20
25
  Cards are areas within a space. Default and wide spaces contain a single card. Stacked spaces
21
26
  contain two cards.
@@ -50,6 +55,48 @@ You can define a template using space notation like this:
50
55
  <template #default="{ item }">{{ item.name }}</template>
51
56
  </px-card-grid>
52
57
 
58
+
59
+ ## Custom Item Selection
60
+
61
+ By default items will be placed in the template in sequential order but it's possible to take
62
+ full control of which item is placed in which slot but providing a callback with the getItemFn
63
+ property. This function will receive an object with parameters describing the space and expects
64
+ an item to be returned. The function will be called once for the number of spaces in the layout
65
+ (include for orphans).
66
+
67
+ When using this method it isn't even necessary to set the items property but you also need to
68
+ provide the getItemCountFn callback. That should be the number of items without orphans (you will
69
+ still get the callback for orphans).
70
+
71
+ <px-card-grid
72
+ :gap="30"
73
+ :maxColumns="4"
74
+ :debug="true",
75
+ :templates="{
76
+ 4: ['== -- -----', '-- == -- --', '----- -- ==', '-- -- == --'],
77
+ 3: ['== -- --', '-- -- ==', '-- == --'],
78
+ 2: ['-- --', '-----', '== --'],
79
+ }"
80
+ :get-item-fn="getItem"
81
+ :get-item-count="getItemCount"
82
+ >
83
+ <template #default="{ item }">{{ item.name }}</template>
84
+ </px-card-grid>
85
+
86
+ getItem(space) {
87
+ if (space.type == 'stacked') {
88
+ ...
89
+ }
90
+ else {
91
+ ...
92
+ }
93
+ }
94
+
95
+ getItemCount() {
96
+ return ...;
97
+ }
98
+
99
+
53
100
  -->
54
101
 
55
102
  <template>
@@ -131,6 +178,13 @@ const spaceTemplateToSpaceRows = rows => {
131
178
  return spaces;
132
179
  };
133
180
 
181
+ const cardCountInRowTemplate = rowTemplate => {
182
+ let count = rowTemplate.reduce((total, space) => {
183
+ return total + space.cards;
184
+ }, 0);
185
+ return count;
186
+ };
187
+
134
188
  export default {
135
189
  name: 'px-card-grid',
136
190
  // components: { Component },
@@ -146,6 +200,8 @@ export default {
146
200
  cardAspect: { default: 3 / 4 },
147
201
  gap: { type: Number, default: 24 },
148
202
  debug: { type: Boolean, default: false },
203
+ getItemCountFn: { default: null },
204
+ getItemFn: { default: null },
149
205
  },
150
206
 
151
207
  data() {
@@ -175,7 +231,9 @@ export default {
175
231
  // Returns an array of rows, each of which contains an array of spaces each
176
232
  // of which contains 1 or more items that can be used to create cards.
177
233
 
178
- if (!this.rowTemplates || !this.items || !this.items.length || this.columnCount == 0) {
234
+ let itemCount = this.getItemCount();
235
+
236
+ if (!this.rowTemplates || !itemCount || this.columnCount == 0) {
179
237
  return [];
180
238
  }
181
239
 
@@ -191,39 +249,58 @@ export default {
191
249
  let rowIndex = 0;
192
250
  let itemIndex = 0;
193
251
 
194
- while (itemIndex < this.items.length) {
252
+ while (itemIndex < itemCount && rowIndex < 1000) {
195
253
  let template = templates[rowIndex % templates.length];
254
+ let rowCardCount = cardCountInRowTemplate(template);
255
+ let isOrphanRow = itemIndex + rowCardCount > itemCount;
256
+
257
+ // render if this is not an orphan row or it is and we allow it
258
+ if (!isOrphanRow || this.allowOrphans) {
259
+ let row = template.map(spaceTemplate => {
260
+ let cards = Array(spaceTemplate.cards).fill(0);
261
+
262
+ cards = cards.map(() => {
263
+ let item = null;
264
+ const isOrphanPadding = itemIndex >= itemCount;
265
+ const isOrphan = isOrphanRow && !isOrphanPadding;
266
+
267
+ if (!isOrphanPadding || this.padOrphans) {
268
+ item = this.getItemForSpace({
269
+ index: itemIndex,
270
+ type: spaceTemplate.name,
271
+ orphan: isOrphan,
272
+ orphanPadding: isOrphanPadding,
273
+ });
274
+ }
275
+
276
+ itemIndex++;
277
+ return item;
278
+ });
279
+
280
+ return {
281
+ name: spaceTemplate.name,
282
+ cardCount: spaceTemplate.cards,
283
+ cards: cards,
284
+ };
285
+ });
196
286
 
197
- let row = template.map(spaceTemplate => {
198
- let cards = Array(spaceTemplate.cards).fill(0);
287
+ rows.push(row);
288
+ }
199
289
 
200
- cards = cards.map(() => {
201
- const item = this.getItemForSpace(itemIndex);
202
- itemIndex++;
203
- return item;
204
- });
290
+ // get out of the loop
291
+ else {
292
+ itemIndex += rowCardCount;
293
+ }
205
294
 
206
- return {
207
- name: spaceTemplate.name,
208
- cardCount: spaceTemplate.cards,
209
- cards: cards,
210
- };
211
- });
212
- rows.push(row);
213
295
  rowIndex++;
214
296
  }
215
297
 
216
- let hasOrphans = itemIndex > this.items.length;
217
-
218
- if (hasOrphans && !this.allowOrphans) {
219
- rows.pop();
220
- } else {
221
- if (!this.padOrphans) {
222
- rows[rows.length - 1] = rows[rows.length - 1].filter(sp => {
223
- sp.cards = sp.cards.filter(card => !!card);
224
- return sp.cards.length;
225
- });
226
- }
298
+ if (!this.padOrphans && itemIndex > itemCount) {
299
+ // remove empty
300
+ rows[rows.length - 1] = rows[rows.length - 1].filter(sp => {
301
+ sp.cards = sp.cards.filter(card => !!card);
302
+ return sp.cards.length;
303
+ });
227
304
  }
228
305
 
229
306
  return rows;
@@ -248,14 +325,30 @@ export default {
248
325
  delete this.resizeObserver;
249
326
  },
250
327
  methods: {
251
- getItemForSpace(index) {
328
+ getItemCount() {
329
+ if (this.getItemCountFn) {
330
+ return this.getItemCountFn();
331
+ }
332
+
333
+ return this.items.length;
334
+ },
335
+
336
+ getItemForSpace(space) {
337
+ if (this.getItemFn) {
338
+ return this.getItemFn(space);
339
+ }
340
+
341
+ if (space.orphanPadding) {
342
+ return space;
343
+ }
344
+
345
+ const index = space.index;
346
+
252
347
  if (this.items && index < this.items.length) {
253
348
  return this.items[index];
254
349
  }
255
350
 
256
- if (index > 100) {
257
- debugger;
258
- }
351
+ console.warn(`No item found for space at index ${index}.`);
259
352
  return null;
260
353
  },
261
354
 
@@ -0,0 +1,260 @@
1
+ <!--
2
+ Keeps a child element contained within boundaries.
3
+
4
+ <px-contain #default="{width, height, left: }" :aspect="16/9" :round="true" :to-px="true">
5
+ <div :style="{position: 'absolute', left: left top: top width: width height: height}" />
6
+ </px-contain>
7
+
8
+ -->
9
+
10
+ <template>
11
+ <div :class="bem()">
12
+ <slot
13
+ name="default"
14
+ :top="contain.top"
15
+ :right="contain.right"
16
+ :left="contain.left"
17
+ :bottom="contain.bottom"
18
+ :width="contain.width"
19
+ :height="contain.height"
20
+ :scale="contain.scale"
21
+ ></slot>
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import { contain } from '../utils/fit';
27
+ import aspectFilter from '../plugins/filters/aspect.js';
28
+ import PxBaseResize from './PxBaseResize.vue';
29
+
30
+ export default {
31
+ name: 'px-contain',
32
+ mixins: [PxBaseResize],
33
+
34
+ props: {
35
+ // an optional aspect ratio that should be maintained when calculating size and position, if
36
+ // not provided then width and height will always cover the full outer container (can be a
37
+ // number or ratio string like '16:9')
38
+ aspect: { default: null },
39
+
40
+ // if either of these is provided, then slot values will also include a value for scale
41
+ // which is the relative scale of the container compared to the natural size of the object;
42
+ // note also that aspect/width/height benefits all become available if just 2 of 3
43
+ // properties have been defined
44
+ naturalWidth: { type: Number, default: null },
45
+ naturalHeight: { type: Number, default: null },
46
+
47
+ // these are adjustments to the available area inside of the container rectangle; the
48
+ // difference between padding and border is that padding values will also affect the final
49
+ // top/left values but border does not (to visualize this, imagine that you use border if
50
+ // the content has a visible border around and the final values reflect that where padding
51
+ // is empty space so palcement gets shfited); specify as either a number or an arry of up to
52
+ // 4 numbers that follow the css box-model pattern
53
+ padding: { default: null },
54
+ border: { default: null },
55
+
56
+ // the horizontal positioning when there is leftover space
57
+ alignX: { default: 0.5 }, // number | 'start' | 'center' | 'end'
58
+
59
+ // the vertical positioning when there is leftover space
60
+ alignY: { default: 0.5 }, // number | 'start' | 'center' | 'end'
61
+
62
+ // whether slot values should be returned as strings with 'px'
63
+ toPx: { type: Boolean, default: true },
64
+
65
+ // whether slot values should be rounded to screen pixels
66
+ round: { type: Boolean, default: true },
67
+ },
68
+ data() {
69
+ return {
70
+ contain: {
71
+ current: false,
72
+ width: 0,
73
+ height: 0,
74
+ top: 0,
75
+ right: 0,
76
+ bottom: 0,
77
+ left: 0,
78
+ scale: 1,
79
+ },
80
+ };
81
+ },
82
+ computed: {
83
+ aspectNumber() {
84
+ return aspectFilter(this.aspect);
85
+ },
86
+ aspectRect() {
87
+ let a = this.aspect ? this.aspectNumber : null;
88
+ let w = this.naturalWidth;
89
+ let h = this.naturalHeight;
90
+
91
+ // width and height (ignore aspect)
92
+ if (w && h) {
93
+ return {
94
+ width: w,
95
+ height: h,
96
+ };
97
+ }
98
+
99
+ // just aspect (no width or height)
100
+ if (!w && !h && a) {
101
+ return {
102
+ width: a,
103
+ height: 1,
104
+ };
105
+ }
106
+
107
+ // width and aspect, infer height
108
+ if (w && a && !h) {
109
+ return {
110
+ width: w,
111
+ height: w * (1 / a),
112
+ };
113
+ }
114
+
115
+ // height and aspect, infer width
116
+ if (w && a && !h) {
117
+ return {
118
+ width: h * a,
119
+ height: h,
120
+ };
121
+ }
122
+
123
+ // no width or height or aspect
124
+ return null;
125
+ },
126
+ },
127
+
128
+ watch: {
129
+ aspectNumber() {
130
+ this.resize();
131
+ },
132
+ },
133
+ methods: {
134
+ resize() {
135
+ let p = this.toBoxModelObject(this.padding);
136
+
137
+ let b = this.toBoxModelObject(this.border);
138
+ let outer = this.$el.getBoundingClientRect();
139
+ let inner = this.aspectRect || outer;
140
+
141
+ outer = {
142
+ width: outer.width - (b.w + p.w),
143
+ height: outer.height - (b.h + p.h),
144
+ };
145
+
146
+ let r;
147
+
148
+ if (this.aspect) {
149
+ r = contain(
150
+ inner,
151
+ outer,
152
+ this.alignToDecimal(this.alignX),
153
+ this.alignToDecimal(this.alignY)
154
+ );
155
+ } else {
156
+ r = {
157
+ x: 0,
158
+ y: 0,
159
+ width: outer.width,
160
+ height: outer.height,
161
+ };
162
+ }
163
+
164
+ let c = {
165
+ current: true,
166
+ width: r.width + b.w,
167
+ height: r.height + b.h,
168
+ top: r.y + p.top,
169
+ left: r.x + p.left,
170
+ right: r.width - (r.x + b.w),
171
+ bottom: r.height - (r.y + b.h),
172
+ scale: this.naturalWidth || this.naturalHeight ? r.scale : 1,
173
+ };
174
+
175
+ this.$emit('update', c);
176
+
177
+ c.width = this.prepValue(c.width);
178
+ c.height = this.prepValue(c.height);
179
+ c.left = this.prepValue(c.left);
180
+ c.top = this.prepValue(c.top);
181
+ c.right = this.prepValue(c.right);
182
+ c.bottom = this.prepValue(c.bottom);
183
+
184
+ this.contain = c;
185
+ },
186
+
187
+ prepValue(n) {
188
+ if (this.round && typeof window != 'undefined') {
189
+ n = Math.round(n * window.devicePixelRatio) / window.devicePixelRatio;
190
+ }
191
+ if (this.toPx) {
192
+ n = this.cssPx(n);
193
+ }
194
+ return n;
195
+ },
196
+
197
+ toBoxModelObject(val) {
198
+ const arr = this.toBoxModelArray(val);
199
+ return {
200
+ top: arr[0],
201
+ right: arr[1],
202
+ bottom: arr[2],
203
+ left: arr[3],
204
+ w: arr[1] + arr[3],
205
+ h: arr[0] + arr[2],
206
+ };
207
+ },
208
+
209
+ toBoxModelArray(val) {
210
+ if (!val) {
211
+ return [0, 0, 0, 0];
212
+ }
213
+
214
+ if (Array.isArray(val)) {
215
+ if (val.length == 0) {
216
+ return [0, 0, 0, 0];
217
+ }
218
+ if (val.length == 1) {
219
+ return [val[0], val[0], val[0], val[0]];
220
+ }
221
+ if (val.length == 2) {
222
+ return [val[0], val[1], val[0], val[1]];
223
+ }
224
+ if (val.length == 3) {
225
+ return [val[0], val[1], val[2], val[1]];
226
+ }
227
+ return [val[0], val[1], val[2], val[3]];
228
+ }
229
+
230
+ return [val, val, val, val];
231
+ },
232
+
233
+ alignToDecimal(align) {
234
+ if (typeof align == 'number') {
235
+ return align;
236
+ }
237
+ if (align === 'center') {
238
+ return 0.5;
239
+ }
240
+ if (align === 'start') {
241
+ return 0;
242
+ }
243
+ if (align === 'end') {
244
+ return 1;
245
+ }
246
+ const float = parseFloat(align);
247
+ return !isNaN(float) ? float : 0.5;
248
+ },
249
+ },
250
+ };
251
+ </script>
252
+
253
+ <style lang="scss">
254
+ @use '../styles/px.scss' as *;
255
+ .px-contain {
256
+ width: 100%;
257
+ height: 100%;
258
+ position: relative;
259
+ }
260
+ </style>
@@ -22,10 +22,10 @@
22
22
  :name="calcTransitionName"
23
23
  @before-enter="transitionVisible = true"
24
24
  @after-leave="transitionVisible = false"
25
- :style="{ zIndex: zStart + currentLayerIndex }"
26
25
  >
27
26
  <div
28
27
  :class="bem({ ...variantsMap, absolute: strategy == 'absolute' })"
28
+ :style="{ zIndex: zStart + currentLayerIndex }"
29
29
  v-show="visibleValueNext"
30
30
  >
31
31
  <!-- overlay element -->
@@ -273,7 +273,6 @@ export default {
273
273
  }
274
274
  } else {
275
275
  if (this.cleanup) {
276
- console.log('cleanup');
277
276
  this.cleanup();
278
277
  this.cleanup = null;
279
278
  }
@@ -8,6 +8,7 @@
8
8
  :debug="true"
9
9
  :gap="60"
10
10
  :allow-orphans="true"
11
+ :pad-orphans="true"
11
12
  :maxColumns="4"
12
13
  :templates="{
13
14
  4: ['== -- -----', '-- == -- --', '----- -- ==', '-- -- == --'],
@@ -49,5 +50,6 @@ export default {
49
50
  <style lang="scss">
50
51
  .demo-px-card-grid {
51
52
  // add styles here
53
+ padding: 5vh 0 10vh;
52
54
  }
53
55
  </style>
@@ -0,0 +1,116 @@
1
+ <!--
2
+ Describe this component...
3
+ -->
4
+ <template>
5
+ <div :class="bem()">
6
+ <px-tester>
7
+ <px-test name="Aspect" testId="pxcontain-aspect" :size="{ h: 600 }">
8
+ <px-contain aspect="16:9" :px="true">
9
+ <template #default="{ top, left, width, height }">
10
+ <div
11
+ :style="{
12
+ position: 'absolute',
13
+ left,
14
+ top,
15
+ width,
16
+ height,
17
+ background: 'red',
18
+ }"
19
+ />
20
+ </template>
21
+ </px-contain>
22
+ </px-test>
23
+
24
+ <px-test name="Width / Height" testId="pxcontain-wh" :size="{ h: 600 }">
25
+ <px-contain :naturalWidth="1600" :naturalHeight="900">
26
+ <template #default="{ top, left, width, height }">
27
+ <div
28
+ :style="{
29
+ position: 'absolute',
30
+ left,
31
+ top,
32
+ width,
33
+ height,
34
+ background: 'gold',
35
+ }"
36
+ />
37
+ </template>
38
+ </px-contain>
39
+ </px-test>
40
+
41
+ <px-test name="Scale" testId="pxcontain-scale" :size="{ h: 600 }">
42
+ <px-contain :naturalWidth="1600" :naturalHeight="900">
43
+ <template #default="{ top, left, scale }">
44
+ <div
45
+ :style="{
46
+ position: 'absolute',
47
+ width: '1600px',
48
+ height: '900px',
49
+ top,
50
+ left,
51
+ transformOrigin: '0 0',
52
+ transform: `scale(${scale})`,
53
+ background: 'gold',
54
+ }"
55
+ />
56
+ </template>
57
+ </px-contain>
58
+ </px-test>
59
+ <px-test name="Basic with Padding" testId="pxcontain-pad" :size="{ h: 600 }">
60
+ <div
61
+ :style="{
62
+ width: '100%',
63
+ height: '100%',
64
+ padding: '49px',
65
+ border: '1px solid red',
66
+ boxSizing: 'border-box',
67
+ }"
68
+ >
69
+ <px-contain :aspect="4 / 3">
70
+ <template #default="{ top, left, width, height }">
71
+ <div
72
+ :style="{
73
+ position: 'absolute',
74
+ left,
75
+ top,
76
+ width,
77
+ height,
78
+ background: 'lightblue',
79
+ }"
80
+ >
81
+ {{ width }} x {{ height }}
82
+ </div>
83
+ </template>
84
+ </px-contain>
85
+ </div>
86
+ </px-test>
87
+ </px-tester>
88
+ </div>
89
+ </template>
90
+
91
+ <script>
92
+ // import { mapState } from 'vuex';
93
+ // import Component from '~/components/Component.vue';
94
+ // import Mixin from '~/components/Mixin.vue';
95
+ import { PxTester, PxTest } from '@thinkpixellab-public/px-vue-tester';
96
+ import PxContain from '../../components/PxContain.vue';
97
+
98
+ export default {
99
+ name: 'demo-px-contain',
100
+ components: { PxTester, PxTest, PxContain },
101
+ // mixins: [ Mixin ],
102
+ // props: { a: { type: Boolean, default: false } },
103
+ // data() { return { b: 0 }; },
104
+ // computed: {},
105
+ // watch: {},
106
+ // mounted() {},
107
+ // methods: {},
108
+ };
109
+ </script>
110
+
111
+ <style lang="scss">
112
+ //@import '@/styles/include.scss';
113
+ .demo-px-contain {
114
+ // add styles here
115
+ }
116
+ </style>
package/dev/dev.vue CHANGED
@@ -17,6 +17,7 @@ import DemoPxBaseDrag from './demos/Demo-PxBaseDrag.vue';
17
17
  import DemoPxStylesPreview from './demos/Demo-PxStylesPreview.vue';
18
18
  import DemoPxPivot from './demos/Demo-PxPivot.vue';
19
19
  import DemoPxFloat from './demos/Demo-PxFloat.vue';
20
+ import DemoPxContain from './demos/Demo-PxContain.vue';
20
21
 
21
22
  export default Vue.extend({
22
23
  components: {
@@ -30,6 +31,7 @@ export default Vue.extend({
30
31
  DemoPxStylesPreview,
31
32
  DemoPxPivot,
32
33
  DemoPxFloat,
34
+ DemoPxContain,
33
35
  },
34
36
  name: 'ServeDev',
35
37
  data() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": "Pixel Lab",
6
6
  "license": "MIT",
package/utils/fit.js CHANGED
@@ -52,13 +52,13 @@ export function contain(inner, outer, posX = 0.5, posY = 0.5) {
52
52
  height = outer.width / aspectInner;
53
53
  x = 0;
54
54
  y = outer.height * posY - height * posY;
55
- scale = height / outer.height;
55
+ scale = height / inner.height;
56
56
  } else {
57
57
  width = outer.height * aspectInner;
58
58
  height = outer.height;
59
59
  x = outer.width * posX - width * posX;
60
60
  y = 0;
61
- scale = width / outer.width;
61
+ scale = width / inner.width;
62
62
  }
63
63
 
64
64
  var rect = {