@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.
- package/lib/ItemGeometry/GenericGeometryController.js +43 -0
- package/lib/ItemGeometry/ItemGeometryContext.js +6 -0
- package/lib/ItemGeometry/ItemGeometryController.js +2 -0
- package/lib/ItemGeometry/ItemGeometryService.js +34 -0
- package/lib/ItemGeometry/RichTextGeometryController.js +135 -0
- package/lib/ItemGeometry/useItemGeometry.js +25 -0
- package/lib/components/Page.js +4 -1
- package/lib/components/items/CodeEmbedItem/CodeEmbedItem.js +2 -0
- package/lib/components/items/ComponentItem/ComponentItem.js +2 -0
- package/lib/components/items/CompoundItem/CompoundItem.js +17 -0
- package/lib/components/items/CustomItem/CustomItem.js +2 -0
- package/lib/components/items/EmbedVideoItem/VimeoEmbed.js +2 -0
- package/lib/components/items/EmbedVideoItem/YoutubeEmbed.js +2 -0
- package/lib/components/items/FileItem/ImageItem.js +2 -0
- package/lib/components/items/FileItem/VideoItem.js +2 -0
- package/lib/components/items/GroupItem/GroupItem.js +17 -0
- package/lib/components/items/Item.js +8 -0
- package/lib/components/items/RectangleItem/RectangleItem.js +2 -0
- package/lib/components/items/RichTextItem/RichTextItem.js +5 -1
- package/lib/components/items/useRichTextItemValues.js +2 -1
- package/lib/interactions/InteractionsRegistry.js +113 -12
- package/lib/interactions/ItemInteractionCtrl.js +1 -1
- package/lib/interactions/getTransition.js +1 -1
- package/lib/provider/InteractionsContext.js +4 -2
- package/lib/utils/CSSNumeric.js +19 -0
- package/lib/utils/RichTextConverter/RichTextConverter.js +4 -4
- package/lib/utils/domDfs.js +15 -0
- package/package.json +2 -2
- package/src/ItemGeometry/GenericGeometryController.ts +59 -0
- package/src/ItemGeometry/ItemGeometryContext.ts +4 -0
- package/src/ItemGeometry/ItemGeometryController.ts +22 -0
- package/src/ItemGeometry/ItemGeometryService.ts +34 -0
- package/src/ItemGeometry/RichTextGeometryController.ts +166 -0
- package/src/ItemGeometry/useItemGeometry.ts +25 -0
- package/src/components/Page.tsx +8 -3
- package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +2 -0
- package/src/components/items/ComponentItem/ComponentItem.tsx +2 -0
- package/src/components/items/CompoundItem/CompoundItem.tsx +18 -1
- package/src/components/items/CustomItem/CustomItem.tsx +2 -0
- package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -0
- package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -0
- package/src/components/items/FileItem/ImageItem.tsx +2 -0
- package/src/components/items/FileItem/VideoItem.tsx +2 -0
- package/src/components/items/GroupItem/GroupItem.tsx +18 -1
- package/src/components/items/Item.tsx +11 -3
- package/src/components/items/RectangleItem/RectangleItem.tsx +3 -1
- package/src/components/items/RichTextItem/RichTextItem.tsx +6 -1
- package/src/components/items/useRichTextItemValues.ts +2 -1
- package/src/interactions/InteractionsRegistry.ts +120 -15
- package/src/interactions/ItemInteractionCtrl.ts +2 -2
- package/src/interactions/getTransition.ts +1 -1
- package/src/interactions/types.ts +2 -2
- package/src/provider/InteractionsContext.tsx +4 -2
- package/src/utils/CSSNumeric.ts +19 -0
- package/src/utils/RichTextConverter/RichTextConverter.tsx +20 -4
- package/src/utils/domDfs.ts +11 -0
- package/cntrl-site-sdk-nextjs-1.9.14-2.tgz +0 -0
- /package/lib/{interactions → ItemGeometry}/CSSPropertyNameMap.js +0 -0
- /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 (!('
|
|
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.
|
|
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 (
|
|
247
|
+
if (trigger.type !== 'scroll-position' && trigger.type !== 'item-scroll-position')
|
|
158
248
|
return false;
|
|
159
|
-
|
|
160
|
-
|
|
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 ||
|
|
261
|
+
if (!matchingTrigger || (matchingTrigger.type !== 'scroll-position' && matchingTrigger.type !== 'item-scroll-position') || !activeStateId)
|
|
167
262
|
continue;
|
|
168
|
-
|
|
169
|
-
|
|
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) => '
|
|
312
|
+
const matchingTrigger = interaction.triggers.find((trigger) => 'triggerEvent' in trigger
|
|
212
313
|
&& trigger.itemId === itemId
|
|
213
314
|
&& trigger.from === currentStateId
|
|
214
|
-
&& trigger.
|
|
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("
|
|
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("
|
|
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.
|
|
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.
|
|
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,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
|
+
}
|
package/src/components/Page.tsx
CHANGED
|
@@ -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
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
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
|
);
|