@cntrl-site/sdk-nextjs 1.9.14 → 1.9.16

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.
Files changed (59) hide show
  1. package/lib/ItemGeometry/GenericGeometryController.js +43 -0
  2. package/lib/ItemGeometry/ItemGeometryContext.js +6 -0
  3. package/lib/ItemGeometry/ItemGeometryController.js +2 -0
  4. package/lib/ItemGeometry/ItemGeometryService.js +34 -0
  5. package/lib/ItemGeometry/RichTextGeometryController.js +135 -0
  6. package/lib/ItemGeometry/useItemGeometry.js +25 -0
  7. package/lib/components/Page.js +4 -1
  8. package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +2 -0
  9. package/lib/components/items/ComponentItem/ComponentItem.js +2 -0
  10. package/lib/components/items/CompoundItem/CompoundItem.js +17 -0
  11. package/lib/components/items/CustomItem/CustomItem.js +2 -0
  12. package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +2 -0
  13. package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +2 -0
  14. package/lib/components/items/FileItem/ImageItem.js +2 -0
  15. package/lib/components/items/FileItem/VideoItem.js +2 -0
  16. package/lib/components/items/GroupItem/GroupItem.js +17 -0
  17. package/lib/components/items/Item.js +8 -0
  18. package/lib/components/items/RectangleItem/RectangleItem.js +2 -0
  19. package/lib/components/items/RichTextItem/RichTextItem.js +5 -1
  20. package/lib/components/items/useRichTextItemValues.js +2 -1
  21. package/lib/interactions/InteractionsRegistry.js +113 -12
  22. package/lib/interactions/ItemInteractionCtrl.js +1 -1
  23. package/lib/interactions/getTransition.js +1 -1
  24. package/lib/provider/InteractionsContext.js +4 -2
  25. package/lib/utils/CSSNumeric.js +19 -0
  26. package/lib/utils/RichTextConverter/RichTextConverter.js +4 -4
  27. package/lib/utils/domDfs.js +15 -0
  28. package/package.json +2 -2
  29. package/src/ItemGeometry/GenericGeometryController.ts +59 -0
  30. package/src/ItemGeometry/ItemGeometryContext.ts +4 -0
  31. package/src/ItemGeometry/ItemGeometryController.ts +22 -0
  32. package/src/ItemGeometry/ItemGeometryService.ts +34 -0
  33. package/src/ItemGeometry/RichTextGeometryController.ts +166 -0
  34. package/src/ItemGeometry/useItemGeometry.ts +25 -0
  35. package/src/components/Page.tsx +8 -3
  36. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +2 -0
  37. package/src/components/items/ComponentItem/ComponentItem.tsx +2 -0
  38. package/src/components/items/CompoundItem/CompoundItem.tsx +18 -1
  39. package/src/components/items/CustomItem/CustomItem.tsx +2 -0
  40. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -0
  41. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -0
  42. package/src/components/items/FileItem/ImageItem.tsx +2 -0
  43. package/src/components/items/FileItem/VideoItem.tsx +2 -0
  44. package/src/components/items/GroupItem/GroupItem.tsx +18 -1
  45. package/src/components/items/Item.tsx +11 -3
  46. package/src/components/items/RectangleItem/RectangleItem.tsx +3 -1
  47. package/src/components/items/RichTextItem/RichTextItem.tsx +6 -1
  48. package/src/components/items/useRichTextItemValues.ts +2 -1
  49. package/src/interactions/InteractionsRegistry.ts +120 -15
  50. package/src/interactions/ItemInteractionCtrl.ts +2 -2
  51. package/src/interactions/getTransition.ts +1 -1
  52. package/src/interactions/types.ts +2 -2
  53. package/src/provider/InteractionsContext.tsx +4 -2
  54. package/src/utils/CSSNumeric.ts +19 -0
  55. package/src/utils/RichTextConverter/RichTextConverter.tsx +20 -4
  56. package/src/utils/domDfs.ts +11 -0
  57. package/cntrl-site-sdk-nextjs-1.9.14-2.tgz +0 -0
  58. /package/lib/{interactions → ItemGeometry}/CSSPropertyNameMap.js +0 -0
  59. /package/src/{interactions → ItemGeometry}/CSSPropertyNameMap.ts +0 -0
@@ -4,10 +4,12 @@ exports.InteractionsRegistry = void 0;
4
4
  const sdk_1 = require("@cntrl-site/sdk");
5
5
  const isItemType_1 = require("../utils/isItemType");
6
6
  class InteractionsRegistry {
7
- constructor(article, layoutId) {
7
+ constructor(article, layoutId, itemGeometryService) {
8
8
  var _a;
9
9
  this.ctrls = new Map();
10
+ this.itemScrollPositionAnimationMap = new Map();
10
11
  this.items = this.unpackItems(article);
12
+ this.itemGeometryService = itemGeometryService;
11
13
  const interactions = (_a = article.interactions[layoutId]) !== null && _a !== void 0 ? _a : [];
12
14
  const activeStatesIds = interactions.reduce((map, inter) => {
13
15
  var _a;
@@ -45,6 +47,7 @@ class InteractionsRegistry {
45
47
  this.interactionStateMap = interactionStateMap;
46
48
  const itemStages = this.getDefaultItemStages();
47
49
  this.itemsStages = itemStages;
50
+ this.itemScrollPositionAnimationMap = new Map();
48
51
  }
49
52
  register(itemId, ctrl) {
50
53
  this.ctrls.set(itemId, ctrl);
@@ -93,23 +96,62 @@ class InteractionsRegistry {
93
96
  for (const interaction of this.interactions) {
94
97
  const { triggers } = interaction;
95
98
  for (const trigger of triggers) {
96
- if (!('itemId' in trigger))
99
+ if (!('triggerEvent' in trigger))
97
100
  continue;
98
101
  if (trigger.itemId !== itemId)
99
102
  continue;
100
103
  if (activeStates.includes(trigger.from)) {
101
- available.add(trigger.type);
104
+ available.add(trigger.triggerEvent);
102
105
  }
103
106
  }
104
107
  }
105
108
  return available;
106
109
  }
110
+ getTriggerPositionForItemScrollTrigger(trigger) {
111
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
112
+ let triggerPosition = 0;
113
+ switch (trigger.screenPosition) {
114
+ case 'top':
115
+ triggerPosition = itemRect.top;
116
+ break;
117
+ case 'center':
118
+ triggerPosition = itemRect.top - window.innerHeight / 2;
119
+ break;
120
+ case 'bottom':
121
+ triggerPosition = itemRect.top - window.innerHeight;
122
+ break;
123
+ default:
124
+ triggerPosition = itemRect.top;
125
+ break;
126
+ }
127
+ switch (trigger.itemPosition) {
128
+ case 'top':
129
+ triggerPosition += 0;
130
+ break;
131
+ case 'center':
132
+ triggerPosition += itemRect.height / 2;
133
+ break;
134
+ case 'bottom':
135
+ triggerPosition += itemRect.height;
136
+ break;
137
+ default:
138
+ triggerPosition += 0;
139
+ break;
140
+ }
141
+ triggerPosition += trigger.offset;
142
+ return triggerPosition;
143
+ }
144
+ isItemScrollTriggerOnStartScreen(trigger) {
145
+ const triggerPosition = this.getTriggerPositionForItemScrollTrigger(trigger);
146
+ return triggerPosition < 0;
147
+ }
107
148
  notifyLoad() {
108
149
  var _a, _b;
109
150
  const timestamp = Date.now();
110
151
  for (const interaction of this.interactions) {
111
152
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
112
- const matchingTrigger = interaction.triggers.find(trigger => 'position' in trigger && trigger.position === 0 && trigger.from === currentStateId);
153
+ const matchingTrigger = interaction.triggers.find(trigger => ('position' in trigger && trigger.position === 0 && trigger.from === currentStateId)
154
+ || (trigger.type === 'item-scroll-position' && this.items.find((item) => item.id === trigger.itemId) && trigger.from === currentStateId && this.isItemScrollTriggerOnStartScreen(trigger)));
113
155
  if (!matchingTrigger)
114
156
  continue;
115
157
  const activeStateId = this.getActiveInteractionState(interaction.id);
@@ -147,6 +189,54 @@ class InteractionsRegistry {
147
189
  this.notifyTransitionStartForItems(transitioningItems, activeStateId);
148
190
  }
149
191
  }
192
+ scrollPositionNotifyScroll(trigger, position) {
193
+ if (trigger.position === 0)
194
+ return false;
195
+ const triggerPosition = trigger.position * window.innerWidth;
196
+ return triggerPosition < position;
197
+ }
198
+ scrollItemPositionNotifyScroll(trigger, position, interaction) {
199
+ let triggerPosition;
200
+ let isScrolledPastTrigger = false;
201
+ const itemRect = this.itemGeometryService.getItemBoundary(trigger.itemId);
202
+ if (!itemRect)
203
+ return false;
204
+ switch (trigger.screenPosition) {
205
+ case 'top':
206
+ triggerPosition = 0;
207
+ break;
208
+ case 'center':
209
+ triggerPosition = window.innerHeight / 2;
210
+ break;
211
+ case 'bottom':
212
+ triggerPosition = window.innerHeight;
213
+ break;
214
+ default:
215
+ triggerPosition = 0;
216
+ break;
217
+ }
218
+ triggerPosition -= trigger.offset;
219
+ switch (trigger.itemPosition) {
220
+ case 'top':
221
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
222
+ break;
223
+ case 'center':
224
+ isScrolledPastTrigger = itemRect.y + itemRect.height / 2 < triggerPosition;
225
+ break;
226
+ case 'bottom':
227
+ isScrolledPastTrigger = itemRect.bottom < triggerPosition;
228
+ break;
229
+ default:
230
+ isScrolledPastTrigger = itemRect.y < triggerPosition;
231
+ break;
232
+ }
233
+ if (isScrolledPastTrigger && !this.itemScrollPositionAnimationMap.get(interaction.id)) {
234
+ this.itemScrollPositionAnimationMap.set(interaction.id, position + itemRect.y - triggerPosition);
235
+ }
236
+ if (this.itemScrollPositionAnimationMap.get(interaction.id) >= position)
237
+ isScrolledPastTrigger = false;
238
+ return isScrolledPastTrigger;
239
+ }
150
240
  notifyScroll(position) {
151
241
  var _a, _b, _c;
152
242
  const timestamp = Date.now();
@@ -154,19 +244,30 @@ class InteractionsRegistry {
154
244
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
155
245
  const activeStateId = (_a = interaction.states.find((state) => state.id !== interaction.startStateId)) === null || _a === void 0 ? void 0 : _a.id;
156
246
  const matchingTrigger = interaction.triggers.find((trigger) => {
157
- if (!('position' in trigger) || trigger.position === 0)
247
+ if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position')
158
248
  return false;
159
- const triggerPosition = trigger.position * window.innerWidth;
160
- const isScrolledPastTrigger = triggerPosition < position;
249
+ let isScrolledPastTrigger = false;
250
+ if (trigger.type === 'scroll-position') {
251
+ isScrolledPastTrigger = this.scrollPositionNotifyScroll(trigger, position);
252
+ }
253
+ if (trigger.type === 'item-scroll-position') {
254
+ isScrolledPastTrigger = this.scrollItemPositionNotifyScroll(trigger, position, interaction);
255
+ }
161
256
  if (!isScrolledPastTrigger && !trigger.isReverse)
162
257
  return false;
163
258
  const stateId = isScrolledPastTrigger ? trigger.from : trigger.to;
164
259
  return stateId === currentStateId;
165
260
  });
166
- if (!matchingTrigger || !('position' in matchingTrigger) || !activeStateId)
261
+ if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId)
167
262
  continue;
168
- const triggerPosition = matchingTrigger.position * window.innerWidth;
169
- const isScrolledPastTrigger = triggerPosition < position;
263
+ let isScrolledPastTrigger = false;
264
+ if (matchingTrigger.type === 'scroll-position') {
265
+ const triggerPosition = matchingTrigger.position * window.innerWidth;
266
+ isScrolledPastTrigger = triggerPosition < position;
267
+ }
268
+ if (matchingTrigger.type === 'item-scroll-position') {
269
+ isScrolledPastTrigger = this.itemScrollPositionAnimationMap.get(interaction.id) < position; // scroll down
270
+ }
170
271
  const targetStateId = isScrolledPastTrigger ? matchingTrigger.to : matchingTrigger.from;
171
272
  this.setCurrentStateForInteraction(interaction.id, targetStateId);
172
273
  const transitioningItems = (_b = this.stateItemsIdsMap[activeStateId]) !== null && _b !== void 0 ? _b : [];
@@ -208,10 +309,10 @@ class InteractionsRegistry {
208
309
  const timestamp = Date.now();
209
310
  for (const interaction of this.interactions) {
210
311
  const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
211
- const matchingTrigger = interaction.triggers.find((trigger) => 'itemId' in trigger
312
+ const matchingTrigger = interaction.triggers.find((trigger) => 'triggerEvent' in trigger
212
313
  && trigger.itemId === itemId
213
314
  && trigger.from === currentStateId
214
- && trigger.type === triggerType);
315
+ && trigger.triggerEvent === triggerType);
215
316
  if (!matchingTrigger)
216
317
  continue;
217
318
  const activeStateId = this.getActiveInteractionState(interaction.id);
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ItemInteractionController = void 0;
4
4
  const getTransition_1 = require("./getTransition");
5
- const CSSPropertyNameMap_1 = require("./CSSPropertyNameMap");
5
+ const CSSPropertyNameMap_1 = require("../ItemGeometry/CSSPropertyNameMap");
6
6
  class ItemInteractionController {
7
7
  constructor(itemId, registry, onChange) {
8
8
  this.itemId = itemId;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getTransition = void 0;
4
- const CSSPropertyNameMap_1 = require("./CSSPropertyNameMap");
4
+ const CSSPropertyNameMap_1 = require("../ItemGeometry/CSSPropertyNameMap");
5
5
  function getTransition(state, keys) {
6
6
  if (Object.keys(state).length === 0)
7
7
  return 'none';
@@ -6,15 +6,17 @@ const react_1 = require("react");
6
6
  const InteractionsRegistry_1 = require("../interactions/InteractionsRegistry");
7
7
  const useCurrentLayout_1 = require("../common/useCurrentLayout");
8
8
  const ArticleRectContext_1 = require("./ArticleRectContext");
9
+ const ItemGeometryContext_1 = require("../ItemGeometry/ItemGeometryContext");
9
10
  exports.InteractionsContext = (0, react_1.createContext)(undefined);
10
11
  const InteractionsProvider = ({ article, children }) => {
11
12
  const { layoutId } = (0, useCurrentLayout_1.useCurrentLayout)();
12
13
  const articleRectObserver = (0, react_1.useContext)(ArticleRectContext_1.ArticleRectContext);
14
+ const itemGeometryService = (0, react_1.useContext)(ItemGeometryContext_1.ItemGeometryContext);
13
15
  const registry = (0, react_1.useMemo)(() => {
14
16
  if (!layoutId)
15
17
  return;
16
- return new InteractionsRegistry_1.InteractionsRegistry(article, layoutId);
17
- }, [layoutId]);
18
+ return new InteractionsRegistry_1.InteractionsRegistry(article, layoutId, itemGeometryService);
19
+ }, [layoutId, itemGeometryService]);
18
20
  (0, react_1.useEffect)(() => {
19
21
  if (!registry || !articleRectObserver)
20
22
  return;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCSSNumber = exports.parseCSSNumber = void 0;
4
+ const reCSSNumeric = /([+-]?(?:\d*\.\d|\d)\d*)(\w+)/;
5
+ function parseCSSNumber(input) {
6
+ const match = reCSSNumeric.exec(input);
7
+ if (!match)
8
+ return { value: Number.NaN };
9
+ return {
10
+ value: Number.parseFloat(match[1]),
11
+ unit: match[2]
12
+ };
13
+ }
14
+ exports.parseCSSNumber = parseCSSNumber;
15
+ function formatCSSNumber(input) {
16
+ var _a;
17
+ return `${input.value}${(_a = input.unit) !== null && _a !== void 0 ? _a : ''}`;
18
+ }
19
+ exports.formatCSSNumber = formatCSSNumber;
@@ -76,18 +76,18 @@ class RichTextConverter {
76
76
  for (const entity of entitiesGroups) {
77
77
  const entityKids = [];
78
78
  if (offset < entity.start) {
79
- kids.push(sliceSymbols(content, offset, entity.start));
79
+ kids.push((0, jsx_runtime_1.jsx)("span", { children: sliceSymbols(content, offset, entity.start) }));
80
80
  offset = entity.start;
81
81
  }
82
82
  for (const style of entity.stylesGroup) {
83
83
  if (offset < style.start) {
84
- entityKids.push(sliceSymbols(content, offset, style.start));
84
+ entityKids.push((0, jsx_runtime_1.jsx)("span", { children: sliceSymbols(content, offset, style.start) }));
85
85
  }
86
86
  entityKids.push((0, jsx_runtime_1.jsx)("span", { className: `s-${style.start}-${style.end}`, children: sliceSymbols(content, style.start, style.end) }, style.start));
87
87
  offset = style.end;
88
88
  }
89
89
  if (offset < entity.end) {
90
- entityKids.push(sliceSymbols(content, offset, entity.end));
90
+ entityKids.push((0, jsx_runtime_1.jsx)("span", { children: sliceSymbols(content, offset, entity.end) }));
91
91
  offset = entity.end;
92
92
  }
93
93
  if (entity.link) {
@@ -97,7 +97,7 @@ class RichTextConverter {
97
97
  kids.push(...entityKids);
98
98
  }
99
99
  if (offset < getSymbolsCount(content)) {
100
- kids.push(sliceSymbols(content, offset));
100
+ kids.push((0, jsx_runtime_1.jsx)("span", { children: sliceSymbols(content, offset) }));
101
101
  }
102
102
  for (const item of group) {
103
103
  const { exemplary } = layouts.find(l => l.id === item.layout);
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.domDfs = void 0;
4
+ function domDfs(root, visitor) {
5
+ const q = root;
6
+ for (let i = 0; i < q.length; i += 1) {
7
+ if (q[i].children.length > 0) {
8
+ q.push(...q[i].children);
9
+ }
10
+ }
11
+ for (let i = q.length - 1; i >= 0; i -= 1) {
12
+ visitor(q[i]);
13
+ }
14
+ }
15
+ exports.domDfs = domDfs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.9.14",
3
+ "version": "1.9.16",
4
4
  "description": "SDK for Next.js",
5
5
  "author": "arsen@momdesign.nyc",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "@cntrl-site/color": "^1.0.0",
33
33
  "@cntrl-site/components": "^0.0.3",
34
34
  "@cntrl-site/effects": "^1.4.0",
35
- "@cntrl-site/sdk": "^1.25.0",
35
+ "@cntrl-site/sdk": "^1.25.1",
36
36
  "@types/vimeo__player": "^2.18.0",
37
37
  "@vimeo/player": "^2.25.0",
38
38
  "html-react-parser": "^3.0.1",
@@ -0,0 +1,59 @@
1
+ import { Rect } from '@cntrl-site/sdk';
2
+ import { ItemGeometryController } from './ItemGeometryController';
3
+
4
+ type GeomtryRegistry = {
5
+ getControllerById: (itemId: string) => ItemGeometryController;
6
+ };
7
+
8
+ export class GenericGeometryController implements ItemGeometryController {
9
+ private scale: number = 1;
10
+ private parentId?: string;
11
+ private offsetParent: HTMLElement | null = null;
12
+ private angle: number = 0;
13
+ private registry: GeomtryRegistry | undefined;
14
+
15
+ constructor(
16
+ private container: HTMLElement
17
+ ) { }
18
+
19
+ setRegistry(registry: GeomtryRegistry) {
20
+ this.registry = registry;
21
+ }
22
+
23
+ setParentId(parentId?: string) {
24
+ this.parentId = parentId;
25
+ }
26
+
27
+ getParentId(): string | undefined {
28
+ return this.parentId;
29
+ }
30
+
31
+ getBoundary(): Rect {
32
+ const rect = this.container.getBoundingClientRect();
33
+ return Rect.fromObject(rect);
34
+ }
35
+
36
+ getContentBoundary(): Rect {
37
+ return this.getBoundary();
38
+ }
39
+
40
+ setScale(scale: number): void {
41
+ this.scale = scale;
42
+ }
43
+
44
+ setAngle(angle: number) {
45
+ this.angle = angle;
46
+ }
47
+
48
+ setOptions(options?: unknown) {
49
+
50
+ }
51
+
52
+ getAngle(): number {
53
+ return 0;
54
+ }
55
+
56
+ getScale() {
57
+ return 1;
58
+ }
59
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+ import { ItemGeometryService } from "./ItemGeometryService";
3
+
4
+ export const ItemGeometryContext = createContext(new ItemGeometryService());
@@ -0,0 +1,22 @@
1
+ import { Rect } from '@cntrl-site/sdk';
2
+
3
+ export interface GeometryControllerCtor {
4
+ new (element: HTMLElement): ItemGeometryController;
5
+ }
6
+
7
+ interface Registry {
8
+ getControllerById(itemId: string): ItemGeometryController;
9
+ }
10
+
11
+ export interface ItemGeometryController {
12
+ getBoundary(isRotatedBoundary?: boolean): Rect;
13
+ getContentBoundary(isRotatedBoundary?: boolean): Rect;
14
+ getScale(): number;
15
+ setScale(scale: number): void;
16
+ setAngle(angle: number): void;
17
+ setRegistry(regisrty: Registry): void;
18
+ setOptions(options?: unknown): void;
19
+ setParentId(parentId?: string): void;
20
+ getParentId(): string | undefined;
21
+ getAngle(): number;
22
+ }
@@ -0,0 +1,34 @@
1
+ import { Rect } from '@cntrl-site/sdk';
2
+ import { ItemGeometryController } from './ItemGeometryController';
3
+
4
+ export class ItemGeometryService {
5
+ private registry: Map<string, ItemGeometryController> = new Map();
6
+
7
+ register(itemId: string, controller: ItemGeometryController): () => void {
8
+ this.registry.set(itemId, controller);
9
+ controller.setRegistry(this);
10
+ return () => {
11
+ this.registry.delete(itemId);
12
+ };
13
+ }
14
+
15
+ getItemBoundary(itemId: string) {
16
+ const controller = this.registry.get(itemId);
17
+ if (!controller) {
18
+ return new Rect(0, 0, 0, 0);
19
+ }
20
+ return controller.getContentBoundary();
21
+ }
22
+
23
+ getControllerById(itemId: string): ItemGeometryController {
24
+ const controller = this.registry.get(itemId);
25
+ if (!controller) {
26
+ throw new ReferenceError(`There is no registred contoller for item w/ id ${itemId}`);
27
+ }
28
+ return controller;
29
+ }
30
+
31
+ hasItem(itemId: string): boolean {
32
+ return this.registry.has(itemId);
33
+ }
34
+ }
@@ -0,0 +1,166 @@
1
+ import { measureFont, Rect } from '@cntrl-site/sdk';
2
+ import { ItemGeometryController } from './ItemGeometryController';
3
+ import { domDfs } from '../utils/domDfs';
4
+ import { formatCSSNumber, parseCSSNumber } from '../utils/CSSNumeric';
5
+
6
+ export enum SizingType {
7
+ Auto = 'auto',
8
+ Manual = 'manual',
9
+ }
10
+
11
+ interface RichTextGeometryOptions {
12
+ xSizing: SizingType;
13
+ }
14
+
15
+ type GeomtryRegistry = {
16
+ getControllerById: (itemId: string) => ItemGeometryController;
17
+ };
18
+
19
+ export class RichTextGeometryController implements ItemGeometryController {
20
+ private scale: number = 1;
21
+ private angle: number = 0;
22
+ private options!: RichTextGeometryOptions;
23
+ private parentId?: string;
24
+ private registry: GeomtryRegistry | undefined;
25
+ static parseCssValue(value: string) {
26
+ const parsedLs = Number.parseInt(value);
27
+ return Number.isNaN(parsedLs) ? 0 : parsedLs;
28
+ }
29
+
30
+ constructor(
31
+ private container: HTMLElement
32
+ ) { }
33
+
34
+ setRegistry(registry: GeomtryRegistry) {
35
+ this.registry = registry;
36
+ }
37
+
38
+ getBoundary(isRotatedBoundary?: boolean): Rect {
39
+ const rect = this.container.getBoundingClientRect();
40
+ if (isRotatedBoundary) {
41
+ return Rect.fromObject(rect);
42
+ }
43
+ const angle = this.getAngle();
44
+ if (angle === 0) return Rect.fromObject(rect);
45
+ const styles = getComputedStyle(this.container);
46
+ const ratio = Number.parseInt(styles.width) / Number.parseInt(styles.height);
47
+ return Rect.getOriginRectFromBoundary(rect, angle, ratio);
48
+ }
49
+
50
+ getContentGuides(): [boundary: Rect, xs: number[], ys: number[]] {
51
+ const boundary = this.getBoundary();
52
+ const contentXs: Set<number> = new Set();
53
+ const { xSizing } = this.options;
54
+ const contentYs: Set<number> = new Set();
55
+ domDfs(Array.from(this.container.children), (el) => {
56
+ if (el.children.length !== 0) return;
57
+ const rects = el.getClientRects();
58
+ const styles = window.getComputedStyle(el);
59
+ const ls = RichTextGeometryController.parseCssValue(styles.letterSpacing);
60
+ const metrics = measureFont(this.getFontStr(styles), el.textContent?.trim() ?? '');
61
+ const {
62
+ capHeight,
63
+ baseline,
64
+ fontBoxHeight,
65
+ xHeight,
66
+ leftMargin,
67
+ rightMargin
68
+ } = metrics;
69
+ for (let i = 0; i <= rects.length - 1; i += 1) {
70
+ const rect = rects[i];
71
+ const halfLead = (rect.height - fontBoxHeight) / 2;
72
+ contentXs.add(rect.left + leftMargin - boundary.left);
73
+ contentXs.add(rect.right - rightMargin - ls - boundary.left);
74
+ contentYs.add(rect.top + halfLead + baseline - boundary.top); // baseline
75
+ if (i === 0) {
76
+ contentYs.add(rect.top + halfLead + baseline - capHeight - boundary.top); // cap height
77
+ }
78
+ if (rects.length === 1) {
79
+ contentYs.add(rect.top + halfLead + baseline - xHeight - boundary.top); // x-height
80
+ }
81
+ }
82
+ });
83
+ const left = xSizing === SizingType.Auto ? Math.min(...contentXs.values()) : 0;
84
+ const right = xSizing === SizingType.Auto ? Math.max(...contentXs.values()) : boundary.width;
85
+ const center = left + (right - left) / 2;
86
+ return [
87
+ boundary,
88
+ [left, center, right],
89
+ [...Array.from(contentYs.values(), y => y)]
90
+ ];
91
+ }
92
+
93
+ setParentId(parentId?: string) {
94
+ this.parentId = parentId;
95
+ }
96
+
97
+ getParentId(): string | undefined {
98
+ return this.parentId;
99
+ }
100
+
101
+ setScale(scale: number) {
102
+ this.scale = scale;
103
+ }
104
+
105
+ setAngle(angle: number) {
106
+ this.angle = angle;
107
+ }
108
+
109
+ getContentBoundary(): Rect {
110
+ if (this.getAngle() !== 0) {
111
+ return this.getBoundary(true);
112
+ }
113
+ const [boundary, xs, ys] = this.getContentGuides();
114
+ const left = Math.min(...xs);
115
+ const right = Math.max(...xs);
116
+
117
+ const top = Math.min(...ys);
118
+ const bottom = Math.max(...ys);
119
+ const content = new Rect(
120
+ boundary.left + left,
121
+ boundary.top + top,
122
+ right - left,
123
+ bottom - top
124
+ );
125
+ return content;
126
+ }
127
+
128
+ private getFontStr(styles: CSSStyleDeclaration) {
129
+ const fontSize = parseCSSNumber(styles.fontSize);
130
+ const lineHeight = parseCSSNumber(styles.lineHeight);
131
+ const scaledFontSize = formatCSSNumber({
132
+ value: fontSize.value * this.getScale(),
133
+ unit: fontSize.unit
134
+ });
135
+ const scaledLineHeight = formatCSSNumber({
136
+ value: lineHeight.value * this.getScale(),
137
+ unit: lineHeight.unit
138
+ });
139
+ return `${scaledFontSize} / ${scaledLineHeight} ${styles.fontFamily}`;
140
+ }
141
+
142
+ setOptions(options?: RichTextGeometryOptions) {
143
+ if (!options) return;
144
+ this.options = options;
145
+ }
146
+
147
+ getAngle(): number {
148
+ return this.angle + this.getParentAngle();
149
+ }
150
+
151
+ getScale() {
152
+ return this.scale * this.getParentScale();
153
+ }
154
+
155
+ private getParentAngle() {
156
+ if (!this.registry || !this.parentId) return 0;
157
+ const parentController = this.registry.getControllerById(this.parentId);
158
+ return parentController.getAngle();
159
+ }
160
+
161
+ getParentScale(): number {
162
+ if (!this.registry || !this.parentId) return 1;
163
+ const parentController = this.registry.getControllerById(this.parentId);
164
+ return parentController.getScale();
165
+ }
166
+ }
@@ -0,0 +1,25 @@
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import { GenericGeometryController } from './GenericGeometryController';
3
+ import { GeometryControllerCtor } from './ItemGeometryController';
4
+ import { ItemGeometryContext } from './ItemGeometryContext';
5
+
6
+ export function useItemGeometry<T extends GeometryControllerCtor = typeof GenericGeometryController>(
7
+ itemId: string,
8
+ element?: HTMLElement | null,
9
+ // @ts-ignore
10
+ controllerCtor: T = GenericGeometryController,
11
+ options?: any
12
+ ): InstanceType<T> | undefined {
13
+ const itemGeometryService = useContext(ItemGeometryContext);
14
+ const controller = useMemo(() => {
15
+ if (!element) return undefined;
16
+ return new controllerCtor(element);
17
+ }, [element, controllerCtor]);
18
+ useEffect(() => {
19
+ if (!controller) return;
20
+ controller.setOptions(options);
21
+ return itemGeometryService.register(itemId, controller);
22
+ }, [itemGeometryService, itemId, controller]);
23
+ // @ts-ignore
24
+ return controller;
25
+ }
@@ -5,6 +5,8 @@ import { Article } from './Article';
5
5
  import { KeyframesContext } from '../provider/KeyframesContext';
6
6
  import { CNTRLHead } from './Head';
7
7
  import { Keyframes } from '../provider/Keyframes';
8
+ import { ItemGeometryContext } from '../ItemGeometry/ItemGeometryContext';
9
+ import { ItemGeometryService } from '../ItemGeometry/ItemGeometryService';
8
10
 
9
11
  export interface PageProps {
10
12
  article: TArticle;
@@ -18,13 +20,16 @@ export const Page: FC<PageProps> = ({ article, project, meta, keyframes, section
18
20
  const afterBodyOpen = HTMLReactParser(project.html.afterBodyOpen);
19
21
  const beforeBodyClose = HTMLReactParser(project.html.beforeBodyClose);
20
22
  const keyframesRepo = useMemo(() => new Keyframes(keyframes), [keyframes]);
23
+ const itemGeometryService = useMemo(() => new ItemGeometryService(), []);
21
24
  return (
22
25
  <>
23
26
  <CNTRLHead project={project} meta={meta} />
24
27
  {afterBodyOpen}
25
- <KeyframesContext.Provider value={keyframesRepo}>
26
- <Article article={article} sectionData={sectionData} />
27
- </KeyframesContext.Provider>
28
+ <ItemGeometryContext.Provider value={itemGeometryService}>
29
+ <KeyframesContext.Provider value={keyframesRepo}>
30
+ <Article article={article} sectionData={sectionData} />
31
+ </KeyframesContext.Provider>
32
+ </ItemGeometryContext.Provider>
28
33
  {beforeBodyClose}
29
34
  </>
30
35
  );