11const defaultStyles = require ( './defaultStyles' ) ;
2+ const localizedData = require ( 'apiops-cycles-method-data/localizedData.json' ) ;
3+
4+ function getLocaleKey ( locale ) {
5+ if ( ! locale ) return defaultStyles . defaultLocale ;
6+ const lower = String ( locale ) . toLowerCase ( ) ;
7+ if ( localizedData [ lower ] ) return lower ;
8+ const base = lower . split ( '-' ) [ 0 ] ;
9+ return localizedData [ base ] ? base : lower ;
10+ }
11+
12+ function wrapTextApprox ( text , maxWidth = defaultStyles . maxLineWidth ) {
13+ const normalized = String ( text || '' ) . replace ( / \n { 2 , } / g, '\n' ) ;
14+ const words = normalized . split ( ' ' ) ;
15+ const lines = [ ] ;
16+ let line = '' ;
17+
18+ for ( const word of words ) {
19+ const testLine = `${ line } ${ word } ` ;
20+ if ( testLine . length * 6 > maxWidth && line . trim ( ) ) {
21+ lines . push ( line . trim ( ) ) ;
22+ line = `${ word } ` ;
23+ } else {
24+ line = testLine ;
25+ }
26+ }
27+
28+ if ( line . trim ( ) ) {
29+ lines . push ( line . trim ( ) ) ;
30+ }
31+
32+ return lines . join ( '\n' ) ;
33+ }
234
335function sanitizeInput ( text ) {
436 // Remove script tags entirely
@@ -19,18 +51,25 @@ function validateInput(text) {
1951}
2052
2153function distributeMissingPositions ( content , canvasDef , styles = defaultStyles ) {
54+ const resolvedStyles = { ...defaultStyles , ...styles } ;
2255 const cellWidth = Math . floor (
23- ( styles . width - canvasDef . layout . columns * styles . padding ) /
56+ ( resolvedStyles . width - canvasDef . layout . columns * resolvedStyles . padding ) /
2457 canvasDef . layout . columns ,
2558 ) ;
2659
2760 const cellHeight = Math . floor (
28- ( styles . height -
29- styles . headerHeight -
30- styles . footerHeight -
31- 4 * styles . padding ) /
61+ ( resolvedStyles . height -
62+ resolvedStyles . headerHeight -
63+ resolvedStyles . footerHeight -
64+ 4 * resolvedStyles . padding ) /
3265 canvasDef . layout . rows ,
3366 ) ;
67+ const locale = getLocaleKey ( content . locale || resolvedStyles . defaultLocale ) ;
68+ const localizedCanvas =
69+ ( localizedData [ locale ] && localizedData [ locale ] [ canvasDef . id ] ) || { } ;
70+ const noteGap = Math . max ( 4 , Math . floor ( resolvedStyles . stickyNoteSpacing / 2 ) ) ;
71+ const borderGap = Math . max ( 4 , Math . floor ( resolvedStyles . padding / 2 ) ) ;
72+ const journeyNoteInsetY = Math . max ( 4 , Math . floor ( noteGap / 2 ) ) ;
3473
3574 content . sections . forEach ( ( section ) => {
3675 const templateSection = canvasDef . sections . find (
@@ -44,36 +83,147 @@ function distributeMissingPositions(content, canvasDef, styles = defaultStyles)
4483 if ( notesToPlace . length === 0 ) return ;
4584
4685 const startX =
47- templateSection . gridPosition . column * cellWidth + 2 * styles . padding ;
48- const startY =
49- templateSection . gridPosition . row * cellHeight + styles . headerHeight ;
86+ templateSection . gridPosition . column * cellWidth + borderGap ;
5087 const secWidth = templateSection . gridPosition . colSpan * cellWidth ;
5188 const secHeight = templateSection . gridPosition . rowSpan * cellHeight ;
89+ const sectionTop =
90+ templateSection . gridPosition . row * cellHeight + resolvedStyles . headerHeight ;
91+ const sectionBottom = sectionTop + secHeight ;
92+ const sectionTitle =
93+ ( localizedCanvas . sections &&
94+ localizedCanvas . sections [ section . sectionId ] &&
95+ localizedCanvas . sections [ section . sectionId ] . section ) ||
96+ templateSection . id ;
97+ const sectionDescription =
98+ ( localizedCanvas . sections &&
99+ localizedCanvas . sections [ section . sectionId ] &&
100+ localizedCanvas . sections [ section . sectionId ] . description ) ||
101+ '' ;
102+ const titleLines = wrapTextApprox (
103+ sectionTitle ,
104+ secWidth - 2 * resolvedStyles . padding - resolvedStyles . circleRadius ,
105+ )
106+ . split ( '\n' )
107+ . filter ( ( line ) => line . length > 0 ) . length || 1 ;
108+ const titleTop = sectionTop + resolvedStyles . padding + resolvedStyles . circleRadius ;
109+ const titleBottom =
110+ titleTop + titleLines * ( resolvedStyles . fontSize + 6 ) ;
111+ const descriptionBottom =
112+ titleBottom + noteGap ;
113+ const sectionBox = {
114+ x : templateSection . gridPosition . column * cellWidth + 2 * resolvedStyles . padding ,
115+ y : sectionTop ,
116+ width : secWidth ,
117+ height : secHeight ,
118+ } ;
119+ const journeyLayout = templateSection . journeySteps
120+ ? getJourneyStepsLayout ( templateSection , sectionBox , resolvedStyles )
121+ : null ;
122+ const startY = templateSection . journeySteps
123+ ? Math . max (
124+ descriptionBottom + noteGap ,
125+ journeyLayout ? journeyLayout . boxes [ 0 ] . y + noteGap : titleBottom + noteGap ,
126+ )
127+ : descriptionBottom + noteGap ;
128+
129+ const noteSize = resolvedStyles . stickyNoteSize ;
130+ if ( journeyLayout ) {
131+ notesToPlace . forEach ( ( note , index ) => {
132+ const box = journeyLayout . boxes [ index % journeyLayout . boxes . length ] ;
133+ const row = Math . floor ( index / journeyLayout . boxes . length ) ;
134+ const rowOffset = row * ( noteSize + noteGap ) ;
135+ note . position = {
136+ x : box . x + Math . max ( 0 , Math . floor ( ( box . width - noteSize ) / 2 ) ) ,
137+ y : box . y + Math . max ( 0 , Math . floor ( ( box . height - noteSize ) / 2 ) ) +
138+ journeyNoteInsetY +
139+ rowOffset ,
140+ } ;
141+ } ) ;
142+ return ;
143+ }
52144
53- const noteSize = styles . stickyNoteSize ;
145+ const innerLeft = sectionBox . x ;
146+ const innerRight = sectionBox . x + secWidth ;
147+ const availableWidth = Math . max ( noteSize , innerRight - innerLeft ) ;
148+ const availableHeight = sectionBottom - startY - borderGap - ( resolvedStyles . circleRadius / 2 ) ;
54149 const maxCols = Math . max (
55150 1 ,
56- Math . floor ( secWidth / ( noteSize + styles . stickyNoteSpacing ) ) ,
151+ Math . floor ( ( availableWidth + noteGap ) / ( noteSize + noteGap ) ) ,
152+ ) ;
153+ const maxRows = Math . max (
154+ 1 ,
155+ Math . floor ( ( availableHeight + noteGap ) / ( noteSize + noteGap ) ) ,
156+ ) ;
157+ let cols = Math . max (
158+ 1 ,
159+ Math . min ( maxCols , Math . ceil ( notesToPlace . length / maxRows ) ) ,
57160 ) ;
58- const cols = Math . min ( notesToPlace . length , maxCols ) ;
59- const rows = Math . ceil ( notesToPlace . length / cols ) ;
60-
61- const spaceX = Math . max ( 0 , ( secWidth - cols * noteSize ) / ( cols + 1 ) ) ;
62- const spaceY = Math . max ( 0 , ( secHeight - rows * noteSize ) / ( rows + 1 ) ) ;
63-
64- notesToPlace . forEach ( ( note , index ) => {
65- const c = index % cols ;
66- const r = Math . floor ( index / cols ) ;
67- note . position = {
68- x : startX + spaceX + c * ( noteSize + spaceX ) ,
69- y : startY + spaceY + r * ( noteSize + spaceY ) ,
70- } ;
161+ while ( Math . ceil ( notesToPlace . length / cols ) > maxRows && cols < maxCols ) {
162+ cols += 1 ;
163+ }
164+ const gridStartY = startY ;
165+ const rows = Array . from ( { length : Math . ceil ( notesToPlace . length / cols ) } , ( _ , rowIndex ) =>
166+ notesToPlace . slice ( rowIndex * cols , ( rowIndex + 1 ) * cols ) ,
167+ ) ;
168+
169+ rows . forEach ( ( rowNotes , rowIndex ) => {
170+ const rowWidth = rowNotes . length * noteSize + Math . max ( 0 , rowNotes . length - 1 ) * noteGap ;
171+ let rowStartX ;
172+ // if (rowIndex === 0) {
173+ rowStartX = innerLeft + Math . max ( 0 , Math . floor ( ( availableWidth - rowWidth ) / 2 ) ) ;
174+ /* } else {
175+ rowStartX = Math.max(innerLeft, innerRight - rowWidth);
176+ } */
177+
178+ rowNotes . forEach ( ( note , index ) => {
179+ note . position = {
180+ x : rowStartX + index * ( noteSize + noteGap ) ,
181+ y : gridStartY + rowIndex * ( noteSize + noteGap ) ,
182+ } ;
183+ } ) ;
71184 } ) ;
72185 } ) ;
73186}
74187
188+ function getJourneyStepsLayout ( sectionDef , sectionBox , styles = defaultStyles ) {
189+ const resolvedStyles = { ...defaultStyles , ...styles } ;
190+ if ( ! sectionDef || ! sectionDef . journeySteps || ! sectionBox ) {
191+ return null ;
192+ }
193+
194+ const stepCount = 5 ;
195+ const stepWidth = Math . max (
196+ sectionBox . width / stepCount - 2 * resolvedStyles . padding ,
197+ resolvedStyles . stickyNoteSize ,
198+ ) ;
199+ const stepHeight = resolvedStyles . stickyNoteSize ;
200+ const stepY = sectionBox . y + sectionBox . height - stepHeight - ( resolvedStyles . padding * 2 ) ;
201+
202+ const boxes = Array . from ( { length : stepCount } , ( _ , index ) => ( {
203+ x : sectionBox . x + index * ( stepWidth + 2 * resolvedStyles . stickyNoteSpacing ) ,
204+ y : stepY ,
205+ width : stepWidth ,
206+ height : stepHeight ,
207+ } ) ) ;
208+
209+ const arrows = boxes . slice ( 0 , - 1 ) . map ( ( box , index ) => {
210+ const nextBox = boxes [ index + 1 ] ;
211+ const centerY = stepY + stepHeight / 2 ;
212+
213+ return {
214+ x1 : box . x + stepWidth ,
215+ y1 : centerY ,
216+ x2 : nextBox . x ,
217+ y2 : centerY ,
218+ } ;
219+ } ) ;
220+
221+ return { boxes, arrows } ;
222+ }
223+
75224module . exports = {
76225 sanitizeInput,
77226 validateInput,
78227 distributeMissingPositions,
228+ getJourneyStepsLayout,
79229} ;
0 commit comments