@salesforcedevs/docs-components 1.3.344 → 1.3.345-refactor-tab-alpha1

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/lwc.config.json CHANGED
@@ -13,11 +13,13 @@
13
13
  "doc/contentLayout",
14
14
  "doc/contentMedia",
15
15
  "doc/docXmlContent",
16
+ "doc/lwcContentLayout",
16
17
  "doc/header",
17
18
  "doc/heading",
18
19
  "doc/headingAnchor",
19
20
  "doc/overview",
20
21
  "doc/phase",
22
+ "doc/specificationContent",
21
23
  "doc/versionPicker",
22
24
  "doc/xmlContent",
23
25
  "docUtils/utils"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.3.344",
3
+ "version": "1.3.345-refactor-tab-alpha1",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -24,5 +24,5 @@
24
24
  "@types/lodash.orderby": "4.6.9",
25
25
  "@types/lodash.uniqby": "4.7.9"
26
26
  },
27
- "gitHead": "189d356c6cb2b3cca66b7728a288036c6850421a"
27
+ "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad"
28
28
  }
@@ -1,131 +1 @@
1
- :host {
2
- --dx-c-content-vertical-spacing: var(--dx-g-spacing-lg);
3
- --dx-c-content-sidebar-sticky-top: calc(
4
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
- );
6
- --dx-c-sidebar-height: calc(
7
- 100vh -
8
- calc(
9
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
10
- )
11
- );
12
- --dx-c-content-scroll-margin-top: calc(
13
- var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
14
- var(--dx-g-spacing-2xl)
15
- );
16
-
17
- display: block;
18
- }
19
-
20
- doc-breadcrumbs {
21
- --dx-c-popover-z-index: 5;
22
-
23
- display: block;
24
- margin-bottom: var(--dx-g-spacing-2xl);
25
- }
26
-
27
- dx-sidebar,
28
- dx-sidebar-old {
29
- --dx-c-sidebar-vertical-padding: var(--dx-g-spacing-md);
30
-
31
- z-index: calc(var(--dx-g-z-index-100) + 5);
32
- }
33
-
34
- dx-toc {
35
- --dx-c-toc-width: unset;
36
-
37
- height: calc(100% - var(--dx-c-content-vertical-spacing) * 2);
38
- margin: var(--dx-c-content-vertical-spacing) 0;
39
- overflow-y: auto;
40
- }
41
-
42
- dx-sidebar,
43
- dx-toc {
44
- display: block;
45
- }
46
-
47
- /* offset page jump link due to fixed header */
48
- ::slotted(doc-heading) {
49
- scroll-margin-top: var(--dx-c-content-scroll-margin-top);
50
- }
51
-
52
- .content {
53
- display: flex;
54
- position: relative;
55
- }
56
-
57
- .content-body-doc-phase-container {
58
- flex: 1;
59
- border-left: 1px solid var(--dx-g-gray-90);
60
- }
61
-
62
- .content-body-container {
63
- display: flex;
64
- flex-direction: row;
65
- justify-content: center;
66
- max-width: var(--dx-g-doc-content-max-width);
67
-
68
- /* Derived this manually by substracting (topHeader, doc header, banner and the content). */
69
- min-height: 62vh;
70
- margin: auto;
71
- padding: 0 var(--dx-g-global-header-padding-horizontal);
72
- margin-bottom: calc(2 * (var(--dx-g-spacing-5xl) + 4px));
73
- }
74
-
75
- .content-body {
76
- margin: var(--dx-g-spacing-md) 0 0;
77
- max-width: 900px;
78
- flex: 1;
79
- width: 0;
80
- }
81
-
82
- .is-sticky {
83
- align-self: flex-start;
84
- position: sticky;
85
- top: var(--dx-c-content-sidebar-sticky-top);
86
- }
87
-
88
- .right-nav-bar {
89
- margin-left: var(--dx-g-spacing-2xl);
90
- }
91
-
92
- @media screen and (max-width: 1024px) {
93
- .right-nav-bar {
94
- display: none;
95
- }
96
- }
97
-
98
- @media screen and (max-width: 800px) {
99
- .content-body {
100
- margin-top: var(--dx-c-content-vertical-spacing);
101
- }
102
-
103
- .content-body-doc-phase-container {
104
- border-left: 0;
105
- }
106
- }
107
-
108
- @media screen and (max-width: 768px) {
109
- .is-sticky {
110
- width: 100%;
111
- }
112
-
113
- .content {
114
- flex-direction: column;
115
- }
116
-
117
- .content-body-container {
118
- padding-right: 0;
119
- overflow-x: auto;
120
- margin-bottom: calc(var(--dx-g-spacing-5xl) + 4px);
121
- }
122
-
123
- .left-nav-bar {
124
- height: unset;
125
- }
126
-
127
- .content-body {
128
- margin-left: var(--dx-g-spacing-mlg, 20px);
129
- margin-right: var(--dx-g-spacing-mlg, 20px);
130
- }
131
- }
1
+ @import "docHelpers/contentLayoutStyle";
@@ -11,7 +11,7 @@ type AnchorMap = { [key: string]: { intersect: boolean; id: string } };
11
11
  declare const Sprig: (eventType: string, eventNme: string) => void;
12
12
 
13
13
  const TOC_HEADER_TAG = "doc-heading";
14
- const RNB_BY_TAB = "docs-tab";
14
+
15
15
  const HIGHLIGHTABLE_SELECTOR = [
16
16
  "p",
17
17
  "h1",
@@ -83,87 +83,26 @@ export default class ContentLayout extends LightningElement {
83
83
  }
84
84
 
85
85
  @track
86
- private _sidebarContent: unknown;
86
+ protected _sidebarContent: unknown;
87
87
 
88
- private _breadcrumbs = null;
88
+ protected _breadcrumbs = null;
89
89
 
90
90
  @track
91
- private _tocOptions!: Array<unknown>;
91
+ protected _tocOptions!: Array<unknown>;
92
92
 
93
- private tocOptionIdsSet = new Set();
94
- private anchoredElements: AnchorMap = {};
95
- private lastScrollPosition!: number;
96
- private observer?: IntersectionObserver;
97
- private hasRendered: boolean = false;
98
- private contentLoaded: boolean = false;
99
- private sidebarOpen: boolean = false;
100
- private rnbByTab: boolean = false;
93
+ protected tocOptionIdsSet = new Set();
94
+ protected anchoredElements: AnchorMap = {};
95
+ protected lastScrollPosition!: number;
96
+ protected observer?: IntersectionObserver;
97
+ protected hasRendered: boolean = false;
98
+ protected contentLoaded: boolean = false;
99
+ protected sidebarOpen: boolean = false;
101
100
 
102
101
  get shouldDisplayFeedback() {
103
102
  return this.contentLoaded && typeof Sprig !== "undefined";
104
103
  }
105
104
 
106
- private setRNBByTab() {
107
- const tabPanelListItem: any = this.getTabPanelList();
108
- this.rnbByTab = tabPanelListItem?.id === RNB_BY_TAB ? true : false;
109
- }
110
-
111
- get showTabBasedRNB() {
112
- return this.rnbByTab;
113
- }
114
-
115
- onTabChanged = () => {
116
- this.updateRNB();
117
- };
118
-
119
- private getTabPanelList() {
120
- return document.querySelector("dx-tab-panel-list");
121
- }
122
-
123
- updateRNB = () => {
124
- const headingElements = this.getHeadingElements();
125
- headingElements.forEach((headingElement: any) => {
126
- // Sometimes elements hash and header is not being set when slot content is wrapped with div
127
- headingElement.hash = headingElement.attributes.hash?.nodeValue;
128
- headingElement.header = headingElement.attributes.header?.nodeValue;
129
- });
130
- this.updateTocItems(headingElements);
131
- };
132
-
133
- private getHeadingElements() {
134
- let headingElements = document.querySelectorAll(TOC_HEADER_TAG);
135
- if (this.showTabBasedRNB) {
136
- const tabPanelListItem: any = this.getTabPanelList();
137
- const tabPanels =
138
- tabPanelListItem?.querySelectorAll("dx-tab-panel");
139
- for (const tabPanelItem of tabPanels) {
140
- if (tabPanelItem.active) {
141
- headingElements =
142
- tabPanelItem.querySelectorAll(TOC_HEADER_TAG);
143
- break;
144
- }
145
- }
146
- }
147
- return headingElements;
148
- }
149
-
150
- private updateURL() {
151
- const tabs = this.getAllTabs();
152
- const selectedTabId = this.getSelectedTabId();
153
- tabs.forEach((tab: any) => {
154
- if (tab.getAttribute("aria-selected") === "true") {
155
- const tabID = tab.getAttribute("aria-label");
156
- const url = new URL(window.location.href);
157
- if (selectedTabId !== tabID) {
158
- url.searchParams.set("type", tabID);
159
- url.hash = "";
160
- window.history.pushState({}, "", url.toString());
161
- }
162
- }
163
- });
164
- }
165
-
166
- private searchSyncer = new SearchSyncer({
105
+ protected searchSyncer = new SearchSyncer({
167
106
  callbacks: {
168
107
  onSearchChange: (nextSearchString: string): void => {
169
108
  this.dispatchHighlightChange(
@@ -177,11 +116,11 @@ export default class ContentLayout extends LightningElement {
177
116
  shouldStopPropagation: true,
178
117
  target: window
179
118
  });
180
- private tocValue?: string = undefined;
119
+ protected tocValue?: string = undefined;
181
120
  // eslint-disable-next-line no-undef
182
- private observerTimerId?: NodeJS.Timeout;
183
- private didScrollToSelectedHash = false;
184
- private _scrollInterval = 0;
121
+ protected observerTimerId?: NodeJS.Timeout;
122
+ protected didScrollToSelectedHash = false;
123
+ protected _scrollInterval = 0;
185
124
 
186
125
  get showToc(): boolean {
187
126
  return this.tocOptions && this.tocOptions.length > 0;
@@ -198,13 +137,6 @@ export default class ContentLayout extends LightningElement {
198
137
  );
199
138
  }
200
139
 
201
- // This event gets triggered when navigating back/forward
202
- handlePopState = (): void => {
203
- if (this.showTabBasedRNB) {
204
- this.restoreTabSelection();
205
- }
206
- };
207
-
208
140
  connectedCallback(): void {
209
141
  const hasParentHighlightListener = closest(
210
142
  "doc-xml-content",
@@ -217,45 +149,6 @@ export default class ContentLayout extends LightningElement {
217
149
  );
218
150
  this.searchSyncer.init();
219
151
  }
220
- window.addEventListener("popstate", this.handlePopState);
221
- }
222
-
223
- private getSelectedTabId() {
224
- const urlParams = new URLSearchParams(window.location.search);
225
- const selectedTabId = urlParams.get("type");
226
- return selectedTabId;
227
- }
228
-
229
- private restoreTabSelection() {
230
- requestAnimationFrame(() => {
231
- const selectedTabId = this.getSelectedTabId();
232
- if (selectedTabId) {
233
- this.selectTabById(selectedTabId);
234
- }
235
- });
236
- }
237
-
238
- private getAllTabs(): any[] {
239
- const tabPanelListItem: any = this.getTabPanelList();
240
- if (tabPanelListItem?.shadowRoot) {
241
- return Array.from(
242
- tabPanelListItem.shadowRoot.querySelectorAll(
243
- "dx-tab-panel-item"
244
- )
245
- ).map((tabPanelItem: any) =>
246
- tabPanelItem.shadowRoot.querySelector("button")
247
- );
248
- }
249
- return [];
250
- }
251
-
252
- private selectTabById(tabId: string) {
253
- const tabs = this.getAllTabs();
254
- tabs.forEach((tab: any) => {
255
- if (tab.getAttribute("aria-label") === tabId) {
256
- tab.click();
257
- }
258
- });
259
152
  }
260
153
 
261
154
  renderedCallback(): void {
@@ -275,11 +168,6 @@ export default class ContentLayout extends LightningElement {
275
168
 
276
169
  if (!this.hasRendered) {
277
170
  this.hasRendered = true;
278
- this.setRNBByTab();
279
- if (this.showTabBasedRNB) {
280
- window.addEventListener("tabchanged", this.onTabChanged);
281
- this.restoreTabSelection();
282
- }
283
171
  this.restoreScroll();
284
172
  }
285
173
  }
@@ -292,8 +180,6 @@ export default class ContentLayout extends LightningElement {
292
180
  );
293
181
  window.removeEventListener("scroll", this.adjustNavPosition);
294
182
  window.removeEventListener("resize", this.adjustNavPosition);
295
- window.removeEventListener("tabchanged", this.onTabChanged);
296
- window.removeEventListener("popstate", this.handlePopState);
297
183
  this.searchSyncer.dispose();
298
184
  this.clearRenderObserverTimer();
299
185
 
@@ -439,12 +325,7 @@ export default class ContentLayout extends LightningElement {
439
325
  );
440
326
 
441
327
  // Note: We are doing document.querySelectorAll as a quick fix as we are not getting heading elements reference this.querySelectorAll
442
- const headingElements = this.getHeadingElements();
443
-
444
- // We only need to update URL in case of /docs and ignore if tabs are used anywhere else in DSC
445
- if (this.showTabBasedRNB) {
446
- this.updateURL();
447
- }
328
+ const headingElements = document.querySelectorAll(TOC_HEADER_TAG);
448
329
 
449
330
  for (const headingElement of headingElements as any) {
450
331
  // Add headingElements to intersectionObserver for highlighting respective RNB item when user scroll
@@ -456,45 +337,57 @@ export default class ContentLayout extends LightningElement {
456
337
  this.observer.observe(headingElement);
457
338
  }
458
339
 
459
- this.contentLoaded = true;
460
-
461
340
  if (!this.didScrollToSelectedHash) {
462
341
  this.didScrollToSelectedHash = true;
463
342
  this.scrollToHash(headingElements);
464
343
  }
465
344
  };
466
345
 
467
- onSlotChange(): void {
468
- this.updateRNB();
469
- }
346
+ onSlotChange(event: Event): void {
347
+ const slotElements = (
348
+ event.target as HTMLSlotElement
349
+ ).assignedElements();
350
+
351
+ if (slotElements.length) {
352
+ this.contentLoaded = true;
353
+ const slotContentElement = slotElements[0];
354
+ const headingElements =
355
+ slotContentElement.ownerDocument?.getElementsByTagName(
356
+ TOC_HEADER_TAG
357
+ );
470
358
 
471
- // eslint-disable-next-line no-undef
472
- private updateTocItems(headingElements: NodeListOf<Element>): void {
473
- const tocOptions = [];
359
+ for (const headingElement of headingElements as any) {
360
+ // Sometimes elements hash and header is not being set when slot content is wrapped with div
361
+ headingElement.hash = headingElement.attributes.hash?.nodeValue;
362
+ headingElement.header =
363
+ headingElement.attributes.header?.nodeValue;
364
+ }
474
365
 
475
- for (const headingElement of headingElements as any) {
476
- headingElement.id = headingElement.hash;
477
-
478
- // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
479
- const headingAriaLevel =
480
- headingElement.attributes["aria-level"]?.nodeValue || "2";
481
- const isH2 = headingAriaLevel === "2";
482
-
483
- if (isH2) {
484
- const tocItem = {
485
- anchor: `#${headingElement.hash}`,
486
- id: headingElement.id,
487
- label: headingElement.header
488
- };
489
- tocOptions.push(tocItem);
490
- this.tocOptionIdsSet.add(headingElement.id);
366
+ const tocOptions = [];
367
+ for (const headingElement of headingElements as any) {
368
+ headingElement.id = headingElement.hash;
369
+
370
+ // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
371
+ const headingAriaLevel =
372
+ headingElement.attributes["aria-level"]?.nodeValue || "2";
373
+ const isH2 = headingAriaLevel === "2";
374
+
375
+ if (isH2) {
376
+ const tocItem = {
377
+ anchor: `#${headingElement.hash}`,
378
+ id: headingElement.id,
379
+ label: headingElement.header
380
+ };
381
+ tocOptions.push(tocItem);
382
+ this.tocOptionIdsSet.add(headingElement.id);
383
+ }
491
384
  }
492
- }
493
385
 
494
- this._tocOptions = tocOptions;
386
+ this._tocOptions = tocOptions;
387
+ }
495
388
  }
496
389
 
497
- private disconnectObserver(): void {
390
+ protected disconnectObserver(): void {
498
391
  if (this.observer) {
499
392
  this.observer.disconnect();
500
393
  this.observer = undefined;
@@ -502,7 +395,7 @@ export default class ContentLayout extends LightningElement {
502
395
  }
503
396
 
504
397
  // eslint-disable-next-line no-undef
505
- private scrollToHash(headingElements: NodeListOf<Element>): void {
398
+ protected scrollToHash(headingElements: NodeListOf<Element>): void {
506
399
  let { hash } = window.location;
507
400
  if (hash) {
508
401
  hash = hash.substr(1);
@@ -539,7 +432,7 @@ export default class ContentLayout extends LightningElement {
539
432
  }
540
433
  }
541
434
 
542
- private scrollIntoViewWithOffset(
435
+ protected scrollIntoViewWithOffset(
543
436
  headingElement: HTMLElement,
544
437
  offset: number
545
438
  ) {
@@ -552,7 +445,7 @@ export default class ContentLayout extends LightningElement {
552
445
  });
553
446
  }
554
447
 
555
- private calculateActualSection(): void {
448
+ protected calculateActualSection(): void {
556
449
  const currentScrollPosition = document.documentElement.scrollTop;
557
450
  const id = Object.keys(this.anchoredElements).find(
558
451
  (_id) => this.anchoredElements[_id].intersect
@@ -567,21 +460,21 @@ export default class ContentLayout extends LightningElement {
567
460
  this.lastScrollPosition = currentScrollPosition;
568
461
  }
569
462
 
570
- private calculatePreviousElementId(): string | undefined {
463
+ protected calculatePreviousElementId(): string | undefined {
571
464
  const keys = Object.keys(this.anchoredElements);
572
465
  const currentIndex = keys.findIndex((id) => this.tocValue === id);
573
466
 
574
467
  return currentIndex > 0 ? keys[currentIndex - 1] : undefined;
575
468
  }
576
469
 
577
- private assignElementId(id: string | undefined): void {
470
+ protected assignElementId(id: string | undefined): void {
578
471
  // Change toc(RNB) highlight only for H2
579
472
  if (this.tocOptionIdsSet.has(id)) {
580
473
  this.tocValue = id;
581
474
  }
582
475
  }
583
476
 
584
- private dispatchHighlightChange(term: string): void {
477
+ protected dispatchHighlightChange(term: string): void {
585
478
  this.dispatchEvent(
586
479
  new CustomEvent("highlightedtermchange", {
587
480
  detail: term,
@@ -591,14 +484,14 @@ export default class ContentLayout extends LightningElement {
591
484
  );
592
485
  }
593
486
 
594
- private updateHighlightsAndSearch(nextSearchString: string): void {
487
+ protected updateHighlightsAndSearch(nextSearchString: string): void {
595
488
  const nextSearchParam =
596
489
  new URLSearchParams(nextSearchString).get("q") || "";
597
490
  this.setSidebarInputValue(nextSearchParam);
598
491
  this.dispatchHighlightChange(nextSearchParam);
599
492
  }
600
493
 
601
- private onToggleSidebar(e: CustomEvent): void {
494
+ protected onToggleSidebar(e: CustomEvent): void {
602
495
  this.sidebarOpen = e.detail.open;
603
496
 
604
497
  // eslint-disable-next-line @lwc/lwc/no-document-query
@@ -0,0 +1 @@
1
+ @import "docHelpers/contentLayoutStyle";
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div class="content">
3
+ <template lwc:if={useOldSidebar}>
4
+ <dx-sidebar-old
5
+ class="is-sticky left-nav-bar"
6
+ trees={sidebarContent}
7
+ value={sidebarValue}
8
+ header={sidebarHeader}
9
+ ontogglesidebar={onToggleSidebar}
10
+ languages={languages}
11
+ language={language}
12
+ bail-href={bailHref}
13
+ bail-label={bailLabel}
14
+ >
15
+ <slot name="sidebar-header" slot="version-picker"></slot>
16
+ </dx-sidebar-old>
17
+ </template>
18
+ <template lwc:else>
19
+ <dx-sidebar
20
+ class="is-sticky left-nav-bar"
21
+ trees={sidebarContent}
22
+ value={sidebarValue}
23
+ header={sidebarHeader}
24
+ coveo-organization-id={coveoOrganizationId}
25
+ coveo-public-access-token={coveoPublicAccessToken}
26
+ coveo-search-hub={coveoSearchHub}
27
+ coveo-advanced-query-config={coveoAdvancedQueryConfig}
28
+ ontogglesidebar={onToggleSidebar}
29
+ languages={languages}
30
+ language={language}
31
+ bail-href={bailHref}
32
+ bail-label={bailLabel}
33
+ >
34
+ <slot name="sidebar-header" slot="version-picker"></slot>
35
+ </dx-sidebar>
36
+ </template>
37
+ <div class="content-body-doc-phase-container">
38
+ <slot name="doc-phase"></slot>
39
+ <slot name="version-banner"></slot>
40
+ <div class="content-body-container">
41
+ <div class="content-body">
42
+ <doc-breadcrumbs
43
+ lwc:if={showBreadcrumbs}
44
+ breadcrumbs={breadcrumbs}
45
+ ></doc-breadcrumbs>
46
+ <slot onslotchange={onSlotChange}></slot>
47
+ <doc-sprig-survey
48
+ lwc:if={shouldDisplayFeedback}
49
+ ></doc-sprig-survey>
50
+ </div>
51
+ <div lwc:if={showToc} class="right-nav-bar is-sticky">
52
+ <dx-toc
53
+ header={tocTitle}
54
+ options={tocOptions}
55
+ value={tocValue}
56
+ ></dx-toc>
57
+ </div>
58
+ </div>
59
+ <div lwc:if={showFooter} class="footer-container">
60
+ <dx-footer variant="no-signup"></dx-footer>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </template>
@@ -0,0 +1,257 @@
1
+ import ContentLayout from "doc/contentLayout";
2
+
3
+ const TOC_HEADER_TAG = "doc-heading";
4
+ const RNB_BY_TAB = "docs-tab";
5
+ const SPECIFICATION_TAB_TITLE = "Specification";
6
+ export const OBSERVER_ATTACH_WAIT_TIME = 500;
7
+
8
+ export default class LwcContentLayout extends ContentLayout {
9
+ private rnbByTab: boolean = false;
10
+
11
+ private setRNBByTab() {
12
+ const tabPanelListItem: any = this.getTabPanelList();
13
+ this.rnbByTab = tabPanelListItem?.id === RNB_BY_TAB ? true : false;
14
+ }
15
+
16
+ get showTabBasedRNB() {
17
+ return this.rnbByTab;
18
+ }
19
+
20
+ onRNBClick = (event: CustomEvent) => {
21
+ event.stopPropagation();
22
+ const currentTab = this.getSelectedTabId();
23
+ if (currentTab === SPECIFICATION_TAB_TITLE) {
24
+ this.didScrollToSelectedHash = false;
25
+ }
26
+ };
27
+
28
+ onTabChanged = () => {
29
+ this.updateRNB();
30
+ };
31
+
32
+ private getTabPanelList() {
33
+ return document.querySelector("dx-tab-panel-list");
34
+ }
35
+
36
+ updateRNB = () => {
37
+ const headingElements = this.getHeadingElements();
38
+ headingElements.forEach((headingElement: any) => {
39
+ // Sometimes elements hash and header is not being set when slot content is wrapped with div
40
+ if (!headingElement.hash) {
41
+ headingElement.hash = headingElement.attributes.hash?.nodeValue;
42
+ }
43
+
44
+ if (!headingElement.header) {
45
+ headingElement.header =
46
+ headingElement.attributes.header?.nodeValue;
47
+ }
48
+ });
49
+ this.updateTocItems(headingElements);
50
+ };
51
+
52
+ private getHeadingElements() {
53
+ let headingElements = document.querySelectorAll(TOC_HEADER_TAG);
54
+ if (this.showTabBasedRNB) {
55
+ const tabPanelListItem: any = this.getTabPanelList();
56
+ const tabPanels =
57
+ tabPanelListItem?.querySelectorAll("dx-tab-panel");
58
+ for (const tabPanelItem of tabPanels) {
59
+ if (tabPanelItem.active) {
60
+ // This is needed for Specification tab content
61
+ const specificationElement = tabPanelItem.querySelector(
62
+ "doc-specification-content"
63
+ );
64
+ if (specificationElement) {
65
+ headingElements =
66
+ specificationElement.shadowRoot.querySelectorAll(
67
+ TOC_HEADER_TAG
68
+ );
69
+ } else {
70
+ headingElements =
71
+ tabPanelItem.querySelectorAll(TOC_HEADER_TAG);
72
+ }
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ return headingElements;
78
+ }
79
+
80
+ private updateURL() {
81
+ const tabs = this.getAllTabs();
82
+ const selectedTabId = this.getSelectedTabId();
83
+ tabs.forEach((tab: any) => {
84
+ if (tab.getAttribute("aria-selected") === "true") {
85
+ const tabID = tab.getAttribute("aria-label");
86
+ const url = new URL(window.location.href);
87
+ if (selectedTabId !== tabID) {
88
+ url.searchParams.set("type", tabID);
89
+ url.hash = "";
90
+ window.history.pushState({}, "", url.toString());
91
+ }
92
+ }
93
+ });
94
+ }
95
+
96
+ // This event gets triggered when navigating back/forward
97
+ handlePopState = (): void => {
98
+ if (this.showTabBasedRNB) {
99
+ this.restoreTabSelection();
100
+ }
101
+ };
102
+
103
+ connectedCallback(): void {
104
+ window.addEventListener("popstate", this.handlePopState);
105
+ }
106
+
107
+ private getSelectedTabId() {
108
+ const urlParams = new URLSearchParams(window.location.search);
109
+ const selectedTabId = urlParams.get("type");
110
+ return selectedTabId;
111
+ }
112
+
113
+ private restoreTabSelection() {
114
+ requestAnimationFrame(() => {
115
+ const selectedTabId = this.getSelectedTabId();
116
+ if (selectedTabId) {
117
+ this.selectTabById(selectedTabId);
118
+ }
119
+ });
120
+ }
121
+
122
+ private getAllTabs(): any[] {
123
+ const tabPanelListItem: any = this.getTabPanelList();
124
+ if (tabPanelListItem?.shadowRoot) {
125
+ return Array.from(
126
+ tabPanelListItem.shadowRoot.querySelectorAll(
127
+ "dx-tab-panel-item"
128
+ )
129
+ ).map((tabPanelItem: any) =>
130
+ tabPanelItem.shadowRoot.querySelector("button")
131
+ );
132
+ }
133
+ return [];
134
+ }
135
+
136
+ private selectTabById(tabId: string) {
137
+ const tabs = this.getAllTabs();
138
+ tabs.forEach((tab: any) => {
139
+ if (tab.getAttribute("aria-label") === tabId) {
140
+ tab.click();
141
+ }
142
+ });
143
+ }
144
+
145
+ renderedCallback(): void {
146
+ if (!this.hasRendered) {
147
+ this.hasRendered = true;
148
+ this.setRNBByTab();
149
+ if (this.showTabBasedRNB) {
150
+ window.addEventListener("tabchanged", this.onTabChanged);
151
+ window.addEventListener(
152
+ "specificationdatarendered",
153
+ this.onTabChanged
154
+ );
155
+ window.addEventListener("selectedcontent", (event) =>
156
+ this.onRNBClick(event as CustomEvent)
157
+ );
158
+ this.restoreTabSelection();
159
+ }
160
+ this.restoreScroll();
161
+ }
162
+ }
163
+
164
+ disconnectedCallback(): void {
165
+ if (this.showTabBasedRNB) {
166
+ window.removeEventListener("tabchanged", this.onTabChanged);
167
+ window.removeEventListener(
168
+ "specificationdatarendered",
169
+ this.onTabChanged
170
+ );
171
+ window.removeEventListener("selectedcontent", (event) =>
172
+ this.onRNBClick(event as CustomEvent)
173
+ );
174
+ window.removeEventListener("popstate", this.handlePopState);
175
+ }
176
+ }
177
+
178
+ attachInteractionObserver = (): void => {
179
+ if (!this.enableSlotChange) {
180
+ return;
181
+ }
182
+ this.disconnectObserver();
183
+
184
+ const globalNavOffset = `-${getComputedStyle(
185
+ document.documentElement
186
+ ).getPropertyValue("--dx-g-doc-header-main-nav-height")}`;
187
+
188
+ this.observer = new IntersectionObserver(
189
+ (entries) => {
190
+ entries.forEach(
191
+ (entry) =>
192
+ (this.anchoredElements[
193
+ entry.target.getAttribute("id")!
194
+ ].intersect = entry.isIntersecting)
195
+ );
196
+ this.calculateActualSection();
197
+ },
198
+ {
199
+ rootMargin: globalNavOffset.trim()
200
+ }
201
+ );
202
+
203
+ // Note: We are doing document.querySelectorAll as a quick fix as we are not getting heading elements reference this.querySelectorAll
204
+ const headingElements = this.getHeadingElements();
205
+
206
+ // We only need to update URL in case of /docs and ignore if tabs are used anywhere else in DSC
207
+ if (this.showTabBasedRNB) {
208
+ this.updateURL();
209
+ }
210
+
211
+ for (const headingElement of headingElements as any) {
212
+ // Add headingElements to intersectionObserver for highlighting respective RNB item when user scroll
213
+ const id = headingElement.getAttribute("id")!;
214
+ this.anchoredElements[id] = {
215
+ id,
216
+ intersect: false
217
+ };
218
+ this.observer.observe(headingElement);
219
+ }
220
+
221
+ if (!this.didScrollToSelectedHash) {
222
+ this.didScrollToSelectedHash = true;
223
+ this.scrollToHash(headingElements);
224
+ }
225
+ };
226
+
227
+ onSlotChange(): void {
228
+ this.updateRNB();
229
+ this.contentLoaded = true;
230
+ }
231
+
232
+ // eslint-disable-next-line no-undef
233
+ private updateTocItems(headingElements: NodeListOf<Element>): void {
234
+ const tocOptions = [];
235
+
236
+ for (const headingElement of headingElements as any) {
237
+ headingElement.id = headingElement.hash;
238
+
239
+ // Update tocOptions from anchorTags only for H2, consider default as 2 as per component
240
+ const headingAriaLevel =
241
+ headingElement.attributes["aria-level"]?.nodeValue || "2";
242
+ const isH2 = headingAriaLevel === "2";
243
+
244
+ if (isH2) {
245
+ const tocItem = {
246
+ anchor: `#${headingElement.hash}`,
247
+ id: headingElement.id,
248
+ label: headingElement.header
249
+ };
250
+ tocOptions.push(tocItem);
251
+ this.tocOptionIdsSet.add(headingElement.id);
252
+ }
253
+ }
254
+
255
+ this._tocOptions = tocOptions;
256
+ }
257
+ }
@@ -0,0 +1,3 @@
1
+ @import "dxHelpers/reset";
2
+ @import "dxHelpers/text";
3
+ @import "dxHelpers/table";
@@ -0,0 +1,145 @@
1
+ <template>
2
+ <div class="specification-properties">
3
+ <template if:true={hasAttributes}>
4
+ <doc-heading
5
+ header="Attributes"
6
+ hash="attributes"
7
+ aria-level="2"
8
+ id="attributes"
9
+ ></doc-heading>
10
+ <table>
11
+ <thead>
12
+ <tr>
13
+ <th>Name</th>
14
+ <th>Description</th>
15
+ <th>Type</th>
16
+ <th>Default</th>
17
+ <th>Required</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <template for:each={attributes} for:item="attribute">
22
+ <tr key={attribute.name}>
23
+ <td>{attribute.nameInKebabCase}</td>
24
+ <td>{attribute.description}</td>
25
+ <td>{attribute.type}</td>
26
+ <td>{attribute.default}</td>
27
+ <td>
28
+ <template lwc:if={attribute.required}>
29
+ <dx-icon
30
+ symbol="success"
31
+ size="large"
32
+ color="green-vibrant-65"
33
+ ></dx-icon>
34
+ </template>
35
+ </td>
36
+ </tr>
37
+ </template>
38
+ </tbody>
39
+ </table>
40
+ </template>
41
+
42
+ <template if:true={hasMethods}>
43
+ <doc-heading
44
+ header="Methods"
45
+ hash="methods"
46
+ aria-level="2"
47
+ id="methods"
48
+ ></doc-heading>
49
+ <table>
50
+ <thead>
51
+ <tr>
52
+ <th>Name</th>
53
+ <th>Description</th>
54
+ <th>Argument Name</th>
55
+ <th>Argument Type</th>
56
+ <th>Argument Description</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ <template for:each={processedMethods} for:item="method">
61
+ <template if:true={method.firstArgument}>
62
+ <tr key={method.name}>
63
+ <td rowspan={method.arguments.length}>
64
+ {method.nameInKebabCase}
65
+ </td>
66
+ <td rowspan={method.arguments.length}>
67
+ {method.description}
68
+ </td>
69
+ <td>{method.firstArgument.name}</td>
70
+ <td>{method.firstArgument.type}</td>
71
+ <td>{method.firstArgument.description}</td>
72
+ </tr>
73
+ <template
74
+ for:each={method.remainingArguments}
75
+ for:item="arg"
76
+ >
77
+ <tr key={arg.name}>
78
+ <td>{arg.name}</td>
79
+ <td>{arg.type}</td>
80
+ <td>{arg.description}</td>
81
+ </tr>
82
+ </template>
83
+ </template>
84
+ <template if:false={method.firstArgument}>
85
+ <tr key={method.name}>
86
+ <td>{method.nameInKebabCase}</td>
87
+ <td>{method.description}</td>
88
+ <td colspan="3"></td>
89
+ </tr>
90
+ </template>
91
+ </template>
92
+ </tbody>
93
+ </table>
94
+ </template>
95
+
96
+ <template if:true={hasSlots}>
97
+ <doc-heading
98
+ header="Slots"
99
+ hash="slots"
100
+ aria-level="2"
101
+ id="slots"
102
+ ></doc-heading>
103
+ <table>
104
+ <thead>
105
+ <tr>
106
+ <th>Name</th>
107
+ <th>Description</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <template for:each={slots} for:item="slot">
112
+ <tr key={slot.name}>
113
+ <td>{slot.nameInKebabCase}</td>
114
+ <td>{slot.description}</td>
115
+ </tr>
116
+ </template>
117
+ </tbody>
118
+ </table>
119
+ </template>
120
+
121
+ <template if:true={hasEvents}>
122
+ <doc-heading
123
+ header="Events"
124
+ hash="events"
125
+ aria-level="2"
126
+ ></doc-heading>
127
+ <table>
128
+ <thead>
129
+ <tr>
130
+ <th>Name</th>
131
+ <th>Description</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ <template for:each={events} for:item="event">
136
+ <tr key={event.name}>
137
+ <td>{event.nameInKebabCase}</td>
138
+ <td>{event.description}</td>
139
+ </tr>
140
+ </template>
141
+ </tbody>
142
+ </table>
143
+ </template>
144
+ </div>
145
+ </template>
@@ -0,0 +1,112 @@
1
+ import { LightningElement, track, api } from "lwc";
2
+ import { Method, Specification } from "typings/custom";
3
+ import debounce from "debounce";
4
+
5
+ export default class SpecificationContent extends LightningElement {
6
+ @track data: any;
7
+ // TODO: added these default values for testing, will drop this once the backend is ready.
8
+ @api component: string = "button";
9
+ @api model: string = "lwc";
10
+ @api namespace: string = "lightning";
11
+
12
+ /* TODO: The actual URL is as follows:
13
+ * http://api.salesforce.com/doc-platform/developer/v1/{type}/{sub-type}/{component-name}
14
+ * Until the API integration is ready, we will go ahead with mocked-router-url.
15
+ */
16
+ @api apiBaseUrl: string =
17
+ "https://cx-mock-router-internal-07a18d7b3f61.herokuapp.com";
18
+
19
+ private attributes: Specification[] = [];
20
+ private methods: Method[] = [];
21
+ private slots: Specification[] = [];
22
+ private events: Specification[] = [];
23
+
24
+ /* TODO: For now setting the timeout to 300ms,
25
+ * post integration with CX-Router API will test and change if required.
26
+ */
27
+ private debouncedNotifyDataRendered = debounce(() => {
28
+ this.notifySpecificationDataRendered();
29
+ }, 300);
30
+
31
+ connectedCallback() {
32
+ this.fetchComponentMetadata();
33
+ }
34
+
35
+ async fetchComponentMetadata() {
36
+ const url = `${this.apiBaseUrl}/${this.model}/${this.namespace}/${this.component}`;
37
+
38
+ try {
39
+ const response = await fetch(url);
40
+
41
+ if (!response.ok) {
42
+ // TODO: Will add loader and show error as follow-up
43
+ throw new Error(`Failed to fetch: ${response.statusText}`);
44
+ }
45
+
46
+ const result = await response.json();
47
+ this.data = result;
48
+ ({
49
+ attributes: this.attributes,
50
+ methods: this.methods,
51
+ slots: this.slots,
52
+ events: this.events
53
+ } = this.data);
54
+ } catch (error) {
55
+ this.data = {};
56
+ console.error("fetchComponentMetadata() failed for:" + url);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * This getter is to preprocess the methods for easier rendering in the template.
62
+ * Each method is augmented with additional properties:
63
+ * - `firstArgument`: The first argument (if any).
64
+ * - `remainingArguments`: All other arguments (if any).
65
+ * - `hasArguments`: A boolean indicating whether the method has arguments or not.
66
+ */
67
+ get processedMethods(): Method[] {
68
+ return this.methods.map((method) => {
69
+ const [firstArgument, ...remainingArguments] =
70
+ method.arguments || [];
71
+ return {
72
+ ...method,
73
+ firstArgument,
74
+ remainingArguments,
75
+ hasArguments: method.arguments && method.arguments.length > 0
76
+ };
77
+ });
78
+ }
79
+
80
+ get hasAttributes() {
81
+ return this.attributes?.length > 0;
82
+ }
83
+
84
+ get hasMethods() {
85
+ return this.methods?.length > 0;
86
+ }
87
+
88
+ get hasSlots() {
89
+ return this.slots?.length > 0;
90
+ }
91
+
92
+ get hasEvents() {
93
+ return this.events?.length > 0;
94
+ }
95
+
96
+ renderedCallback(): void {
97
+ if (this.data) {
98
+ this.debouncedNotifyDataRendered();
99
+ }
100
+ }
101
+
102
+ notifySpecificationDataRendered() {
103
+ // Dispatch a custom event to notify the specification tab has rendered.
104
+ this.dispatchEvent(
105
+ new CustomEvent("specificationdatarendered", {
106
+ detail: { name: "doc-specification-content" },
107
+ bubbles: true,
108
+ composed: true
109
+ })
110
+ );
111
+ }
112
+ }
@@ -0,0 +1,131 @@
1
+ :host {
2
+ --dx-c-content-vertical-spacing: var(--dx-g-spacing-lg);
3
+ --dx-c-content-sidebar-sticky-top: calc(
4
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
+ );
6
+ --dx-c-sidebar-height: calc(
7
+ 100vh -
8
+ calc(
9
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
10
+ )
11
+ );
12
+ --dx-c-content-scroll-margin-top: calc(
13
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
14
+ var(--dx-g-spacing-2xl)
15
+ );
16
+
17
+ display: block;
18
+ }
19
+
20
+ doc-breadcrumbs {
21
+ --dx-c-popover-z-index: 5;
22
+
23
+ display: block;
24
+ margin-bottom: var(--dx-g-spacing-2xl);
25
+ }
26
+
27
+ dx-sidebar,
28
+ dx-sidebar-old {
29
+ --dx-c-sidebar-vertical-padding: var(--dx-g-spacing-md);
30
+
31
+ z-index: calc(var(--dx-g-z-index-100) + 5);
32
+ }
33
+
34
+ dx-toc {
35
+ --dx-c-toc-width: unset;
36
+
37
+ height: calc(100% - var(--dx-c-content-vertical-spacing) * 2);
38
+ margin: var(--dx-c-content-vertical-spacing) 0;
39
+ overflow-y: auto;
40
+ }
41
+
42
+ dx-sidebar,
43
+ dx-toc {
44
+ display: block;
45
+ }
46
+
47
+ /* offset page jump link due to fixed header */
48
+ ::slotted(doc-heading) {
49
+ scroll-margin-top: var(--dx-c-content-scroll-margin-top);
50
+ }
51
+
52
+ .content {
53
+ display: flex;
54
+ position: relative;
55
+ }
56
+
57
+ .content-body-doc-phase-container {
58
+ flex: 1;
59
+ border-left: 1px solid var(--dx-g-gray-90);
60
+ }
61
+
62
+ .content-body-container {
63
+ display: flex;
64
+ flex-direction: row;
65
+ justify-content: center;
66
+ max-width: var(--dx-g-doc-content-max-width);
67
+
68
+ /* Derived this manually by substracting (topHeader, doc header, banner and the content). */
69
+ min-height: 62vh;
70
+ margin: auto;
71
+ padding: 0 var(--dx-g-global-header-padding-horizontal);
72
+ margin-bottom: calc(2 * (var(--dx-g-spacing-5xl) + 4px));
73
+ }
74
+
75
+ .content-body {
76
+ margin: var(--dx-g-spacing-md) 0 0;
77
+ max-width: 900px;
78
+ flex: 1;
79
+ width: 0;
80
+ }
81
+
82
+ .is-sticky {
83
+ align-self: flex-start;
84
+ position: sticky;
85
+ top: var(--dx-c-content-sidebar-sticky-top);
86
+ }
87
+
88
+ .right-nav-bar {
89
+ margin-left: var(--dx-g-spacing-2xl);
90
+ }
91
+
92
+ @media screen and (max-width: 1024px) {
93
+ .right-nav-bar {
94
+ display: none;
95
+ }
96
+ }
97
+
98
+ @media screen and (max-width: 800px) {
99
+ .content-body {
100
+ margin-top: var(--dx-c-content-vertical-spacing);
101
+ }
102
+
103
+ .content-body-doc-phase-container {
104
+ border-left: 0;
105
+ }
106
+ }
107
+
108
+ @media screen and (max-width: 768px) {
109
+ .is-sticky {
110
+ width: 100%;
111
+ }
112
+
113
+ .content {
114
+ flex-direction: column;
115
+ }
116
+
117
+ .content-body-container {
118
+ padding-right: 0;
119
+ overflow-x: auto;
120
+ margin-bottom: calc(var(--dx-g-spacing-5xl) + 4px);
121
+ }
122
+
123
+ .left-nav-bar {
124
+ height: unset;
125
+ }
126
+
127
+ .content-body {
128
+ margin-left: var(--dx-g-spacing-mlg, 20px);
129
+ margin-right: var(--dx-g-spacing-mlg, 20px);
130
+ }
131
+ }
package/LICENSE DELETED
@@ -1,12 +0,0 @@
1
- Copyright (c) 2020, Salesforce.com, Inc.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
-
8
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
-
10
- * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
-
12
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.