sr-npm 3.1.24 → 3.1.26

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.
@@ -119,8 +119,10 @@ async function htmlRichContentConverter(sections,richContentConverterToken) {
119
119
  const richContentObject = {}
120
120
  for (const [sectionTitle, sectionData] of Object.entries(sections)) {
121
121
  if (sectionData.text) {
122
+ // Strip span tags but keep their content , since <span> tags paragraphs are deleted by the converter
123
+ const cleanedHtml = sectionData.text.replace(/<\/?span[^>]*>/gi, '');
122
124
  const raw = JSON.stringify({
123
- content: sectionData.text,
125
+ content: cleanedHtml,
124
126
  });
125
127
  const requestOptions = {
126
128
  method: 'post',
@@ -137,7 +139,10 @@ async function htmlRichContentConverter(sections,richContentConverterToken) {
137
139
  );
138
140
  if (response.ok) {
139
141
  const data = await response.json();
140
- const richContentWithSpacing=addSpacingToRichContent(sectionData.text,data.richContent.richContent);
142
+ // Fix list items with nested paragraphs (causes line breaks after bold text)
143
+ const flattenedContent = flattenListItems(data.richContent.richContent);
144
+ // const richContentWithSpacing=addSpacingToRichContent(cleanedHtml,flattenedContent);
145
+ const richContentWithSpacing=addEmptyParagraphsBetweenConsecutive(cleanedHtml,flattenedContent);
141
146
  richContentObject[sectionTitle] = richContentWithSpacing
142
147
  }
143
148
  else {
@@ -148,148 +153,6 @@ async function htmlRichContentConverter(sections,richContentConverterToken) {
148
153
  return richContentObject;
149
154
  }
150
155
 
151
- //Adds empty paragraph nodes between sections in rich content
152
- // to create visual spacing that the Wix RICOS converter strips out
153
- function addSpacingToRichContent(html, richContent) {
154
- if (!richContent || !richContent.nodes) {
155
- return richContent;
156
- }
157
-
158
- // Extract paragraph texts from HTML that end with &#xa0;
159
- const htmlParagraphsWithSpace = [];
160
- // Extract paragraphs with <br> tags
161
- const htmlParagraphsWithBr = new Map(); // text -> array of parts split by <br>
162
-
163
- const pTagRegex = /<p>(.*?)<\/p>/gi;
164
- let match;
165
-
166
- while ((match = pTagRegex.exec(html)) !== null) {
167
- const content = match[1];
168
-
169
- // Check if this paragraph ends with &#xa0; (before closing tags)
170
- if (content.includes('&#xa0;')) {
171
- const textOnly = content.replace(/<[^>]+>/g, '').trim();
172
- htmlParagraphsWithSpace.push(textOnly);
173
- }
174
-
175
- // Check if this paragraph contains <br> tags
176
- if (content.includes('<br>') || content.includes('<br/>') || content.includes('<br />')) {
177
- // Split by <br> tags and extract text parts
178
- const parts = content.split(/<br\s*\/?>/i).map(part =>
179
- part.replace(/<[^>]+>/g, '').trim()
180
- ).filter(part => part.length > 0);
181
-
182
- if (parts.length > 1) {
183
- // Store the parts for this paragraph
184
- const fullText = content.replace(/<[^>]+>/g, '').replace(/\s+/g, '').trim();
185
- htmlParagraphsWithBr.set(fullText, parts);
186
- }
187
- }
188
- }
189
-
190
- const nodes = richContent.nodes;
191
- const newNodes = [];
192
- let nodeIdCounter = 0;
193
-
194
- // Check if a paragraph is bold (has BOLD decoration)
195
- const isBoldParagraph = (node) => {
196
- if (node.type !== 'PARAGRAPH') return false;
197
- const decorations = node.nodes?.[0]?.textData?.decorations || [];
198
- return decorations.some(d => d.type === 'BOLD');
199
- };
200
-
201
- // Check if a paragraph node's text matches one with &#xa0; in HTML
202
- const needsSpacingAfter = (node) => {
203
- if (node.type !== 'PARAGRAPH') return false;
204
-
205
- // Add spacing after bold paragraphs
206
- if (isBoldParagraph(node)) {
207
- return true;
208
- }
209
-
210
- const text = node.nodes?.[0]?.textData?.text || '';
211
- const trimmedText = text.trim();
212
-
213
- // Check if this text matches any HTML paragraph that had &#xa0;
214
- return htmlParagraphsWithSpace.some(htmlText => {
215
- const cleanHtml = htmlText.replace(/&#xa0;/g, ' ').trim();
216
- return trimmedText.includes(cleanHtml) || cleanHtml.includes(trimmedText);
217
- });
218
- };
219
-
220
- // Check if a paragraph contains text that should be split by <br>
221
- const shouldSplitByBr = (node) => {
222
- if (node.type !== 'PARAGRAPH') return null;
223
-
224
- const text = node.nodes?.[0]?.textData?.text || '';
225
- const normalizedText = text.replace(/\s+/g, '').trim();
226
-
227
- // Find matching HTML paragraph with <br>
228
- for (const [htmlText, parts] of htmlParagraphsWithBr.entries()) {
229
- if (normalizedText.includes(htmlText) || htmlText.includes(normalizedText)) {
230
- return parts;
231
- }
232
- }
233
- return null;
234
- };
235
-
236
- // Add spacing after bulleted lists
237
- const isListEnd = (currentNode, nextNode) => {
238
- return currentNode.type === 'BULLETED_LIST' &&
239
- nextNode &&
240
- nextNode.type === 'PARAGRAPH';
241
- };
242
-
243
- for (let i = 0; i < nodes.length; i++) {
244
- const currentNode = nodes[i];
245
- const nextNode = nodes[i + 1];
246
-
247
- // Check if this paragraph should be split by <br>
248
- const brParts = shouldSplitByBr(currentNode);
249
- if (brParts && brParts.length > 1) {
250
- // Split into multiple paragraphs and add spacing between them
251
- const decorations = currentNode.nodes?.[0]?.textData?.decorations || [];
252
-
253
- brParts.forEach((part, idx) => {
254
- newNodes.push({
255
- ...currentNode,
256
- id: `${currentNode.id}_split_${idx}`,
257
- nodes: [{
258
- type: "TEXT",
259
- id: "",
260
- nodes: [],
261
- textData: {
262
- text: part,
263
- decorations: decorations
264
- }
265
- }]
266
- });
267
-
268
- // Add empty paragraph after each split part except the last
269
- if (idx < brParts.length - 1) {
270
- newNodes.push(createEmptyParagraph(`empty_br_${nodeIdCounter++}`));
271
- }
272
- });
273
-
274
- // Add spacing after the split paragraphs if there's a next node
275
- if (nextNode) {
276
- newNodes.push(createEmptyParagraph(`empty_${nodeIdCounter++}`));
277
- }
278
- } else {
279
- newNodes.push(currentNode);
280
-
281
- // Add empty paragraph ONLY after paragraphs with &#xa0; or after lists
282
- if ((needsSpacingAfter(currentNode) || isListEnd(currentNode, nextNode)) && nextNode) {
283
- newNodes.push(createEmptyParagraph(`empty_${nodeIdCounter++}`));
284
- }
285
- }
286
- }
287
-
288
- return {
289
- ...richContent,
290
- nodes: newNodes
291
- };
292
- }
293
156
 
294
157
  function createEmptyParagraph(id) {
295
158
  return {
@@ -314,6 +177,84 @@ function createEmptyParagraph(id) {
314
177
  };
315
178
  }
316
179
 
180
+ // Flattens LIST_ITEM nodes by removing nested PARAGRAPH wrappers
181
+ // Fixes Wix converter bug where bold text + regular text in <li> creates line breaks
182
+ function flattenListItems(richContent) {
183
+ if (!richContent || !richContent.nodes) return richContent;
184
+
185
+ const processNode = (node) => {
186
+ if (node.type === 'BULLETED_LIST' || node.type === 'ORDERED_LIST') {
187
+ return {
188
+ ...node,
189
+ nodes: node.nodes.map(listItem => {
190
+ if (listItem.type !== 'LIST_ITEM') return listItem;
191
+
192
+ // Flatten: extract TEXT nodes from nested PARAGRAPHs
193
+ const flattenedChildren = [];
194
+ for (const child of listItem.nodes) {
195
+ if (child.type === 'PARAGRAPH' && child.nodes) {
196
+ // Pull TEXT nodes out of the PARAGRAPH
197
+ flattenedChildren.push(...child.nodes);
198
+ } else {
199
+ flattenedChildren.push(child);
200
+ }
201
+ }
202
+
203
+ return { ...listItem, nodes: flattenedChildren };
204
+ })
205
+ };
206
+ }
207
+ return node;
208
+ };
209
+
210
+ return {
211
+ ...richContent,
212
+ nodes: richContent.nodes.map(processNode)
213
+ };
214
+ }
215
+
216
+ // Adds empty paragraph nodes between consecutive paragraphs and before lists
217
+ function addEmptyParagraphsBetweenConsecutive(html, richContent) {
218
+ if (!richContent || !richContent.nodes) return richContent;
219
+ const hasConsecutiveParagraphs = /<\/p>\s*<p/i.test(html);
220
+ const hasParagraphBeforeList = /<\/p>\s*<ul/i.test(html);
221
+ const hasParagraphAfterList = /<\/ul>\s*<p/i.test(html);
222
+
223
+ if (!hasConsecutiveParagraphs && !hasParagraphBeforeList && !hasParagraphAfterList) return richContent;
224
+
225
+ const nodes = richContent.nodes;
226
+ const newNodes = [];
227
+ let nodeIdCounter = 0;
228
+
229
+ for (let i = 0; i < nodes.length; i++) {
230
+ const currentNode = nodes[i];
231
+ const nextNode = nodes[i + 1];
232
+
233
+ newNodes.push(currentNode);
234
+
235
+ if (currentNode.type === 'PARAGRAPH' && nextNode) {
236
+ // Add empty paragraph between consecutive paragraphs
237
+ if (hasConsecutiveParagraphs && nextNode.type === 'PARAGRAPH') {
238
+ newNodes.push(createEmptyParagraph(`empty_consecutive_${nodeIdCounter++}`));
239
+ }
240
+ // Add empty paragraph before list
241
+ if (hasParagraphBeforeList && nextNode.type === 'BULLETED_LIST') {
242
+ newNodes.push(createEmptyParagraph(`empty_before_list_${nodeIdCounter++}`));
243
+ }
244
+ }
245
+
246
+ // Add empty paragraph after list
247
+ if (hasParagraphAfterList && currentNode.type === 'BULLETED_LIST' && nextNode && nextNode.type === 'PARAGRAPH') {
248
+ newNodes.push(createEmptyParagraph(`empty_after_list_${nodeIdCounter++}`));
249
+ }
250
+ }
251
+
252
+ return {
253
+ ...richContent,
254
+ nodes: newNodes
255
+ };
256
+ }
257
+
317
258
 
318
259
 
319
260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sr-npm",
3
- "version": "3.1.24",
3
+ "version": "3.1.26",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,6 +30,7 @@ const countsByFieldId = new Map(); // fieldId -> {valueId: count} map of counts
30
30
  let allfields=[] // all fields in the database
31
31
  let alljobs=[] // all jobs in the database
32
32
  let allvaluesobjects=[] // all values in the database
33
+ let cities=[] // all cities in the database
33
34
  let valueToJobs={} // valueId -> array of jobIds
34
35
  let currentJobs=[] // current jobs that are displayed in the jobs repeater
35
36
  let allsecondarySearchJobs=[] // secondary search results that are displayed in the jobs repeater
@@ -37,6 +38,8 @@ let currentSecondarySearchJobs=[] // current secondary search results that are d
37
38
  let secondarySearchIsFilled=false // whether the secondary search is filled with results
38
39
  let keywordAllJobs; // all jobs that are displayed in the jobs repeater when the keyword is filled
39
40
  let ActivateURLOnchange=true; // whether to activate the url onchange
41
+ let considerAllJobs=false; // whether to consider all jobs or not
42
+
40
43
  const pagination = {
41
44
  pageSize: 10,
42
45
  currentPage: 1,
@@ -81,10 +84,10 @@ async function handleBackAndForth(_$w){
81
84
  }
82
85
 
83
86
  async function clearAll(_$w,urlOnChange=false) {
84
- if(selectedByField.size>0 || _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.SECONDARY_SEARCH_INPUT).value || _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PRIMARY_SEARCH_INPUT).value) {
85
87
 
86
88
  for(const field of allfields) {
87
89
  _$w(`#${FiltersIds[field.title]}CheckBox`).selectedIndices = [];
90
+ _$w(`#${FiltersIds[field.title]}input`).value='';
88
91
  }
89
92
  selectedByField.clear();
90
93
  _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.SECONDARY_SEARCH_INPUT).value='';
@@ -100,7 +103,6 @@ async function clearAll(_$w,urlOnChange=false) {
100
103
  await updateJobsAndNumbersAndFilters(_$w,true);
101
104
  }
102
105
 
103
- }
104
106
  }
105
107
 
106
108
  function handleFilterInMobile(_$w) {
@@ -285,11 +287,16 @@ async function handleParams(_$w,param,values) {
285
287
  let fieldTitle=field.title.toLowerCase().replace(' ', '');
286
288
  fieldTitle==="brands"? fieldTitle="brand":fieldTitle;
287
289
  ActivateURLOnchange=false;
290
+ const previousSelectedSize=selectedByField.size;
288
291
  if (updated.length) {
289
292
  selectedByField.set(fieldId, updated);
293
+
294
+
290
295
  queryParams.add({ [fieldTitle] : updated.map(val=>encodeURIComponent(val)).join(',') });
291
296
  } else {
292
297
  selectedByField.delete(fieldId);
298
+ handleConsiderAllJobs(previousSelectedSize,selectedByField.size);
299
+
293
300
  queryParams.remove([fieldTitle ]);
294
301
  }
295
302
 
@@ -357,7 +364,7 @@ async function loadJobsRepeater(_$w) {
357
364
  async function loadFilters(_$w) {
358
365
  try {
359
366
  // 1) Load all categories (fields)
360
- const cities = await getAllRecords(COLLECTIONS.CITIES);
367
+ cities = await getAllRecords(COLLECTIONS.CITIES);
361
368
  for(const city of cities) {
362
369
  valueToJobs[city._id] = city.jobIds;
363
370
  }
@@ -398,8 +405,12 @@ async function loadJobsRepeater(_$w) {
398
405
  let fieldTitle=field.title.toLowerCase().replace(' ', '');
399
406
  fieldTitle==="brands"? fieldTitle="brand":fieldTitle;
400
407
  ActivateURLOnchange=false;
408
+ const previousSelectedSize=selectedByField.size;
409
+
401
410
  if (selected && selected.length) {
402
411
  selectedByField.set(field._id, selected);
412
+
413
+
403
414
  if(fieldTitle==="brand" || fieldTitle==="storename") {
404
415
  //in this case we need the label not valueid
405
416
  const valueLabels=getValueFromValueId(selected,value);
@@ -410,7 +421,8 @@ async function loadJobsRepeater(_$w) {
410
421
  }
411
422
 
412
423
  } else {
413
- selectedByField.delete(field._id);
424
+ selectedByField.delete(field._id);
425
+ handleConsiderAllJobs(previousSelectedSize,selectedByField.size);
414
426
  queryParams.remove([fieldTitle ]);
415
427
  }
416
428
 
@@ -451,14 +463,39 @@ function getValueFromValueId(valueIds, value) {
451
463
  updateTotalJobsCountText(_$w);
452
464
  }
453
465
 
466
+ function handleConsiderAllJobs(previousSelectedSize,currentSelectedSize) {
467
+ if(previousSelectedSize===2 && currentSelectedSize===1) {
468
+
469
+ considerAllJobs=true;
470
+ }
471
+ else{
472
+ considerAllJobs=false;
473
+ }
474
+ }
475
+
454
476
  function updateOptionsUI(_$w,fieldTitle, fieldId, searchQuery,clearAll=false) {
455
477
  let base = optionsByFieldId.get(fieldId) || [];
456
- const countsMap = countsByFieldId.get(fieldId) || new Map();
478
+ let countsMap=countsByFieldId.get(fieldId) || new Map();
479
+ if(considerAllJobs)
480
+ {
481
+ const selectedFieldId=Array.from( selectedByField.keys() )[0]
482
+ if(selectedFieldId===fieldId) {
483
+ if(selectedFieldId==="Location") {
484
+ countsMap = new Map(cities.map(city=>[city._id, city.count]));
485
+ }
486
+ else{
487
+ const relevantFields=allvaluesobjects.filter(val=>val.customField===selectedFieldId)
488
+ countsMap = new Map(relevantFields.map(val=>[val.valueId, val.count]));
489
+ }
490
+ considerAllJobs=false;
491
+ }
492
+ }
457
493
  if(dontUpdateThisCheckBox===fieldId && !clearAll && selectedByField.has(fieldId) )
458
494
  {
459
495
  dontUpdateThisCheckBox=null;
460
496
  return;
461
497
  }
498
+
462
499
  let filteredbase=[]
463
500
  for (const element of base)
464
501
  {
@@ -486,11 +523,7 @@ function getValueFromValueId(valueIds, value) {
486
523
 
487
524
  // Sort alphabetically by label
488
525
  filtered.sort((a, b) => (a.label || '').localeCompare(b.label || ''));
489
- // Preserve currently selected values that are still visible
490
- // let prevSelected=[]
491
- // clearAll? prevSelected=[]:prevSelected= _$w(`#${FiltersIds[fieldTitle]}CheckBox`).value;
492
526
  const visibleSet = new Set(filtered.map(o => o.value));
493
- //const preserved = prevSelected.filter(v => visibleSet.has(v));
494
527
  if(filtered.length===0) {
495
528
  _$w(`#${FiltersIds[fieldTitle]}MultiBox`).changeState(`${FiltersIds[fieldTitle]}NoResults`);
496
529
  }
@@ -559,11 +592,14 @@ function getValueFromValueId(valueIds, value) {
559
592
  _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.paginationTotalCountText).text = secondarySearchIsFilled? Math.ceil(currentSecondarySearchJobs.length/pagination.pageSize).toString():Math.ceil(currentJobs.length/pagination.pageSize).toString();
560
593
  if(jobsFirstPage.length===0) {
561
594
  await _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.JOBS_MULTI_STATE_BOX).changeState("noJobs");
595
+ _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.paginationCurrentText).text = "0";
596
+ pagination.currentPage=0;
562
597
  }
563
598
  else{
564
599
  await _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.JOBS_MULTI_STATE_BOX).changeState("searchResult");
600
+ pagination.currentPage=1;
565
601
  }
566
- pagination.currentPage=1;
602
+
567
603
  handlePaginationButtons(_$w);
568
604
  }
569
605
 
@@ -571,7 +607,7 @@ function handlePaginationButtons(_$w)
571
607
  {
572
608
  handlePageUrlParam();
573
609
 
574
- pagination.currentPage===1? _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PAGE_BUTTON_PREVIOUS).disable():_$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PAGE_BUTTON_PREVIOUS).enable();
610
+ pagination.currentPage===1 || pagination.currentPage===0? _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PAGE_BUTTON_PREVIOUS).disable():_$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PAGE_BUTTON_PREVIOUS).enable();
575
611
  if(secondarySearchIsFilled) {
576
612
  if(currentSecondarySearchJobs.length===0) {
577
613
  _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.PAGE_BUTTON_NEXT).disable();
@@ -590,9 +626,8 @@ function handlePaginationButtons(_$w)
590
626
 
591
627
  function handlePageUrlParam() {
592
628
  ActivateURLOnchange=false;
593
- if(pagination.currentPage==1)
629
+ if(pagination.currentPage==1 || pagination.currentPage==0)
594
630
  {
595
-
596
631
  queryParams.remove(["page"]);
597
632
  }
598
633
  else{
@@ -613,8 +648,8 @@ async function refreshFacetCounts(_$w,clearAll=false) {
613
648
 
614
649
  function countJobsPerField(jobs) {
615
650
  const fieldIds = Array.from(optionsByFieldId.keys());
651
+
616
652
  const currentJobsIds=jobs.map(job=>job._id);
617
-
618
653
  for (const fieldId of fieldIds) {
619
654
  let currentoptions = optionsByFieldId.get(fieldId)
620
655
  let counter=new Map();
@@ -672,6 +707,8 @@ async function secondarySearch(_$w,query) {
672
707
 
673
708
  if(jobsFirstPage.length===0) {
674
709
  await _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.JOBS_MULTI_STATE_BOX).changeState("noJobs");
710
+ _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.paginationCurrentText).text = "0";
711
+ pagination.currentPage=0;
675
712
  }
676
713
  else{
677
714
  await _$w(CAREERS_MULTI_BOXES_PAGE_CONSTS.JOBS_MULTI_STATE_BOX).changeState("searchResult");
@@ -698,6 +735,7 @@ async function secondarySearch(_$w,query) {
698
735
  }
699
736
 
700
737
 
738
+
701
739
  module.exports = {
702
740
  careersMultiBoxesPageOnReady,
703
741
  secondarySearch
@@ -78,13 +78,7 @@ function getFieldByTitle(title,allFields) {
78
78
 
79
79
  function getCorrectOption(value,options,param) {
80
80
  const standardizedValue = normalizeString(value.toLowerCase())
81
- if(param==="employmenttype" || param==="Employment Type" || param==="Store Name") //employmenttype have a problematic value, added Employment Type for updateOptionsUI fuinction, added Store Name because Store name and location have for example Blenheim
82
- {
83
- //option.value is the id,
84
- return options.find(option=>normalizeString(option.value.toLowerCase())===standardizedValue);
85
- }
86
- //option.label is what we see live in the UI
87
- return options.find(option=>normalizeString(option.label.toLowerCase())===standardizedValue);
81
+ return options.find(option=>normalizeString(option.value.toLowerCase())===standardizedValue || normalizeString(option.label.toLowerCase())===standardizedValue);
88
82
  }
89
83
 
90
84
  function getOptionIndexFromCheckBox(options,value) {