hdoc-tools 0.62.5 → 0.63.0
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/editor/dist/assets/{index-BtxvGZHW.js → index-Dknn5g5A.js} +5 -4
- package/editor/dist/index.html +13 -13
- package/hdoc-build.js +5 -3
- package/hdoc-content-routes.js +136 -4
- package/hdoc-serve.js +26 -3
- package/package.json +1 -1
- package/ui/css/theme-default/styles/base.css +86 -21
- package/ui/css/theme-default/styles/components/api-doc.css +6 -3
- package/ui/css/theme-default/styles/components/content.css +140 -39
- package/ui/css/theme-default/styles/components/custom-block.css +1 -1
- package/ui/css/theme-default/styles/components/htl-doc.css +243 -64
- package/ui/css/theme-default/styles/components/htl-library.css +152 -0
- package/ui/css/theme-default/styles/components/htl-search.css +267 -0
- package/ui/css/theme-default/styles/components/sidebar.css +59 -16
- package/ui/css/theme-default/styles/fonts.css +17 -3
- package/ui/css/theme-default/styles/htldoc.layouts.css +756 -87
- package/ui/css/theme-default/styles/vars.css +40 -35
- package/ui/favicon.svg +64 -0
- package/ui/images/hornbill-logo-full-reversed.svg +92 -0
- package/ui/images/hornbill-logo-full.svg +92 -0
- package/ui/images/hug_library.jpg +0 -0
- package/ui/images/mcp-catalog.svg +9 -0
- package/ui/images/products/hornbill-square.svg +1 -0
- package/ui/index.html +1106 -342
- package/ui/js/bootstrap.js +89 -0
- package/ui/js/doc.hornbill.js +3156 -751
- package/ui/js/hb.vue.js +121 -0
- package/ui/js/highlightjs/highlight.pack.js +1513 -2
- package/ui/js/highlightjs/styles/vs2015-accessible.css +141 -0
- package/ui/js/highlightjs-badge.js +41 -58
- package/ui/js/mermaid.min.js +1447 -1295
- package/ui/js/webcomponents/hdocApprove.js +115 -0
- package/ui/js/highlightjs/styles/brown-paper.css +0 -64
- package/ui/js/highlightjs/styles/brown-papersq.png +0 -0
- package/ui/js/highlightjs/styles/codepen-embed.css +0 -60
- package/ui/js/highlightjs/styles/color-brewer.css +0 -71
- package/ui/js/highlightjs/styles/darcula.css +0 -77
- package/ui/js/highlightjs/styles/dark.css +0 -63
- package/ui/js/highlightjs/styles/darkula.css +0 -6
- package/ui/js/highlightjs/styles/default.css +0 -99
- package/ui/js/highlightjs/styles/dracula.css +0 -76
- package/ui/js/highlightjs/styles/far.css +0 -71
- package/ui/js/highlightjs/styles/foundation.css +0 -88
- package/ui/js/highlightjs/styles/github-gist.css +0 -71
- package/ui/js/highlightjs/styles/github-mm.css +0 -71
- package/ui/js/highlightjs/styles/github.css +0 -99
- package/ui/js/highlightjs/styles/googlecode.css +0 -89
- package/ui/js/highlightjs/styles/grayscale.css +0 -101
- package/ui/js/highlightjs/styles/idea.css +0 -97
- package/ui/js/highlightjs/styles/ir-black.css +0 -73
- package/ui/js/highlightjs/styles/kavadocs.css +0 -71
- package/ui/js/highlightjs/styles/kavadocsdark.css +0 -120
- package/ui/js/highlightjs/styles/kimbie.dark.css +0 -74
- package/ui/js/highlightjs/styles/kimbie.light.css +0 -74
- package/ui/js/highlightjs/styles/magula.css +0 -70
- package/ui/js/highlightjs/styles/mono-blue.css +0 -59
- package/ui/js/highlightjs/styles/monokai-sublime.css +0 -83
- package/ui/js/highlightjs/styles/monokai.css +0 -70
- package/ui/js/highlightjs/styles/obsidian.css +0 -88
- package/ui/js/highlightjs/styles/paraiso-dark.css +0 -72
- package/ui/js/highlightjs/styles/paraiso-light.css +0 -72
- package/ui/js/highlightjs/styles/railscasts.css +0 -106
- package/ui/js/highlightjs/styles/rainbow.css +0 -85
- package/ui/js/highlightjs/styles/solarized-dark.css +0 -84
- package/ui/js/highlightjs/styles/solarized-light.css +0 -84
- package/ui/js/highlightjs/styles/sunburst.css +0 -102
- package/ui/js/highlightjs/styles/twilight.css +0 -97
- package/ui/js/highlightjs/styles/vs.css +0 -68
- package/ui/js/highlightjs/styles/vs2015.css +0 -117
- package/ui/js/highlightjs/styles/xcode.css +0 -104
- package/ui/js/highlightjs/styles/zenburn.css +0 -80
package/ui/js/doc.hornbill.js
CHANGED
|
@@ -2,808 +2,3213 @@
|
|
|
2
2
|
//-- THIS CONTROLS NAVIGATION
|
|
3
3
|
//-- loaded in index.html
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
var bInitialPageLoad = true;
|
|
6
|
+
var global = {stateParams:{},session:{}};
|
|
7
|
+
var arrSearchResponseTimes = {};
|
|
8
|
+
var arrSearchResponseTimesByWordCount = {};
|
|
9
|
+
|
|
10
|
+
//-- Search request tracking. The debounce in searchDocuments only cancels a pending
|
|
11
|
+
//-- TIMER - once a fetch is away it runs to completion, so two overlapping searches
|
|
12
|
+
//-- resolved in arrival order and a slow earlier query could land last and clobber
|
|
13
|
+
//-- the newer results. searchRequestSeq is a monotonic id: the .then compares its
|
|
14
|
+
//-- captured id against the current one and drops the response if it has been
|
|
15
|
+
//-- superseded. searchAbortController additionally cancels the older request so we
|
|
16
|
+
//-- aren't paying for a response we would only throw away. //-- SG
|
|
17
|
+
var searchRequestSeq = 0;
|
|
18
|
+
var searchAbortController = null;
|
|
19
|
+
|
|
20
|
+
var docAppMethods = {
|
|
21
|
+
setSessionInfo:function(info)
|
|
22
|
+
{
|
|
23
|
+
let self = this;
|
|
24
|
+
self.docApp.userSession = info;
|
|
25
|
+
global.session = info;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
switchSettingsView:function()
|
|
29
|
+
{
|
|
30
|
+
// Store current page so we can return to it
|
|
31
|
+
sessionStorage.setItem("hdoc-settings-return-url", document.location.href);
|
|
32
|
+
const currUrl = new URL(document.location.href);
|
|
33
|
+
document.location.href = currUrl.origin + "/settings";
|
|
34
|
+
},
|
|
35
|
+
switchViewTheme:function()
|
|
36
|
+
{
|
|
37
|
+
if(ThemePreference)document.documentElement.classList.remove(ThemePreference);
|
|
38
|
+
|
|
39
|
+
if(ThemePreference === 'dark')
|
|
40
|
+
{
|
|
41
|
+
ThemePreference = 'light';
|
|
42
|
+
}
|
|
43
|
+
else{
|
|
44
|
+
ThemePreference = 'dark';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
localStorage.setItem('hdocbook-theme-appearance',ThemePreference);
|
|
48
|
+
document.documentElement.classList.add(ThemePreference);
|
|
49
|
+
|
|
50
|
+
//-- so can show different theme button icon need to store pref against view app
|
|
51
|
+
//-- ThemePreference is declared in data(), so this assignment re-renders the two theme
|
|
52
|
+
//-- buttons on its own - no $forceUpdate() needed. //-- SG
|
|
53
|
+
let self = this;
|
|
54
|
+
self.docApp.ThemePreference = ThemePreference;
|
|
55
|
+
|
|
56
|
+
},
|
|
57
|
+
renderNavigation:function()
|
|
58
|
+
{
|
|
59
|
+
let self = this;
|
|
60
|
+
|
|
61
|
+
//-- Seed expand:false on every node before handing the tree to Vue. hdocbook.json has no
|
|
62
|
+
//-- expand key, so it was previously created on first click. Vue 3 tracks late-added
|
|
63
|
+
//-- properties so that worked, but declaring it up front means the collapse state is part
|
|
64
|
+
//-- of the shape Vue observes from the outset rather than appearing mid-flight - which is
|
|
65
|
+
//-- what lets toggleNavCollapse() drop its $forceUpdate(). //-- SG
|
|
66
|
+
let seedExpand = function(items)
|
|
67
|
+
{
|
|
68
|
+
if(!Array.isArray(items)) return;
|
|
69
|
+
for(let x=0;x<items.length;x++)
|
|
70
|
+
{
|
|
71
|
+
if(items[x].expand === undefined) items[x].expand = false;
|
|
72
|
+
seedExpand(items[x].items);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
seedExpand(self.docApp.book.navigation.items);
|
|
76
|
+
|
|
77
|
+
self.docApp.navSections = self.docApp.book.navigation.items; //-- show be an array of nav groups
|
|
78
|
+
},
|
|
79
|
+
toggleTocMenu:function()
|
|
80
|
+
{
|
|
81
|
+
view.docApp.tocMobileOpen = !view.docApp.tocMobileOpen;
|
|
82
|
+
},
|
|
83
|
+
closeTocMenu:function()
|
|
84
|
+
{
|
|
85
|
+
view.docApp.tocMobileOpen = false;
|
|
86
|
+
},
|
|
87
|
+
toggleMobileMenu:function()
|
|
88
|
+
{
|
|
89
|
+
view.docApp.bookNavOpen = !view.docApp.bookNavOpen;
|
|
90
|
+
},
|
|
91
|
+
resetMobileMenu:function(ev)
|
|
92
|
+
{
|
|
93
|
+
//-- Runs on EVERY click anywhere in the app, via data-root-click on the mount host.
|
|
94
|
+
//--
|
|
95
|
+
//-- Both panels it governs are Vue-rendered, so their open state is Vue state and this
|
|
96
|
+
//-- function only ever writes that state - never a class. It used to toggle .open on
|
|
97
|
+
//-- .DocSidebar directly while keeping a second copy of the flag in
|
|
98
|
+
//-- $(".mobile-menu-btn").data("isopen"): two sources of truth over DOM that Vue
|
|
99
|
+
//-- rewrites on re-render. That is exactly what made the search Filters button look
|
|
100
|
+
//-- dead - the class was stripped here on the same click that opened it, while Vue
|
|
101
|
+
//-- still believed the panel was open and so never re-added it. //-- SG
|
|
102
|
+
if(!ev || !ev.target || !ev.target.closest) return;
|
|
103
|
+
|
|
104
|
+
let target = ev.target;
|
|
105
|
+
|
|
106
|
+
//-- a click on a content link (a.DocLink) should dismiss the menu - the user has picked a
|
|
107
|
+
//-- page. Group expand/collapse rows are .navSectionGroupItem divs, not .DocLink anchors,
|
|
108
|
+
//-- so they still fall through to the keep-open clause below. //-- SG
|
|
109
|
+
//--
|
|
110
|
+
//-- closest() matches the element itself as well as its ancestors, which is why the old
|
|
111
|
+
//-- hasClass(...) || closest(...) pairs collapse to one test each. //-- SG
|
|
112
|
+
let clickedNavLink = !!target.closest("a.DocLink");
|
|
113
|
+
let keepBookNavOpen = view.docApp.bookNavOpen && !clickedNavLink && !!target.closest(".hdoc-nav");
|
|
114
|
+
|
|
115
|
+
if(!(keepBookNavOpen || target.closest(".mobile-menu-btn") || target.closest(".navSectionGroupItem")))
|
|
116
|
+
{
|
|
117
|
+
view.docApp.bookNavOpen = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//-- the search filter panel dismisses on an outside click too. Its own button is excluded
|
|
121
|
+
//-- or it would close on the very click that opened it. //-- SG
|
|
122
|
+
if(view.docApp.searchFiltersOpen
|
|
123
|
+
&& !target.closest(".LibrarySearch .DocSidebar")
|
|
124
|
+
&& !target.closest(".search-filter-btn"))
|
|
125
|
+
{
|
|
126
|
+
view.docApp.searchFiltersOpen = false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
organiseLibraryByGroup:function(oGroupMode,registerUrlState)
|
|
130
|
+
{
|
|
131
|
+
//-- coming from event - so want to change url state so can click back to last lib view mode
|
|
132
|
+
if(registerUrlState)
|
|
133
|
+
{
|
|
134
|
+
//-- set url to root
|
|
135
|
+
let urlToSet = window.location.origin + "/?mode=" + oGroupMode.mode;
|
|
136
|
+
const url = new URL(urlToSet);
|
|
137
|
+
window.history.pushState(null, null, url);
|
|
138
|
+
}
|
|
139
|
+
organiseLibrary(oGroupMode);
|
|
140
|
+
},
|
|
141
|
+
loadProductHome:function(productGroup,registerUrlState)
|
|
142
|
+
{
|
|
143
|
+
//-- coming from event - so want to change url state so can click back to last lib view mode
|
|
144
|
+
if(registerUrlState)
|
|
145
|
+
{
|
|
146
|
+
//-- set url to root
|
|
147
|
+
let urlToSet = window.location.origin + "/?mode=product&product="+productGroup.id;
|
|
148
|
+
const url = new URL(urlToSet);
|
|
149
|
+
window.history.pushState(null, null, url);
|
|
150
|
+
}
|
|
151
|
+
view.docApp.libraryHomeViewProduct = productGroup;
|
|
152
|
+
|
|
153
|
+
},
|
|
154
|
+
loadLibraryHome:function()
|
|
155
|
+
{
|
|
156
|
+
//-- will reset view
|
|
157
|
+
view.docApp.boolSearchInProgress = false;
|
|
158
|
+
view.docApp.searchMode = false;
|
|
159
|
+
view.docApp.mcpCatalogMode = false;
|
|
160
|
+
view.docApp.searchResponse = null;
|
|
161
|
+
view.docApp.searchFilterBySpecificBookId = "";
|
|
162
|
+
|
|
163
|
+
resetCheckedProducts();
|
|
164
|
+
view.docApp.searchFilteredProducts = {};
|
|
165
|
+
|
|
166
|
+
view.docApp.altSearchText = "";
|
|
167
|
+
view.docApp.searchText = "";
|
|
168
|
+
view.docApp.book = {};
|
|
169
|
+
view.docApp.libraryHomeViewProduct = null;
|
|
170
|
+
|
|
171
|
+
document.title = "Hornbill Documentation";
|
|
172
|
+
|
|
173
|
+
//-- set url to root
|
|
174
|
+
let urlToSet = window.location.origin + "/";
|
|
175
|
+
const url = new URL(urlToSet);
|
|
176
|
+
window.history.pushState(null, null, url);
|
|
177
|
+
|
|
178
|
+
//-- remove hidden class
|
|
179
|
+
revealApp();
|
|
180
|
+
|
|
181
|
+
},
|
|
182
|
+
clearSearchFilters:function($ev)
|
|
183
|
+
{
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
resetCheckedProducts();
|
|
187
|
+
view.docApp.searchFilteredProducts = {};
|
|
188
|
+
view.docApp.hasFilterByProducts = false;
|
|
189
|
+
view.docApp.searchFilterBySpecificBookId = "";
|
|
190
|
+
view.docApp.searchFilterBySpecificBookTitle = "";
|
|
191
|
+
view.docApp.searchAudienceFilter = "";
|
|
192
|
+
view.docApp.searchDocTypeFilter = "";
|
|
193
|
+
//-- clearing the filters is an explicit choice of "no content-type filter", so it
|
|
194
|
+
//-- persists like any other pick - otherwise the remembered value would silently
|
|
195
|
+
//-- come back on the next auto-widen restore //-- SG
|
|
196
|
+
rememberDocTypeFilter("");
|
|
197
|
+
view.searchDocuments(view.docApp.altSearchText,false,$ev,true);
|
|
198
|
+
|
|
199
|
+
},
|
|
200
|
+
filterSearchByAudience:function($ev)
|
|
201
|
+
{
|
|
202
|
+
//-- v-model has already written the selected value to searchAudienceFilter;
|
|
203
|
+
//-- this is a server-side filter so re-run the search //-- SG
|
|
204
|
+
view.searchDocuments(view.docApp.altSearchText,false,$ev,true);
|
|
205
|
+
},
|
|
206
|
+
toggleSearchFilters:function()
|
|
207
|
+
{
|
|
208
|
+
//-- Below 1020px the filter sidebar is hidden and this view has no burger, so
|
|
209
|
+
//-- the filters were unreachable. The panel is already styled to slide in via
|
|
210
|
+
//-- .DocSidebar.open; this just drives that class.
|
|
211
|
+
//--
|
|
212
|
+
//-- Bound through Vue rather than jQuery like the book-page burger: this state
|
|
213
|
+
//-- is reactive, so a jQuery-applied class is patched straight back off on the
|
|
214
|
+
//-- re-render that setting it triggers. The book nav gets away with jQuery only
|
|
215
|
+
//-- because nothing re-renders that element. //-- SG
|
|
216
|
+
view.docApp.searchFiltersOpen = !view.docApp.searchFiltersOpen;
|
|
217
|
+
},
|
|
218
|
+
closeSearchFilters:function()
|
|
219
|
+
{
|
|
220
|
+
view.docApp.searchFiltersOpen = false;
|
|
221
|
+
},
|
|
222
|
+
filterSearchByDocType:function(docType,$ev)
|
|
223
|
+
{
|
|
224
|
+
//-- server-side soft filter; re-run so the counts come back whole-corpus //-- SG
|
|
225
|
+
view.docApp.searchDocTypeFilter = docType;
|
|
226
|
+
rememberDocTypeFilter(docType);
|
|
227
|
+
view.docApp.searchDocTypeRescue = null;
|
|
228
|
+
docTypeAutoWidenedForQuery = null;
|
|
229
|
+
view.closeSearchFilters();
|
|
230
|
+
view.searchDocuments(view.docApp.altSearchText,false,$ev,true);
|
|
231
|
+
},
|
|
232
|
+
filterSearchByProducts:function(product,$ev)
|
|
233
|
+
{
|
|
234
|
+
let productId = product.id;
|
|
235
|
+
if(view.docApp.searchFilteredProducts[productId])
|
|
236
|
+
{
|
|
237
|
+
view.docApp.searchFilteredProducts[productId].checked = !view.docApp.searchFilteredProducts[productId].checked;
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
{
|
|
241
|
+
|
|
242
|
+
view.docApp.searchFilteredProducts[productId] = {checked:true};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
//-- so check or uncheck the flag in ui
|
|
246
|
+
product.checked = view.docApp.searchFilteredProducts[productId].checked;
|
|
247
|
+
|
|
248
|
+
//-- client side filtering
|
|
249
|
+
if(view.docApp.searchResponse && Array.isArray(view.docApp.searchResponse.results))
|
|
250
|
+
{
|
|
251
|
+
prepareSearchResultItems(view.docApp.searchResponse.results || []);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let checkedProducts = getCheckedProductFilterArray();
|
|
255
|
+
view.docApp.hasFilterByProducts = checkedProducts.length > 0; //-- declared boolean, keep it one
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
},
|
|
260
|
+
filterSearchByBook:function(aBook,$ev)
|
|
261
|
+
{
|
|
262
|
+
view.docApp.searchFilterBySpecificBookId = aBook.bookId;
|
|
263
|
+
view.docApp.searchFilterBySpecificBookTitle = aBook.title;
|
|
264
|
+
//-- an explicit pick replaces whatever bad id the rescue reported on
|
|
265
|
+
view.docApp.searchBookRescue = null;
|
|
266
|
+
//-- going to re-search
|
|
267
|
+
view.searchDocuments(view.docApp.altSearchText,false,$ev,true);
|
|
268
|
+
|
|
269
|
+
},
|
|
270
|
+
toolbarSearch:function(strOnLoadSearch,initalBookIdFilter,boolFromPop)
|
|
271
|
+
{
|
|
272
|
+
//-- "|| \"\"": index.html binds keyup.enter="toolbarSearch()" with no arguments, so an
|
|
273
|
+
//-- empty toolbar box used to hand undefined through to searchDocuments, which threw on
|
|
274
|
+
//-- .match() before it could take its own empty-search exit //-- SG
|
|
275
|
+
if(!view.docApp.searchText)view.docApp.searchText = strOnLoadSearch || "";
|
|
276
|
+
view.docApp.altSearchText = view.docApp.searchText;
|
|
277
|
+
view.docApp.currentResultsSearchText = "";
|
|
278
|
+
view.docApp.searchFilterBySpecificBookId = initalBookIdFilter || "";
|
|
279
|
+
view.searchDocuments(view.docApp.searchText,boolFromPop);
|
|
280
|
+
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
openMcpCatalogView:function()
|
|
284
|
+
{
|
|
285
|
+
//-- /mcp - the catalog of MCP servers this site exposes. Same shape as
|
|
286
|
+
//-- openSearchView: clear the other views, push the url, then load. The data
|
|
287
|
+
//-- comes from _api/mcp_catalog, which is generated from the servers actually
|
|
288
|
+
//-- mounted rather than a hand-kept list. //-- SG
|
|
289
|
+
view.docApp.book = {};
|
|
290
|
+
view.docApp.searchMode = false;
|
|
291
|
+
view.docApp.libraryHomeViewProduct = null;
|
|
292
|
+
view.docApp.mcpCatalogMode = true;
|
|
293
|
+
document.title = "Connect an AI assistant - Hornbill Documentation";
|
|
294
|
+
|
|
295
|
+
if(window.location.pathname !== "/mcp")
|
|
296
|
+
{
|
|
297
|
+
window.history.pushState({}, "", "/mcp");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
revealApp();
|
|
301
|
+
view.loadMcpCatalog();
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
loadMcpCatalog:function()
|
|
305
|
+
{
|
|
306
|
+
//-- static enough to fetch once per page load
|
|
307
|
+
if(view.docApp.mcpCatalog.loaded) return;
|
|
308
|
+
|
|
309
|
+
fetchJsonFile("_api/mcp_catalog").then(function(data)
|
|
310
|
+
{
|
|
311
|
+
if(data && data.servers)
|
|
312
|
+
{
|
|
313
|
+
view.docApp.mcpCatalog.servers = data.servers;
|
|
314
|
+
view.docApp.mcpCatalog.loaded = true;
|
|
315
|
+
view.docApp.mcpCatalog.error = false;
|
|
316
|
+
}
|
|
317
|
+
else
|
|
318
|
+
{
|
|
319
|
+
//-- keep the model shape and flag the error; the template dereferences
|
|
320
|
+
//-- mcpCatalog.* at render time, so nulling it would be a blank view //-- SG
|
|
321
|
+
view.docApp.mcpCatalog.error = true;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
mcpToolDescriptionParts:function(text)
|
|
327
|
+
{
|
|
328
|
+
//-- tool descriptions are written for the MODEL, so they mark identifiers with
|
|
329
|
+
//-- backticks. Split on them so the page can render real <code> spans without
|
|
330
|
+
//-- v-html - the descriptions are ours, but nothing here needs to compile HTML
|
|
331
|
+
//-- from data, and this site deliberately never does. //-- SG
|
|
332
|
+
return String(text || "").split("`").map(function(part, i)
|
|
333
|
+
{
|
|
334
|
+
return { text: part, code: (i % 2) === 1 };
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
mcpCopy:function(key, text)
|
|
339
|
+
{
|
|
340
|
+
//-- Copy a connection field. Feedback is a reactive flag per field rather than
|
|
341
|
+
//-- DOM poking, so the icon swap is just a binding. navigator.clipboard needs a
|
|
342
|
+
//-- secure context; the docs sites are https and localhost counts, but fall back
|
|
343
|
+
//-- to the execCommand approach the code-block badge uses so a plain-http
|
|
344
|
+
//-- deployment still works. //-- SG
|
|
345
|
+
var settle = function(ok)
|
|
346
|
+
{
|
|
347
|
+
view.docApp.mcpCopied[key] = ok;
|
|
348
|
+
setTimeout(function(){ delete view.docApp.mcpCopied[key]; }, 1600);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if(navigator.clipboard && window.isSecureContext)
|
|
352
|
+
{
|
|
353
|
+
navigator.clipboard.writeText(text).then(function(){ settle(true); }, function(){ settle(false); });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var el = document.createElement("textarea");
|
|
358
|
+
el.value = text;
|
|
359
|
+
el.setAttribute("readonly", "");
|
|
360
|
+
el.style.position = "absolute";
|
|
361
|
+
el.style.left = "-9999px";
|
|
362
|
+
document.body.appendChild(el);
|
|
363
|
+
el.select();
|
|
364
|
+
var ok = false;
|
|
365
|
+
try { ok = document.execCommand("copy"); } catch(e) { ok = false; }
|
|
366
|
+
document.body.removeChild(el);
|
|
367
|
+
settle(ok);
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
mcpServerUrl:function(path)
|
|
371
|
+
{
|
|
372
|
+
return window.location.origin + path;
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
openSearchView:function()
|
|
376
|
+
{
|
|
377
|
+
//-- narrow-viewport search button: below 602px the inline toolbar search box
|
|
378
|
+
//-- (.toolbar-search-bar) is hidden, so this takes the user to the search page,
|
|
379
|
+
//-- which has the full-width banner search input. With no query yet this renders
|
|
380
|
+
//-- the same view as loading /search directly - the library home with the banner
|
|
381
|
+
//-- box - so refresh and back/forward behave consistently. Clearing book drops the
|
|
382
|
+
//-- document view; the back button restores it via popstate. //-- SG
|
|
383
|
+
view.docApp.book = {};
|
|
384
|
+
view.docApp.searchMode = false;
|
|
385
|
+
view.docApp.mcpCatalogMode = false;
|
|
386
|
+
view.docApp.searchText = "";
|
|
387
|
+
view.docApp.altSearchText = "";
|
|
388
|
+
view.docApp.currentResultsSearchText = "";
|
|
389
|
+
view.docApp.libraryHomeViewProduct = null;
|
|
390
|
+
document.title = "Hornbill Documentation Search";
|
|
391
|
+
if(window.location.pathname !== "/search")
|
|
392
|
+
{
|
|
393
|
+
window.history.pushState(null, null, window.location.origin + "/search");
|
|
394
|
+
}
|
|
395
|
+
//-- focus the banner search box once Vue has painted the library/search layout
|
|
396
|
+
//-- (setTimeout runs after Vue's microtask render flush)
|
|
397
|
+
setTimeout(function(){ let inp = document.querySelector(".library-banner-hug input"); if(inp) inp.focus(); },0);
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
searchDocuments:function(inAltSearchText,inBoolFromPopState,ev,inBoolFromBookFilter)
|
|
401
|
+
{
|
|
402
|
+
if(!inAltSearchText) inAltSearchText = ""; //-- callers can legitimately pass nothing
|
|
403
|
+
//-- kept for the length-gate exit below, which may have to put the view back the way
|
|
404
|
+
//-- it found it (the title is rewritten before the gate is reached) //-- SG
|
|
405
|
+
let titleBeforeSearch = document.title;
|
|
406
|
+
//-- note: despite the name, validSearch is TRUE when the quote count is odd, i.e. when
|
|
407
|
+
//-- the search is INVALID - index.html uses it to show the unbalanced-quotes warning
|
|
408
|
+
view.docApp.validSearch = ((inAltSearchText.match(/"/g) || []).length % 2 === 1);
|
|
409
|
+
let waitTime = 1;
|
|
410
|
+
let boolWasEnterKeyOrSearchButton = false;
|
|
411
|
+
if(ev)
|
|
412
|
+
{
|
|
413
|
+
ev.stopImmediatePropagation();
|
|
414
|
+
ev.preventDefault();
|
|
415
|
+
boolWasEnterKeyOrSearchButton = (ev.key==="Enter" || ev.type==="click"); //-- we will store url state on enter or search btn click
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//-- clear any pending search
|
|
419
|
+
clearTimeout(view.searchTimeoutId);
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
//-- if there is a http search inprogress then store this one to do once response is returned
|
|
425
|
+
view.docApp.nextSearchCriteria = {};
|
|
426
|
+
view.docApp.nextSearchCriteria.altSearchText = inAltSearchText;
|
|
427
|
+
view.docApp.nextSearchCriteria.boolFromPopState = inBoolFromPopState;
|
|
428
|
+
view.docApp.nextSearchCriteria.eventInfo = ev;
|
|
429
|
+
view.docApp.nextSearchCriteria.boolWasEnterKeyOrSearchButton = boolWasEnterKeyOrSearchButton;
|
|
430
|
+
|
|
431
|
+
//-- do we have anything to search
|
|
432
|
+
view.docApp.searchMode = (inAltSearchText || view.docApp.searchText)?true:false;
|
|
433
|
+
|
|
434
|
+
if(!view.docApp.searchMode)
|
|
435
|
+
{
|
|
436
|
+
document.title = "Hornbill Documentation";
|
|
437
|
+
//-- set url home index
|
|
438
|
+
view.docApp.currentResultsSearchText = "";
|
|
439
|
+
view.docApp.searchTooShort = false;
|
|
440
|
+
hideSearchTooShortHint();
|
|
441
|
+
|
|
442
|
+
//-- set url to root (only if the current url is a search url)
|
|
443
|
+
if(window.location.href.indexOf("?q=") !== -1)
|
|
444
|
+
{
|
|
445
|
+
let urlToSet = window.location.origin + "/";
|
|
446
|
+
const url = new URL(urlToSet);
|
|
447
|
+
window.history.replaceState(null, null, url);
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
document.title = "Hornbill Documentation Search";
|
|
452
|
+
|
|
453
|
+
//-- single-character queries never fetch: the server drops unquoted single-char
|
|
454
|
+
//-- terms (they can only return noise), so the round-trip per keystroke would buy
|
|
455
|
+
//-- an empty result set. Raw length check on purpose - a QUOTED single char ("e")
|
|
456
|
+
//-- is three characters and goes through, matching the server's phrase exemption.
|
|
457
|
+
//-- boolSearchInProgress may be true from a just-cancelled pending search, so put
|
|
458
|
+
//-- it back down or the results list stays dimmed //-- SG
|
|
459
|
+
if(inAltSearchText.trim().length === 1)
|
|
460
|
+
{
|
|
461
|
+
view.docApp.boolSearchInProgress = false;
|
|
462
|
+
//-- Returning without touching the view left whatever was on screen standing, and
|
|
463
|
+
//-- the header names currentResultsSearchText, not what is in the box: typing "qq"
|
|
464
|
+
//-- then backspacing showed "0 results for: qq" for a one-character query, and a
|
|
465
|
+
//-- single character typed after any earlier search showed that search's hits as
|
|
466
|
+
//-- though they matched. A first character showed nothing at all - no results, no
|
|
467
|
+
//-- header, no reason. Clear the previous answer and say why there is none. //-- SG
|
|
468
|
+
//-- A document is open (the toolbar search box is on every book page): a single
|
|
469
|
+
//-- character has nothing to show, so leave the document alone rather than half
|
|
470
|
+
//-- opening the search over it. searchMode was set above, and the book view is
|
|
471
|
+
//-- rendered off book.docId - which the gate never reaches the code that clears -
|
|
472
|
+
//-- so both panels were on screen at once, the article squeezed into the corner.
|
|
473
|
+
//-- No notice here: the box still holds what was typed, and destroying what the
|
|
474
|
+
//-- user is reading to say "keep typing" is the worse trade. //-- SG
|
|
475
|
+
if(view.docApp.book && view.docApp.book.docId)
|
|
476
|
+
{
|
|
477
|
+
view.docApp.searchMode = false;
|
|
478
|
+
view.docApp.searchTooShort = false;
|
|
479
|
+
//-- nothing on screen changes on this path, so pressing Enter looked like a dead
|
|
480
|
+
//-- key. Say why, next to the box that was typed in, and take it away again on its
|
|
481
|
+
//-- own - a hint the user has to dismiss is worse than the silence it replaces //-- SG
|
|
482
|
+
showSearchTooShortHint();
|
|
483
|
+
document.title = titleBeforeSearch;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
view.docApp.searchTooShort = true;
|
|
488
|
+
view.docApp.searchResponse = null;
|
|
489
|
+
view.docApp.searchResultFilteredItems = [];
|
|
490
|
+
view.docApp.currentResultsSearchText = "";
|
|
491
|
+
view.docApp.searchError = "";
|
|
492
|
+
view.docApp.searchDocTypeRescue = null;
|
|
493
|
+
view.docApp.searchBookRescue = null;
|
|
494
|
+
docTypeAutoWidenedForQuery = null;
|
|
495
|
+
resetFilteredProductsListCounters();
|
|
496
|
+
//-- the url described the results we just dropped, so it must not keep claiming them
|
|
497
|
+
if(window.location.href.indexOf("?q=") !== -1)
|
|
498
|
+
{
|
|
499
|
+
window.history.replaceState(null, null, window.location.origin + "/search");
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
//-- past the length gate, so any "too short" notice belongs to a previous keystroke
|
|
505
|
+
view.docApp.searchTooShort = false;
|
|
506
|
+
hideSearchTooShortHint();
|
|
507
|
+
|
|
508
|
+
//-- searching same thing so leave
|
|
509
|
+
if(inAltSearchText===view.docApp.currentResultsSearchText && !boolWasEnterKeyOrSearchButton && !inBoolFromPopState && !inBoolFromBookFilter) return;
|
|
510
|
+
|
|
511
|
+
//-- set a wait time. The debounce exists to let a typing user finish their word -
|
|
512
|
+
//-- filter changes (doc type / book / audience radios and selects, which arrive as
|
|
513
|
+
//-- "change" events, not "click") are a completed intent, so they'd only be slowed
|
|
514
|
+
//-- by it: the facet click already paid the full search round-trip, and the extra
|
|
515
|
+
//-- 250ms+ here was most of the perceived lag //-- SG
|
|
516
|
+
if(!boolWasEnterKeyOrSearchButton && !inBoolFromPopState && !inBoolFromBookFilter)
|
|
517
|
+
{
|
|
518
|
+
waitTime = calculateSearchWaitTimeBasedOnQueryText(inAltSearchText);
|
|
519
|
+
hslDocLog.log("SEARCH: SETTING FETCH DELAY: " + waitTime + "ms");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
//-- Committed to the search view now, so drop the document here rather than inside the
|
|
523
|
+
//-- debounced callback below. The search panel renders off searchMode (set above) while
|
|
524
|
+
//-- the book panel renders off book.docId, so for the whole debounce + fetch the article
|
|
525
|
+
//-- was still mounted and shared the viewport with the search layout - the page visibly
|
|
526
|
+
//-- squeezed into the bottom-right corner until the results skeleton replaced it. The
|
|
527
|
+
//-- skeleton needs boolSearchInProgress, set immediately below, so the swap is one frame
|
|
528
|
+
//-- with nothing in between. //-- SG
|
|
529
|
+
if(view.docApp.book && view.docApp.book.docId) view.docApp.book = {};
|
|
530
|
+
|
|
531
|
+
view.docApp.boolSearchInProgress = true;
|
|
532
|
+
view.searchTimeoutId = setTimeout(function(altSearchText,boolFromPopState,boolWasEnterKeyOrSearchButton,srcEvent)
|
|
533
|
+
{
|
|
534
|
+
view.docApp.searchError = "";
|
|
535
|
+
|
|
536
|
+
resetFilteredProductsListCounters();
|
|
537
|
+
|
|
538
|
+
if(view.docApp.nextSearchCriteria)
|
|
539
|
+
{
|
|
540
|
+
altSearchText = view.docApp.nextSearchCriteria.altSearchText;
|
|
541
|
+
boolFromPopState =view.docApp.nextSearchCriteria.boolFromPopState;
|
|
542
|
+
boolWasEnterKeyOrSearchButton =view.docApp.nextSearchCriteria.boolWasEnterKeyOrSearchButton;
|
|
543
|
+
srcEvent =view.docApp.nextSearchCriteria.eventInfo;
|
|
544
|
+
|
|
545
|
+
//-- log the fields, not a deep copy of the criteria object. copyObject is a
|
|
546
|
+
//-- JSON.parse(JSON.stringify()) round-trip and nextSearchCriteria holds eventInfo,
|
|
547
|
+
//-- a jQuery event - so this ran a full serialise per keystroke for output that is
|
|
548
|
+
//-- only read if someone calls hslDocLog.dump(), and it was one property away from
|
|
549
|
+
//-- throwing on a circular structure (target / originalEvent) inside the search
|
|
550
|
+
//-- callback, which would have aborted the search. //-- SG
|
|
551
|
+
hslDocLog.log("SEARCH: NEW CRITERIA DETECTED: There is new search criteria waiting before current http search request takes place....using new criteria",
|
|
552
|
+
{altSearchText:altSearchText,boolFromPopState:boolFromPopState,boolWasEnterKeyOrSearchButton:boolWasEnterKeyOrSearchButton})
|
|
553
|
+
|
|
554
|
+
view.docApp.nextSearchCriteria = null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
//-- user has typed in search
|
|
559
|
+
if(altSearchText)
|
|
560
|
+
{
|
|
561
|
+
//-- only search if search is different to current one
|
|
562
|
+
if(altSearchText===view.docApp.currentResultsSearchText && !boolWasEnterKeyOrSearchButton && !inBoolFromBookFilter)return;
|
|
563
|
+
}
|
|
564
|
+
else
|
|
565
|
+
{
|
|
566
|
+
//-- we did search on the main landing page - so clear search results and filters
|
|
567
|
+
view.docApp.searchResultFilteredItems = [];
|
|
568
|
+
view.docApp.searchResponse = null;
|
|
569
|
+
view.docApp.searchFilterBySpecificBookId = "";
|
|
570
|
+
|
|
571
|
+
//-- if search is empty then do nothing
|
|
572
|
+
if(!view.docApp.searchText)
|
|
573
|
+
{
|
|
574
|
+
view.docApp.searchMode = false;
|
|
575
|
+
view.docApp.mcpCatalogMode = false;
|
|
576
|
+
view.docApp.currentResultsSearchText = "";
|
|
577
|
+
view.docApp.boolSearchInProgress = false;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
view.docApp.book = {};
|
|
583
|
+
|
|
584
|
+
//-- set url to search/or update search term
|
|
585
|
+
let urlToSet = window.location.origin + "/search";
|
|
586
|
+
let searchText = (altSearchText)?altSearchText:view.docApp.searchText;
|
|
587
|
+
|
|
588
|
+
//-- clear small search box
|
|
589
|
+
view.docApp.searchText = "";
|
|
590
|
+
|
|
591
|
+
//-- so can show in # result for {{docApp.currentResultsSearchText}}
|
|
592
|
+
view.docApp.currentResultsSearchText = searchText;
|
|
593
|
+
|
|
594
|
+
//-- NOTE: the previous results are deliberately NOT cleared here. Emptying the array
|
|
595
|
+
//-- at request time unmounted the whole results list (its v-if tests length), so every
|
|
596
|
+
//-- query destroyed and rebuilt all 300 rows even when the new results were nearly the
|
|
597
|
+
//-- same. The list now stays mounted and dimmed until the response replaces it in one
|
|
598
|
+
//-- assignment; the header switches to "Searching for:" so nothing claims the old count
|
|
599
|
+
//-- belongs to the new query. Cleared on the no-search and error paths instead. //-- SG
|
|
600
|
+
if(searchText)
|
|
601
|
+
{
|
|
602
|
+
//-- include bid if one is selected, and the audience filter when the
|
|
603
|
+
//-- (internal-only) picker has a value //-- SG
|
|
604
|
+
//-- restore the default once the query moves on from the one that was
|
|
605
|
+
//-- auto-widened, so the content-type filter keeps working for the next
|
|
606
|
+
//-- search instead of staying open because one query needed everything
|
|
607
|
+
if(docTypeAutoWidenedForQuery !== null && searchText !== docTypeAutoWidenedForQuery)
|
|
608
|
+
{
|
|
609
|
+
view.docApp.searchDocTypeFilter = docTypeUserDefaultFilter();
|
|
610
|
+
docTypeAutoWidenedForQuery = null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let searchterm=encodeQueryData(buildSearchQueryParams(searchText));
|
|
614
|
+
urlToSet += "?" + searchterm;
|
|
615
|
+
|
|
616
|
+
hslDocLog.log("SEARCH: TRIGGER: perform search on ", urlToSet)
|
|
617
|
+
|
|
618
|
+
//-- we will keep trak of last response time for a search of this many words and length
|
|
619
|
+
let searchInfoKey = "s_w" + searchText.split(" ").length + "_l" + searchText.length;
|
|
620
|
+
arrSearchResponseTimes[searchInfoKey] = Date.now();
|
|
621
|
+
|
|
622
|
+
//-- supersede any request still in flight, then claim this id
|
|
623
|
+
if(searchAbortController) searchAbortController.abort();
|
|
624
|
+
searchAbortController = new AbortController();
|
|
625
|
+
let requestSeq = ++searchRequestSeq;
|
|
626
|
+
|
|
627
|
+
fetchJsonFile("_search?"+searchterm,{signal:searchAbortController.signal}).then(function(data)
|
|
628
|
+
{
|
|
629
|
+
//-- a newer search has been issued since this one left - drop the response
|
|
630
|
+
//-- rather than letting it overwrite fresher results. Deliberately does NOT
|
|
631
|
+
//-- touch boolSearchInProgress: the newer request still owns that. //-- SG
|
|
632
|
+
if(requestSeq !== searchRequestSeq)
|
|
633
|
+
{
|
|
634
|
+
hslDocLog.log("SEARCH: RESPONSE: discarded stale response (req " + requestSeq + " of " + searchRequestSeq + ")");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
//-- No body, or one fetchJsonFile could not parse - it resolves null for a bad
|
|
639
|
+
//-- JSON body rather than rejecting (item 20), so this lands in the SUCCESS
|
|
640
|
+
//-- handler. Without this guard `data.query` threw here, and a throw inside
|
|
641
|
+
//-- onFulfilled is not caught by the onRejected passed to the same .then(), so
|
|
642
|
+
//-- the failure was silent and boolSearchInProgress stuck true - the spinner
|
|
643
|
+
//-- never cleared and no error was shown. Same class as item 19. //-- SG
|
|
644
|
+
if(!data)
|
|
645
|
+
{
|
|
646
|
+
hslDocLog.log("SEARCH: RESPONSE: no usable response body");
|
|
647
|
+
view.docApp.searchResultFilteredItems = [];
|
|
648
|
+
view.docApp.boolSearchInProgress = false;
|
|
649
|
+
view.docApp.nextSearchCriteria = null;
|
|
650
|
+
view.docApp.searchError = "Failed to search for books. The server search response is invalid";
|
|
651
|
+
revealApp();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
hslDocLog.log("SEARCH: RESPONSE: completed",data)
|
|
656
|
+
if(data.query)
|
|
657
|
+
{
|
|
658
|
+
//-- telemetry only, and it picks apart the server's echoed query string. Any
|
|
659
|
+
//-- change to that shape made [1] undefined and threw here, aborting the rest
|
|
660
|
+
//-- of the handler - results never rendered and boolSearchInProgress stuck
|
|
661
|
+
//-- true. Guarded so it can never take the render down with it. //-- SG
|
|
662
|
+
try
|
|
663
|
+
{
|
|
664
|
+
let searchedText = data.query.split("MATCH '")[1].split("*' ORDER")[0];
|
|
665
|
+
let searchTextKey = "s_w" + searchedText.split(" ").length + "_l" + searchedText.length;
|
|
666
|
+
if(arrSearchResponseTimes[searchTextKey])
|
|
667
|
+
{
|
|
668
|
+
//-- store the overrall ms taken (also for word count)
|
|
669
|
+
arrSearchResponseTimes[searchTextKey] = Date.now() - arrSearchResponseTimes[searchTextKey];
|
|
670
|
+
arrSearchResponseTimesByWordCount[searchTextKey.split("_l")[0]] = arrSearchResponseTimes[searchTextKey];
|
|
671
|
+
hslDocLog.log("SEARCH: RESPONSE: total UI http response time " + arrSearchResponseTimes[searchTextKey] + "ms");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch(e)
|
|
675
|
+
{
|
|
676
|
+
hslDocLog.log("SEARCH: RESPONSE: could not parse echoed query for timing telemetry",data.query);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
view.docApp.boolSearchInProgress=false;
|
|
681
|
+
view.docApp.nextSearchCriteria = null;
|
|
682
|
+
view.docApp.searchResponse = data;
|
|
683
|
+
|
|
684
|
+
//-- an unknown bid in the url must not dead-end the search with an
|
|
685
|
+
//-- unexplained empty page. See evaluateBookFilterRescue //-- SG
|
|
686
|
+
if(evaluateBookFilterRescue())
|
|
687
|
+
{
|
|
688
|
+
view.searchDocuments(view.docApp.altSearchText,false,null,true);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
//-- the content-type filter defaults ON, so it must never turn a
|
|
693
|
+
//-- findable answer into an empty page. See evaluateDocTypeRescue //-- SG
|
|
694
|
+
if(evaluateDocTypeRescue())
|
|
695
|
+
{
|
|
696
|
+
view.searchDocuments(view.docApp.altSearchText,false,null,true);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
var resultsFoundinCurrentFilteredBook = false;
|
|
701
|
+
if(view.docApp.searchResponse.booksInfo && view.docApp.searchResponse.booksInfo.length)
|
|
702
|
+
{
|
|
703
|
+
//-- associate book title to each searched book
|
|
704
|
+
for(let x=0;x<view.docApp.searchResponse.booksInfo.length;x++)
|
|
705
|
+
{
|
|
706
|
+
let book_info = getBookById(view.docApp.searchResponse.booksInfo[x].bookId);
|
|
707
|
+
|
|
708
|
+
if(book_info !== undefined && book_info.title !== undefined)
|
|
709
|
+
{
|
|
710
|
+
view.docApp.searchResponse.booksInfo[x].title = book_info.title;
|
|
711
|
+
if(view.docApp.searchFilterBySpecificBookId && view.docApp.searchFilterBySpecificBookId===book_info.docId)
|
|
712
|
+
{
|
|
713
|
+
resultsFoundinCurrentFilteredBook = true;
|
|
714
|
+
view.docApp.searchFilterBySpecificBookTitle = book_info.title; //-- set disaply title for filtered results by book
|
|
715
|
+
//-- for when user presses f5 - browser refresh for any reason
|
|
716
|
+
localStorage.setItem('hdocbook-last-filterbooktitle', book_info.title);
|
|
717
|
+
localStorage.setItem('hdocbook-last-filterbookid', book_info.docId);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
//-- alphabetical by display title for the Books filter list (server order
|
|
723
|
+
//-- is relevance-ish and reads as random)
|
|
724
|
+
sortBooksInfoByTitle();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if(view.docApp.searchResponse.results)
|
|
728
|
+
{
|
|
729
|
+
if(!resultsFoundinCurrentFilteredBook && view.docApp.searchFilterBySpecificBookId)
|
|
730
|
+
{
|
|
731
|
+
//-- the selected book has no results for this query, so it is absent from
|
|
732
|
+
//-- booksInfo - show it as a selected zero-count entry, or the Books list
|
|
733
|
+
//-- renders with NOTHING selected (not even All) and the user cannot tell
|
|
734
|
+
//-- a book filter is active. Title resolution: the title stored when the
|
|
735
|
+
//-- user picked the book, else the library (valid book, no matches), else
|
|
736
|
+
//-- the raw bid - a pasted url with an unknown/mistyped bid shows the bad
|
|
737
|
+
//-- id itself as the selected filter, which is the diagnostic //-- SG
|
|
738
|
+
let dummyTitle = null;
|
|
739
|
+
if(view.docApp.searchFilterBySpecificBookId===localStorage.getItem('hdocbook-last-filterbookid'))
|
|
740
|
+
{
|
|
741
|
+
dummyTitle = localStorage.getItem('hdocbook-last-filterbooktitle');
|
|
742
|
+
}
|
|
743
|
+
if(!dummyTitle)
|
|
744
|
+
{
|
|
745
|
+
let book_info = getBookById(view.docApp.searchFilterBySpecificBookId);
|
|
746
|
+
dummyTitle = (book_info && book_info.title) ? book_info.title : view.docApp.searchFilterBySpecificBookId;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
view.docApp.searchFilterBySpecificBookTitle = dummyTitle;
|
|
750
|
+
|
|
751
|
+
//-- a zero-result response may carry no booksInfo at all
|
|
752
|
+
if(!Array.isArray(view.docApp.searchResponse.booksInfo)) view.docApp.searchResponse.booksInfo = [];
|
|
753
|
+
|
|
754
|
+
let dummyBook = {bookId:view.docApp.searchFilterBySpecificBookId,title:dummyTitle,foundCount:0};
|
|
755
|
+
view.docApp.searchResponse.booksInfo.push(dummyBook);
|
|
756
|
+
//-- keep the list alphabetical with the dummy slotted in place
|
|
757
|
+
sortBooksInfoByTitle();
|
|
758
|
+
}
|
|
759
|
+
prepareSearchResultItems(view.docApp.searchResponse.results);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
view.$nextTick(function()
|
|
764
|
+
{
|
|
765
|
+
//-- no rebinding needed: link clicks are handled by one delegated
|
|
766
|
+
//-- handler on document, so freshly rendered result links just work
|
|
767
|
+
revealApp();
|
|
768
|
+
|
|
769
|
+
setTimeout(function()
|
|
770
|
+
{
|
|
771
|
+
if(view.docApp.currentResultsSearchText && !view.docApp.boolSearchInProgress)
|
|
772
|
+
{
|
|
773
|
+
//-- getSearchUrlLink() builds the same q/bid url this used to
|
|
774
|
+
//-- construct inline (third copy of the same expression)
|
|
775
|
+
let newUrlToSet = getSearchUrlLink();
|
|
776
|
+
if( window.location.href!==newUrlToSet) window.history.pushState(null, null, newUrlToSet);
|
|
777
|
+
}
|
|
778
|
+
},1000);
|
|
779
|
+
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
},function(err)
|
|
784
|
+
{
|
|
785
|
+
//-- an abort is us superseding our own request, not a failure - and a stale
|
|
786
|
+
//-- rejection must not surface an error over newer results. //-- SG
|
|
787
|
+
if(err && err.name === "AbortError") return;
|
|
788
|
+
if(requestSeq !== searchRequestSeq) return;
|
|
789
|
+
//-- results are no longer cleared at request time, so drop the previous ones
|
|
790
|
+
//-- here rather than leaving them under an error message //-- SG
|
|
791
|
+
view.docApp.searchResultFilteredItems = [];
|
|
792
|
+
view.docApp.boolSearchInProgress = false;
|
|
793
|
+
view.docApp.searchError = "Failed to search for books. The server search response is invalid";
|
|
794
|
+
revealApp();
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
else
|
|
798
|
+
{
|
|
799
|
+
view.docApp.searchMode = false;
|
|
800
|
+
view.docApp.mcpCatalogMode = false;
|
|
801
|
+
view.docApp.boolSearchInProgress = false;
|
|
802
|
+
|
|
803
|
+
//-- show prompt to enter search criteria
|
|
804
|
+
view.$nextTick(function()
|
|
805
|
+
{
|
|
806
|
+
revealApp();
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
//-- update browser url
|
|
811
|
+
if(!boolFromPopState || boolWasEnterKeyOrSearchButton)
|
|
812
|
+
{
|
|
813
|
+
const url = new URL(urlToSet);
|
|
814
|
+
window.history.pushState(null, null, url);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
},waitTime,inAltSearchText,inBoolFromPopState,boolWasEnterKeyOrSearchButton,ev); //-- passing in params
|
|
818
|
+
},
|
|
819
|
+
loadLibraryBook:function(aBook,fromPopState,ev)
|
|
820
|
+
{
|
|
821
|
+
view.docApp.boolSearchInProgress = false;
|
|
822
|
+
view.docApp.loadingBookInfo = true;
|
|
823
|
+
if(ev)
|
|
824
|
+
{
|
|
825
|
+
//-- if a url that is part of this doc site then handle loading content inline else let link do whatever (i.e. its an external link)
|
|
826
|
+
ev.stopImmediatePropagation();
|
|
827
|
+
ev.preventDefault();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Get hdocbook.json
|
|
831
|
+
//-- hide the current document while the next one loads. Was $(".HTL-doc").addClass
|
|
832
|
+
//-- ("hb-hidden"), which wrote to Vue-owned elements and also caught the MCP catalog
|
|
833
|
+
//-- wrapper and the inline toolbar - both .HTL-doc, neither intended. //-- SG
|
|
834
|
+
view.docApp.contentLoading = true;
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
fetchJsonFile("_books/" + aBook.docId + "/hdocbook.json").then(function(data)
|
|
838
|
+
{
|
|
839
|
+
view.docApp.book = data;
|
|
840
|
+
view.docApp.title = data.title || "";
|
|
841
|
+
view.docApp.description = data.description || "";
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
view.docApp.loadingBookInfo = false; //-- move this here into timeout to avoid loading glitch that shows main landing page
|
|
845
|
+
|
|
846
|
+
//-- now render out the nav menu
|
|
847
|
+
view.renderNavigation();
|
|
848
|
+
|
|
849
|
+
//-- This was a hard-coded setTimeout(...,200) "wait for Vue to render the nav".
|
|
850
|
+
//-- renderNavigation() is synchronous, so $nextTick is the correct primitive: it
|
|
851
|
+
//-- fires as soon as Vue has flushed the DOM patch instead of guessing, which
|
|
852
|
+
//-- takes a flat 200ms off every book open. Kept as $nextTick rather than dropped
|
|
853
|
+
//-- so the nav DOM is in place before content loading starts. //-- SG
|
|
854
|
+
view.$nextTick(function()
|
|
855
|
+
{
|
|
856
|
+
//-- title band is now rendered - centre the floating TOC button against it
|
|
857
|
+
positionTocMobileBtn();
|
|
858
|
+
|
|
859
|
+
//-- check if url is not root - in which case we need to load content for url
|
|
860
|
+
if(fromPopState)
|
|
861
|
+
{
|
|
862
|
+
loadContentUrl(fromPopState,false,fromPopState);
|
|
863
|
+
}
|
|
864
|
+
else
|
|
865
|
+
{
|
|
866
|
+
const loadUrl = new URL(window.location.href);
|
|
867
|
+
var initialLoadContentHref = removeStartingSlash(loadUrl.pathname.replace(window.location.origin+"/",""));
|
|
868
|
+
//if(initialLoadContentHref==="")initialLoadContentHref = findFirstClickableBookLink(initialLoadContentHref);
|
|
869
|
+
let foundLinkHref = findFirstClickableBookLink(initialLoadContentHref);
|
|
870
|
+
if(foundLinkHref)
|
|
871
|
+
{
|
|
872
|
+
//-- find the link we are going to show and make sure parent/s are set to .expand=true
|
|
873
|
+
loadContentUrl(foundLinkHref + loadUrl.hash,false,fromPopState);
|
|
874
|
+
}
|
|
875
|
+
else
|
|
876
|
+
{
|
|
877
|
+
hslDocLog.log("could not find matching link or parent link in navigation menu");
|
|
878
|
+
loadContentUrl(initialLoadContentHref + loadUrl.hash,false,fromPopState);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
//--
|
|
888
|
+
//-- user clicked back or forwards
|
|
889
|
+
var currentPopState = null;
|
|
890
|
+
window.addEventListener('popstate', function (event)
|
|
891
|
+
{
|
|
892
|
+
const loadUrl = new URL(event.target.location.href);
|
|
893
|
+
//loadContentUrl(loadUrl.pathname + loadUrl.hash,{},false,true);
|
|
894
|
+
currentPopState = event;
|
|
895
|
+
loadLibraryOrBookFromCurrentUrl(loadUrl.pathname + loadUrl.hash)
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
//--- GLOBAL FUNCTIONS
|
|
9
900
|
|
|
10
|
-
//--
|
|
11
|
-
//--
|
|
12
|
-
//--
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
901
|
+
//-- vertically centre the floating in-page TOC button (.toc-mobile-btn) against the book title
|
|
902
|
+
//-- band. The button is position:fixed inside .DocContent, so CSS alone cannot centre it on a
|
|
903
|
+
//-- band whose height varies with viewport width and title wrapping - measure the band and
|
|
904
|
+
//-- publish the result as a CSS variable consumed by content.css (tablet range only; the <=602px
|
|
905
|
+
//-- rules dock the button bottom-right and ignore the variable). Called after book render and on
|
|
906
|
+
//-- window resize. 48 = the button's CSS height. //-- SG
|
|
907
|
+
function positionTocMobileBtn()
|
|
908
|
+
{
|
|
909
|
+
let band = document.querySelector(".title-band");
|
|
910
|
+
if(band && band.offsetHeight)
|
|
911
|
+
{
|
|
912
|
+
let bandRect = band.getBoundingClientRect();
|
|
913
|
+
let btnTop = Math.round(bandRect.top + ((bandRect.height - 48) / 2));
|
|
914
|
+
document.documentElement.style.setProperty("--toc-mobile-btn-top", btnTop + "px");
|
|
915
|
+
}
|
|
916
|
+
else
|
|
917
|
+
{
|
|
918
|
+
//-- no band (library view) - fall back to the stylesheet default
|
|
919
|
+
document.documentElement.style.removeProperty("--toc-mobile-btn-top");
|
|
920
|
+
}
|
|
17
921
|
}
|
|
922
|
+
window.addEventListener("resize", positionTocMobileBtn);
|
|
923
|
+
|
|
924
|
+
//-- Minimum debounce between keystrokes and the search request going out. The adaptive
|
|
925
|
+
//-- times below learn from server response times, but never go below this: with hybrid
|
|
926
|
+
//-- (keyword + semantic) search each request costs the server an AI inference, so firing
|
|
927
|
+
//-- on nearly every keystroke wastes server work for results the next keystroke replaces.
|
|
928
|
+
//-- 250ms sits under the perceived-as-instant threshold while letting a steady typer
|
|
929
|
+
//-- finish the word. //-- SG
|
|
930
|
+
const SEARCH_DEBOUNCE_MIN_MS = 250;
|
|
931
|
+
|
|
932
|
+
function calculateSearchWaitTimeBasedOnQueryText(strSearchQuery)
|
|
933
|
+
{
|
|
934
|
+
let arrWords = strSearchQuery.split(" ");
|
|
935
|
+
//-- check length of each word
|
|
936
|
+
let waitTime = 0;
|
|
937
|
+
|
|
938
|
+
//-- do we have a last wait time for a similar search word length and text length?
|
|
939
|
+
let searchInfoKey = "s_w" + arrWords.length + "_l" + strSearchQuery.length;
|
|
940
|
+
let wordCountSearchKey = "s_w" + arrWords.length;
|
|
941
|
+
//-- do we have exact match (same word count and same number of chars?)
|
|
942
|
+
if(arrSearchResponseTimes[searchInfoKey] && arrSearchResponseTimes[searchInfoKey]<1000)
|
|
943
|
+
{
|
|
944
|
+
//-- lets just use this + 20ms, but never below the floor
|
|
945
|
+
return Math.max(arrSearchResponseTimes[searchInfoKey] + 20, SEARCH_DEBOUNCE_MIN_MS);
|
|
946
|
+
}
|
|
947
|
+
delete arrSearchResponseTimes[searchInfoKey];
|
|
948
|
+
|
|
949
|
+
if(arrSearchResponseTimesByWordCount[wordCountSearchKey] && arrWords.length>1 && arrSearchResponseTimesByWordCount[wordCountSearchKey]<1000)
|
|
950
|
+
{
|
|
951
|
+
return Math.max(arrSearchResponseTimesByWordCount[wordCountSearchKey] + 20, SEARCH_DEBOUNCE_MIN_MS);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
delete arrSearchResponseTimesByWordCount[wordCountSearchKey];
|
|
955
|
+
|
|
956
|
+
//-- we have no reference to use
|
|
957
|
+
//-- set initial waittime based of search text length
|
|
958
|
+
switch(strSearchQuery.length) {
|
|
959
|
+
case 0:
|
|
960
|
+
case 1:
|
|
961
|
+
case 2:
|
|
962
|
+
waitTime = 400;
|
|
963
|
+
break;
|
|
964
|
+
case 3:
|
|
965
|
+
waitTime = 300;
|
|
966
|
+
break;
|
|
967
|
+
case 4:
|
|
968
|
+
waitTime = 200;
|
|
969
|
+
break;
|
|
970
|
+
case 5:
|
|
971
|
+
waitTime = 100;
|
|
972
|
+
break;
|
|
973
|
+
default:
|
|
974
|
+
waitTime = 50;
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
//-- now divide by number of words, for example searching for "apple" takes longer than searching for "ap ple";
|
|
979
|
+
waitTime = waitTime / arrWords.length;
|
|
980
|
+
if(waitTime<SEARCH_DEBOUNCE_MIN_MS)waitTime=SEARCH_DEBOUNCE_MIN_MS;
|
|
981
|
+
|
|
982
|
+
return waitTime;
|
|
983
|
+
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
//-- copyObject() lived here. Its only caller was the per-keystroke search log (item 13), which
|
|
987
|
+
//-- now logs the three fields it wanted rather than deep-copying a jQuery event. //-- SG
|
|
988
|
+
|
|
989
|
+
function encodeQueryData(data) {
|
|
990
|
+
const ret = [];
|
|
991
|
+
for (let d in data)
|
|
992
|
+
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
|
|
993
|
+
return ret.join('&');
|
|
994
|
+
}
|
|
995
|
+
function removeTrailingSlash(str) {
|
|
996
|
+
return str.endsWith('/') ? str.slice(0, -1) : str;
|
|
997
|
+
}
|
|
998
|
+
function removeStartingSlash(str) {
|
|
999
|
+
return str.startsWith('/') ? str.slice(1, str.length) : str;
|
|
1000
|
+
|
|
1001
|
+
//if(linkRef.indexOf("/")===0)linkRef = linkRef.replace("/","");
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function urlGetParam(locationSearch,paramName)
|
|
1005
|
+
{
|
|
1006
|
+
let params = new URLSearchParams(locationSearch);
|
|
1007
|
+
return params.get(paramName);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function toSeoUrl(url) {
|
|
1011
|
+
|
|
1012
|
+
return url.toString() // Convert to string
|
|
1013
|
+
.normalize('NFD') // Change diacritics
|
|
1014
|
+
.replace(/[\u0300-\u036f]/g,'') // Remove illegal characters
|
|
1015
|
+
.replace(/\s+/g,'-') // Change whitespace to dashes
|
|
1016
|
+
.toLowerCase() // Change to lowercase
|
|
1017
|
+
.replace(/&/g,'-and-') // Replace ampersand
|
|
1018
|
+
.replace(/[^a-z0-9\-]/g,'') // Remove anything that is not a letter, number or dash
|
|
1019
|
+
.replace(/-+/g,'-') // Remove duplicate dashes
|
|
1020
|
+
.replace(/^-*/,'') // Remove starting dashes
|
|
1021
|
+
.replace(/-*$/,''); // Remove trailing dashes
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
function getMeta(metaName) {
|
|
1026
|
+
const metas = document.getElementsByTagName('meta');
|
|
1027
|
+
|
|
1028
|
+
for (let i = 0; i < metas.length; i++) {
|
|
1029
|
+
if (metas[i].getAttribute('name') === metaName) {
|
|
1030
|
+
return metas[i].getAttribute('content');
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return '';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getAnchorFromHash(strHash,strHasClass)
|
|
1038
|
+
{
|
|
1039
|
+
try{
|
|
1040
|
+
let strSelector = toSeoUrl(strHash.replace("#",""));
|
|
1041
|
+
if (strSelector === '') return null;
|
|
1042
|
+
if(strHasClass)strSelector += "." + strHasClass;
|
|
1043
|
+
|
|
1044
|
+
//-- returns a DOM element (was a jQuery collection, so the caller unwrapped [0])
|
|
1045
|
+
return document.querySelector("#"+strSelector) || null;
|
|
1046
|
+
}
|
|
1047
|
+
catch(e)
|
|
1048
|
+
{
|
|
1049
|
+
hslDocLog.log("Get document anchor element by hash failed ["+strHash+"]",e);
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
//-- Resolve a hash fragment to a DOM element on the current page. Tolerant of headings that
|
|
1056
|
+
//-- were never assigned an id: generateTableOfContentsFromDoc() only gives ids to H2/H3 that
|
|
1057
|
+
//-- have a text label and no .no-auto-anchor class, so links to H1/H4-H6 (or those headings)
|
|
1058
|
+
//-- would otherwise fail and dump the reader at the top of the page. As a last resort we scan
|
|
1059
|
+
//-- every heading, match on its slugified text, and back-fill the id so scroll + future lookups
|
|
1060
|
+
//-- work. Returns a DOM element or null. //-- SG
|
|
1061
|
+
function resolveAnchorElement(strHash)
|
|
1062
|
+
{
|
|
1063
|
+
try
|
|
1064
|
+
{
|
|
1065
|
+
let frag = (strHash || "").replace(/^#/, "");
|
|
1066
|
+
if(!frag) return null;
|
|
1067
|
+
let slug = toSeoUrl(frag);
|
|
1068
|
+
if(!slug) return null;
|
|
1069
|
+
|
|
1070
|
+
//-- 1) direct id match (SEO slug, or an id the author set verbatim)
|
|
1071
|
+
let ele = document.getElementById(slug) || document.getElementById(frag);
|
|
1072
|
+
if(ele) return ele;
|
|
1073
|
+
|
|
1074
|
+
//-- 2) legacy <a name="..."> / [name] targets
|
|
1075
|
+
let named = document.querySelector('[name="' + ((window.CSS && CSS.escape) ? CSS.escape(frag) : frag) + '"]');
|
|
1076
|
+
if(named) return named;
|
|
1077
|
+
|
|
1078
|
+
//-- 3) match any heading by its slugified text and back-fill the id
|
|
1079
|
+
let container = vueContentContainer() || document;
|
|
1080
|
+
let headings = container.querySelectorAll("h1,h2,h3,h4,h5,h6");
|
|
1081
|
+
for(let i=0; i<headings.length; i++)
|
|
1082
|
+
{
|
|
1083
|
+
//-- native .trim(): jQuery 4 removed $.trim() //-- SG
|
|
1084
|
+
if(toSeoUrl(headingText(headings[i]))===slug)
|
|
1085
|
+
{
|
|
1086
|
+
if(!headings[i].id) headings[i].id = slug;
|
|
1087
|
+
return headings[i];
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
catch(e)
|
|
1093
|
+
{
|
|
1094
|
+
hslDocLog.log("Resolve document anchor element by hash failed ["+strHash+"]",e);
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
//-- Accessibility fixes over freshly-injected article content. These used to live inside
|
|
1100
|
+
//-- listenForHrefClicks(), so they re-scanned the whole document every time a click handler
|
|
1101
|
+
//-- was rebound - several times per navigation - even though the DOM they fix only changes
|
|
1102
|
+
//-- when new content is injected. Note the attribute-substring selectors below cannot use
|
|
1103
|
+
//-- any index and walk every element, so keeping this to once per page matters. //-- SG
|
|
1104
|
+
function applyContentAccessibilityFixes()
|
|
1105
|
+
{
|
|
1106
|
+
//-- Accessibility fix: add aria-label and id to menu popper anchors
|
|
1107
|
+
document.querySelectorAll(".documentation-menu-popper").forEach(function(el,index) {
|
|
1108
|
+
if(!el.getAttribute("aria-label")) {
|
|
1109
|
+
el.setAttribute("aria-label", "Document options menu");
|
|
1110
|
+
el.setAttribute("id", "doc-menu-popper-" + index);
|
|
1111
|
+
el.querySelectorAll("i").forEach(function(i){ i.setAttribute("aria-hidden","true"); });
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
//-- Accessibility fix: add aria-label to edit links (GitHub pencil icon)
|
|
1116
|
+
document.querySelectorAll(".toolbar-action").forEach(function(link) {
|
|
1117
|
+
let href = link.getAttribute("href") || "";
|
|
1118
|
+
if(href.includes("github.com") && link.querySelector(".bi-pencil") && !link.getAttribute("aria-label")) {
|
|
1119
|
+
link.setAttribute("aria-label", "Edit this page on GitHub");
|
|
1120
|
+
link.querySelectorAll("i").forEach(function(i){ i.setAttribute("aria-hidden","true"); });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
//-- Accessibility fix: override inline gray colors for WCAG AAA compliance
|
|
1125
|
+
var isDarkMode = document.documentElement.classList.contains('dark');
|
|
1126
|
+
var accessibleGray = isDarkMode ? "#C4C4C4" : "#525252"; /* C4C4C4 = 7.5:1 on #242424 */
|
|
1127
|
+
document.querySelectorAll("[style*='color: gray'], [style*='color:gray'], [style*='color: rgb(82'], [style*='color:rgb(82']").forEach(function(el) {
|
|
1128
|
+
el.style.color = accessibleGray;
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
//-- Trap link clicks so we can cancel them and load content inline instead.
|
|
1133
|
+
//-- Bound ONCE on document as a delegated handler by bindDelegatedLinkHandler() below.
|
|
1134
|
+
//-- It used to be attached directly to every matching element and torn down/rebound on
|
|
1135
|
+
//-- every content injection - thousands of unbind/bind cycles per navigation on a large
|
|
1136
|
+
//-- API reference page - which is also why every code path that injected markup had to
|
|
1137
|
+
//-- remember to re-run the binder. Delegation removes that constraint entirely: anchors
|
|
1138
|
+
//-- created later (by Vue, by the tab strip, by anything) are handled with no rebinding.
|
|
1139
|
+
//--
|
|
1140
|
+
//-- `this` is still the matched element, so the body is unchanged from the bound version.
|
|
1141
|
+
//-- Ordering note: Vue's own @click handlers sit directly on their elements and therefore
|
|
1142
|
+
//-- run first now. The ones that matter (loadLibraryBook, searchDocuments) call
|
|
1143
|
+
//-- stopImmediatePropagation(), so they still suppress this handler - more reliably than
|
|
1144
|
+
//-- before, when it came down to which handler happened to be registered first. //-- SG
|
|
1145
|
+
function handleDocumentLinkClick(ev)
|
|
1146
|
+
{
|
|
1147
|
+
let ele = this;
|
|
1148
|
+
///hslDocLog.log("OnAClick: ", ele.getAttribute("href"));
|
|
1149
|
+
|
|
1150
|
+
//-- clicked the header - so get teh child link
|
|
1151
|
+
if(ele.tagName==="H2" || ele.tagName==="H3")
|
|
1152
|
+
{
|
|
1153
|
+
ele = ele.querySelector(".header-anchor");
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if(ele && ele.href)
|
|
1157
|
+
{
|
|
1158
|
+
//hslDocLog.log("XXX: ", ele.getAttribute("href"));
|
|
1159
|
+
const checkUrl = new URL(ele.href);
|
|
1160
|
+
|
|
1161
|
+
//-- same-page fragment link (raw href starts with #). Because <base href> is the
|
|
1162
|
+
//-- site root, a bare "#anchor" resolves ele.href to the site root + hash (not the
|
|
1163
|
+
//-- current page), so the book-loading logic below can't detect it. Handle it here:
|
|
1164
|
+
//-- just scroll to the matching anchor on the current page. //-- SG
|
|
1165
|
+
let rawHref = ele.getAttribute("href");
|
|
1166
|
+
if(rawHref && rawHref.charAt(0) === "#")
|
|
1167
|
+
{
|
|
1168
|
+
ev.stopImmediatePropagation();
|
|
1169
|
+
ev.preventDefault();
|
|
1170
|
+
let anchorEle = resolveAnchorElement(rawHref);
|
|
1171
|
+
if(anchorEle)
|
|
1172
|
+
{
|
|
1173
|
+
GotoAndHighlightAnchorContent(anchorEle, false); //-- false = also run border highlight animation, like a direct anchor visit
|
|
1174
|
+
let currUrl = new URL(document.location.href);
|
|
1175
|
+
currUrl.hash = rawHref;
|
|
1176
|
+
if(currUrl.href !== document.location.href) window.history.pushState(null, null, currUrl); //-- add state so back button works
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
//-- clicking on a hash tag header link element - so jsut scroll to element
|
|
1182
|
+
if(checkUrl.href===siteBaseLocation)
|
|
1183
|
+
{
|
|
1184
|
+
//-- we want to go to home page
|
|
1185
|
+
}
|
|
1186
|
+
else if(checkUrl.origin +"/"===siteBaseLocation && ele.getAttribute("target")!=="_blank")
|
|
1187
|
+
{
|
|
1188
|
+
|
|
1189
|
+
//-- if a url that is part of this doc site then handle loading content inline else let link do whatever (i.e. its an external link)
|
|
1190
|
+
ev.stopImmediatePropagation();
|
|
1191
|
+
ev.preventDefault();
|
|
1192
|
+
|
|
1193
|
+
//-- a search result click always loads its book, whatever the url shape - this
|
|
1194
|
+
//-- test used to be duplicated token-for-token inside both arms of the anchor
|
|
1195
|
+
//-- check below, so it is hoisted in front of it //-- SG
|
|
1196
|
+
if(ele.classList.contains("searchresult-href"))
|
|
1197
|
+
{
|
|
1198
|
+
//-- we are loading a search result from a book
|
|
1199
|
+
let loadBookId = ele.getAttribute("bookid");
|
|
1200
|
+
hslDocLog.log("Search Result Click : Load Book ("+loadBookId+") Then ContentURL: ", ele.getAttribute("href"));
|
|
1201
|
+
|
|
1202
|
+
//-- remember search history
|
|
1203
|
+
storeUrlInHistory(getSearchUrlLink());
|
|
1204
|
+
|
|
1205
|
+
//-- now set new url for doc we want to load
|
|
1206
|
+
storeUrlInHistory(ele.getAttribute("href"));
|
|
1207
|
+
|
|
1208
|
+
view.docApp.searchMode = false;
|
|
1209
|
+
view.docApp.mcpCatalogMode = false;
|
|
1210
|
+
view.loadLibraryBook({docId:loadBookId},ele.getAttribute("href"));
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
//-- do we need to scroll into view element on this page
|
|
1215
|
+
//-- add fix make use url clicked on is same path as current one
|
|
1216
|
+
let eleAnc = (checkUrl.pathname===document.location.pathname && checkUrl.hash)?getAnchorFromHash(checkUrl.hash,"faq-toc-item"):null;
|
|
1217
|
+
if(eleAnc)
|
|
1218
|
+
{
|
|
1219
|
+
GotoAndHighlightAnchorContent(eleAnc,true);
|
|
1220
|
+
if(checkUrl.pathname==="/")
|
|
1221
|
+
{
|
|
1222
|
+
//-- going to element on current page so just update current browser url with new #
|
|
1223
|
+
let currUrl = new URL(document.location.href);
|
|
1224
|
+
currUrl.hash=checkUrl.hash;
|
|
1225
|
+
if(currUrl.href!==document.location.href) window.history.pushState(null, null, currUrl); //-- user has clicked a link so we want to add state to history so we can click back
|
|
1226
|
+
}
|
|
1227
|
+
else if(ele.href!==document.location.href)
|
|
1228
|
+
{
|
|
1229
|
+
window.history.pushState(null, null, ele.href); //-- user has clicked a link so we want to add state to history so we can click back
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
else
|
|
1233
|
+
{
|
|
1234
|
+
//-- we need to check if the bookId is the same as current one -if it is not then need to do a full load
|
|
1235
|
+
let targetUrl = checkUrl.href.replace("/_books/","/");
|
|
1236
|
+
let targetBookId = targetUrl.replace(checkUrl.origin +"/","").split("/",1)[0];
|
|
1237
|
+
let currentBookId = document.location.href.replace(document.location.origin +"/","").split("/",1)[0];
|
|
1238
|
+
if(targetBookId==currentBookId)
|
|
1239
|
+
{
|
|
1240
|
+
//-- load content inline - no need to change book
|
|
1241
|
+
hslDocLog.log("LoadContentURL: ", targetUrl);
|
|
1242
|
+
loadContentUrl(targetUrl,false,false);
|
|
1243
|
+
}
|
|
1244
|
+
else
|
|
1245
|
+
{
|
|
1246
|
+
//-- load new book
|
|
1247
|
+
hslDocLog.log("Load new book ("+targetBookId+") URL : ", targetUrl);
|
|
1248
|
+
findBookByIdAndLoad(targetBookId,targetUrl);
|
|
1249
|
+
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
else
|
|
1255
|
+
{
|
|
1256
|
+
//-- if not part of this website then always open in new tab as considered an external site
|
|
1257
|
+
ele.setAttribute("target","_blank");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
//-- Attach the single delegated click handler. Called once from intialiseApp().
|
|
1263
|
+
var boolDelegatedLinkHandlerBound = false;
|
|
1264
|
+
function bindDelegatedLinkHandler()
|
|
1265
|
+
{
|
|
1266
|
+
if(boolDelegatedLinkHandlerBound) return;
|
|
1267
|
+
boolDelegatedLinkHandlerBound = true;
|
|
1268
|
+
document.addEventListener("click",function(ev){
|
|
1269
|
+
//-- native equivalent of jQuery's delegated .on(selector,...): find the nearest matching
|
|
1270
|
+
//-- ancestor of the event target and invoke the handler with `this` set to it, which is
|
|
1271
|
+
//-- what the handler body reads. //-- SG
|
|
1272
|
+
let matched = ev.target && ev.target.closest ? ev.target.closest("a, h2.faq-toc-item, h3.faq-toc-item") : null;
|
|
1273
|
+
if(matched) handleDocumentLinkClick.call(matched,ev);
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
var lastSavedUrl = window.location.href;
|
|
1278
|
+
function storeUrlInHistory(strUrl)
|
|
1279
|
+
{
|
|
1280
|
+
if(strUrl!==lastSavedUrl)
|
|
1281
|
+
{
|
|
1282
|
+
window.history.pushState(null, null, strUrl);
|
|
1283
|
+
lastSavedUrl = strUrl;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
//-- The one place the /search query params are assembled, for BOTH the _search request and
|
|
1288
|
+
//-- the browser url, so the two can never disagree. They did: this used to build q/bid only
|
|
1289
|
+
//-- while searchDocuments built q/bid/audience/dt, and the delayed pushState in the response
|
|
1290
|
+
//-- handler then overwrote the correct url with the short one - a filtered search could not
|
|
1291
|
+
//-- be copied, bookmarked or back-buttoned, and popstate (which does read dt/audience) had
|
|
1292
|
+
//-- nothing to restore. Add any new facet here, not at a call site. //-- SG
|
|
1293
|
+
function buildSearchQueryParams(searchText)
|
|
1294
|
+
{
|
|
1295
|
+
let queryParams = {q:searchText};
|
|
1296
|
+
if(view.docApp.searchFilterBySpecificBookId) queryParams.bid = view.docApp.searchFilterBySpecificBookId;
|
|
1297
|
+
if(view.docApp.showAudienceFilter && view.docApp.searchAudienceFilter) queryParams.audience = view.docApp.searchAudienceFilter;
|
|
1298
|
+
if(view.docApp.searchDocTypeFilter) queryParams.dt = view.docApp.searchDocTypeFilter;
|
|
1299
|
+
return queryParams;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
//-- based on current search criteria return what the window url would be
|
|
1303
|
+
function getSearchUrlLink()
|
|
1304
|
+
{
|
|
1305
|
+
let urlToSet = window.location.origin + "/search";
|
|
1306
|
+
let searchText = view.docApp.currentResultsSearchText;
|
|
1307
|
+
if(searchText)
|
|
1308
|
+
{
|
|
1309
|
+
urlToSet += "?" + encodeQueryData(buildSearchQueryParams(searchText));
|
|
1310
|
+
}
|
|
1311
|
+
return urlToSet;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
//-- getProductFamilyCount() lived here. Its only caller, resetFilteredProductsListCounters(),
|
|
1316
|
+
//-- now builds one Map of the counts instead of rescanning productFamiliesInfo per product. //-- SG
|
|
1317
|
+
|
|
1318
|
+
function resetCheckedProducts()
|
|
1319
|
+
{
|
|
1320
|
+
for(let x=0;x<view.docApp.products.length;x++)
|
|
1321
|
+
{
|
|
1322
|
+
view.docApp.products[x].checked=false;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function resetFilteredProductsListCounters()
|
|
1326
|
+
{
|
|
1327
|
+
//-- one pass over productFamiliesInfo instead of getProductFamilyCount() rescanning it per
|
|
1328
|
+
//-- product //-- SG
|
|
1329
|
+
let familyCounts = new Map();
|
|
1330
|
+
if(view.docApp.searchResponse && view.docApp.searchResponse.productFamiliesInfo)
|
|
1331
|
+
{
|
|
1332
|
+
let productFamiliesInfo = view.docApp.searchResponse.productFamiliesInfo;
|
|
1333
|
+
for(let x=0;x<productFamiliesInfo.length;x++) familyCounts.set(productFamiliesInfo[x].productFamilyId,productFamiliesInfo[x].foundCount);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
view.docApp.searchFilteredProducts["unknown"] = {checked:false,returnedCount:0};
|
|
1337
|
+
for(let x=0;x<view.docApp.products.length;x++)
|
|
1338
|
+
{
|
|
1339
|
+
let strProductID = view.docApp.products[x].id;
|
|
1340
|
+
if(!view.docApp.searchFilteredProducts[strProductID])
|
|
1341
|
+
{
|
|
1342
|
+
view.docApp.searchFilteredProducts[strProductID] = {};
|
|
1343
|
+
}
|
|
1344
|
+
view.docApp.searchFilteredProducts[strProductID].checked = view.docApp.products[x].checked || false;
|
|
1345
|
+
view.docApp.searchFilteredProducts[strProductID].returnedCount = familyCounts.get(strProductID) || 0;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function updateBooksEnableDisableStateBasedOnFilteredProducts(checkedProdArray)
|
|
1350
|
+
{
|
|
1351
|
+
//-- booksInfo is a field on the server's search response, so treat it as optional rather than
|
|
1352
|
+
//-- assuming every response carries one //-- SG
|
|
1353
|
+
if(!view.docApp.searchResponse || !view.docApp.searchResponse.booksInfo) return;
|
|
1354
|
+
|
|
1355
|
+
//-- only enable/disable books filter options depending on if book.productFamilyId is one of the checked products
|
|
1356
|
+
view.docApp.searchResponse.booksInfo.forEach( function(aBook){
|
|
1357
|
+
if(checkedProdArray.length)
|
|
1358
|
+
{
|
|
1359
|
+
aBook.productFilterDisable = true;
|
|
1360
|
+
//-- one or more products checked so only enable books that match checked products
|
|
1361
|
+
checkedProdArray.forEach(function(aProductId)
|
|
1362
|
+
{
|
|
1363
|
+
//-- this book belongs to a product family that is checked so enable it's selection
|
|
1364
|
+
if(view.docApp.searchFilteredProducts[aProductId] && view.docApp.searchFilteredProducts[aProductId].checked && aBook.productFamilyId===aProductId)aBook.productFilterDisable = false;
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
else
|
|
1368
|
+
{
|
|
1369
|
+
aBook.productFilterDisable = false;
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
//-- Books filter list order - alphabetical, case-insensitive, by display title (bookId fallback
|
|
1378
|
+
//-- covers entries whose title did not resolve from the library) //-- SG
|
|
1379
|
+
function sortBooksInfoByTitle()
|
|
1380
|
+
{
|
|
1381
|
+
if(!view.docApp.searchResponse || !Array.isArray(view.docApp.searchResponse.booksInfo)) return;
|
|
1382
|
+
view.docApp.searchResponse.booksInfo.sort(function(a,b)
|
|
1383
|
+
{
|
|
1384
|
+
return (a.title || a.bookId || "").localeCompare(b.title || b.bookId || "", undefined, {sensitivity:"base"});
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function prepareSearchResultItems(arrSearchResults)
|
|
1389
|
+
{
|
|
1390
|
+
let checkedProdArray = getCheckedProductFilterArray();
|
|
1391
|
+
resetFilteredProductsListCounters();
|
|
1392
|
+
updateBooksEnableDisableStateBasedOnFilteredProducts(checkedProdArray);
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
//hslDocLog.log(view.docApp.searchFilteredProducts)
|
|
1398
|
+
//-- built locally and assigned once at the end: pushing onto the reactive array fired 300
|
|
1399
|
+
//-- separate reactive triggers, and left the model briefly empty part-way through. //-- SG
|
|
1400
|
+
let filteredItems = [];
|
|
1401
|
+
for(let x=0;x<arrSearchResults.length;x++)
|
|
1402
|
+
{
|
|
1403
|
+
//-- if we don't have a title get it from content ?? bit flakey
|
|
1404
|
+
if(!arrSearchResults[x].title)
|
|
1405
|
+
{
|
|
1406
|
+
arrSearchResults[x].title = arrSearchResults[x].content.split("\n\n",1)[0];
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
//-- create link from url (remove the index.html)
|
|
1410
|
+
arrSearchResults[x].url = arrSearchResults[x].url.replace("/index.html","");
|
|
1411
|
+
|
|
1412
|
+
//-- Replace snippet highlights. The snippet is rendered with v-html (the only way to
|
|
1413
|
+
//-- show the <mark> highlights), and doc content can contain literal HTML - example
|
|
1414
|
+
//-- code, event handlers - so it MUST be escaped first or it executes in the searcher's
|
|
1415
|
+
//-- browser (stored XSS). Escape everything, then turn the plain-text highlight markers
|
|
1416
|
+
//-- into the only tags allowed through.
|
|
1417
|
+
//-- Once per record only: this function re-runs over the SAME response array on every
|
|
1418
|
+
//-- product/book filter click, and escaping is not idempotent - a second pass turned
|
|
1419
|
+
//-- the first pass's <mark> tags into visible "<mark>" text, worse each click,
|
|
1420
|
+
//-- until a fresh search replaced the records. //-- SG
|
|
1421
|
+
if(!arrSearchResults[x].content)arrSearchResults[x].content = "";
|
|
1422
|
+
if(!arrSearchResults[x].contentPrepared)
|
|
1423
|
+
{
|
|
1424
|
+
arrSearchResults[x].content = arrSearchResults[x].content
|
|
1425
|
+
.replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''')
|
|
1426
|
+
.replaceAll('[hb_search_highlight]', '<mark class="highlight">').replaceAll('[/hb_search_highlight]', '</mark>');
|
|
1427
|
+
arrSearchResults[x].contentPrepared = true;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
//-- increase count for product so we can show on search filters
|
|
1431
|
+
let resProdId = arrSearchResults[x].productFamily || "unknown";
|
|
1432
|
+
//-- nwj - 15th Oct 2024 - we have to do this manually as counts for products returned from search arent useful
|
|
1433
|
+
//if(view.docApp.searchFilteredProducts[resProdId])view.docApp.searchFilteredProducts[resProdId].returnedCount++;
|
|
1434
|
+
|
|
1435
|
+
let includeInResults = true;
|
|
1436
|
+
|
|
1437
|
+
if(checkedProdArray.length) //-- are nay checked
|
|
1438
|
+
{
|
|
1439
|
+
if(view.docApp.searchFilteredProducts[resProdId])
|
|
1440
|
+
{
|
|
1441
|
+
includeInResults = view.docApp.searchFilteredProducts[resProdId].checked;
|
|
1442
|
+
}
|
|
1443
|
+
else
|
|
1444
|
+
{
|
|
1445
|
+
includeInResults = false;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
//-- need to filter by checked book if have any checked
|
|
1450
|
+
if(includeInResults && view.docApp.searchFilterBySpecificBookId)
|
|
1451
|
+
{
|
|
1452
|
+
includeInResults = (arrSearchResults[x].bookId === view.docApp.searchFilterBySpecificBookId);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if(includeInResults)
|
|
1456
|
+
{
|
|
1457
|
+
filteredItems.push(arrSearchResults[x]);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
//-- one assignment, one render, and the list is replaced rather than emptied then refilled
|
|
1462
|
+
view.docApp.searchResultFilteredItems = filteredItems;
|
|
1463
|
+
|
|
1464
|
+
//- if we have filter by products update the counts of books to 0 or # of results in books
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function getCheckedProductFilterArray()
|
|
1468
|
+
{
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
let retArray = [];
|
|
1473
|
+
//-- now check if result is ok if we have any product filter set
|
|
1474
|
+
for(let productId in view.docApp.searchFilteredProducts)
|
|
1475
|
+
{
|
|
1476
|
+
if(view.docApp.searchFilteredProducts[productId].checked)
|
|
1477
|
+
{
|
|
1478
|
+
retArray.push(productId);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return retArray;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
//-- Lookup indexes for the two lists that get scanned per search response. getBookById was
|
|
1485
|
+
//-- called once per entry in booksInfo and scanned all of library.books each time - O(n*m) on
|
|
1486
|
+
//-- every keystroke-triggered search. The index is rebuilt lazily whenever the underlying array
|
|
1487
|
+
//-- is replaced (identity) or changes length, so no load path has to remember to invalidate it.
|
|
1488
|
+
//-- //-- SG
|
|
1489
|
+
let libraryBooksIndex = null;
|
|
1490
|
+
let libraryBooksIndexSource = null;
|
|
1491
|
+
function getLibraryBooksIndex()
|
|
1492
|
+
{
|
|
1493
|
+
let books = (view.docApp.library && view.docApp.library.books) || [];
|
|
1494
|
+
if(libraryBooksIndexSource !== books || !libraryBooksIndex || libraryBooksIndex.size !== books.length)
|
|
1495
|
+
{
|
|
1496
|
+
libraryBooksIndex = new Map();
|
|
1497
|
+
for(let x=0;x<books.length;x++) libraryBooksIndex.set(books[x].docId,books[x]);
|
|
1498
|
+
libraryBooksIndexSource = books;
|
|
1499
|
+
}
|
|
1500
|
+
return libraryBooksIndex;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function getBookById(strBookId)
|
|
1504
|
+
{
|
|
1505
|
+
return getLibraryBooksIndex().get(strBookId);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
//-- based on content url highlight match navigation menu item
|
|
1509
|
+
function highlightNavigationLinkFromUrl(matchLinkHref)
|
|
1510
|
+
{
|
|
1511
|
+
|
|
1512
|
+
matchLinkHref = removeStartingSlash(matchLinkHref).split("#")[0]; //-- remvoe # link
|
|
1513
|
+
document.querySelectorAll("a.DocLink").forEach(function(el){ el.classList.remove("active"); });
|
|
1514
|
+
|
|
1515
|
+
let arrItems = document.querySelectorAll('a.DocLink');
|
|
1516
|
+
|
|
1517
|
+
for(let x=0;x<arrItems.length;x++)
|
|
1518
|
+
{
|
|
1519
|
+
let el = arrItems[x];
|
|
1520
|
+
let checkUrl = null;
|
|
1521
|
+
try{
|
|
1522
|
+
checkUrl = new URL(el.href);
|
|
1523
|
+
}
|
|
1524
|
+
catch(e)
|
|
1525
|
+
{
|
|
1526
|
+
//hslDocLog.log("BAD navigation menu item found",el);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if(checkUrl)
|
|
1530
|
+
{
|
|
1531
|
+
let testCurrPath = removeStartingSlash(checkUrl.pathname);
|
|
1532
|
+
if(testCurrPath===matchLinkHref || testCurrPath.indexOf(matchLinkHref)===0 || matchLinkHref.indexOf(removeStartingSlash(checkUrl.pathname) + "/")===0)
|
|
1533
|
+
{
|
|
1534
|
+
//-- exact match
|
|
1535
|
+
el.classList.add("active")
|
|
1536
|
+
if(!elementIsOnVisibleScreen(el))el.scrollIntoView();
|
|
1537
|
+
return false;//break out
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
//-- create items to stick in toc from current loaded content
|
|
1544
|
+
//-- searches for H2,H3
|
|
1545
|
+
//-- Heading text for slugging, WITHOUT depending on the heading being rendered.
|
|
1546
|
+
//--
|
|
1547
|
+
//-- This used to read innerText in both callers. innerText is layout-dependent: it returns ""
|
|
1548
|
+
//-- for anything not currently rendered. That was safe only while the content reveal was a
|
|
1549
|
+
//-- synchronous class removal ($(".hb-hidden").removeClass). The reveal is now reactive state
|
|
1550
|
+
//-- (docApp.contentLoading), which Vue applies on the NEXT tick - and generateTableOfContents-
|
|
1551
|
+
//-- FromDoc runs on the tick before it, while the article is still visibility:hidden. Every
|
|
1552
|
+
//-- heading therefore read as empty: no ids assigned, so no #anchor deep links resolved and no
|
|
1553
|
+
//-- section highlight fired, and the table of contents came back empty so the right-hand nav
|
|
1554
|
+
//-- never rendered.
|
|
1555
|
+
//--
|
|
1556
|
+
//-- textContent has no layout dependency. It is equivalent for these headings because the only
|
|
1557
|
+
//-- thing appended to them is <a class="header-anchor"><i></i></a>, which carries no text.
|
|
1558
|
+
//-- Whitespace is collapsed so the slug matches what innerText produced. //-- SG
|
|
1559
|
+
function headingText(el)
|
|
1560
|
+
{
|
|
1561
|
+
return ((el && el.textContent) || "").replace(/\s+/g," ").trim();
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function generateTableOfContentsFromDoc()
|
|
1565
|
+
{
|
|
1566
|
+
let container = vueContentContainer();
|
|
1567
|
+
if(container)
|
|
1568
|
+
{
|
|
1569
|
+
const currUrl = new URL(document.location.href);
|
|
1570
|
+
view.docApp.tableOfContents = [];
|
|
1571
|
+
for (let i=2; i<=3; i++)
|
|
1572
|
+
{
|
|
1573
|
+
let headers = container.getElementsByTagName("h"+i);
|
|
1574
|
+
for (let j=0; j<headers.length; j++)
|
|
1575
|
+
{
|
|
1576
|
+
//-- only process headers that do not have this class
|
|
1577
|
+
if(!headers[j].classList.contains("no-auto-anchor")) headers[j].classList.add("faq-toc-item");
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
//-- track ids handed out on this page so two headings with the same text do not
|
|
1582
|
+
//-- both claim the same anchor - deep links used to always land on the first. //-- SG
|
|
1583
|
+
let usedHeadingIds = {};
|
|
1584
|
+
|
|
1585
|
+
var headersTocItem = container.getElementsByClassName("faq-toc-item");
|
|
1586
|
+
for (let k = 0; k < headersTocItem.length; k++)
|
|
1587
|
+
{
|
|
1588
|
+
let item = headersTocItem[k];
|
|
1589
|
+
let strLabel = headingText(item);
|
|
1590
|
+
|
|
1591
|
+
//-- Handle badge-only headings (accessibility fix)
|
|
1592
|
+
if(!strLabel) {
|
|
1593
|
+
let badge = item.querySelector("[class*='badge-endpoint-'], [class*='badge-user-'], [class*='badge-portal-']");
|
|
1594
|
+
if(badge) {
|
|
1595
|
+
let badgeClass = badge.getAttribute("class") || "";
|
|
1596
|
+
let badgeText = "API Type";
|
|
1597
|
+
if(badgeClass.includes("internal")) badgeText = "Internal API";
|
|
1598
|
+
else if(badgeClass.includes("api")) badgeText = "API Endpoint";
|
|
1599
|
+
else if(badgeClass.includes("user")) badgeText = "User API";
|
|
1600
|
+
else if(badgeClass.includes("portal")) badgeText = "Portal API";
|
|
1601
|
+
badge.insertAdjacentHTML("beforeend","<span class='visually-hidden'>" + badgeText + "</span>");
|
|
1602
|
+
strLabel = badgeText;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
if(strLabel)
|
|
1607
|
+
{
|
|
1608
|
+
let seoSafeId = toSeoUrl(strLabel); //-- will use SEO to set in url so can scroll to element from url
|
|
1609
|
+
|
|
1610
|
+
//-- de-dupe repeated heading text with a suffix counter
|
|
1611
|
+
if(usedHeadingIds[seoSafeId] === undefined)
|
|
1612
|
+
{
|
|
1613
|
+
usedHeadingIds[seoSafeId] = 0;
|
|
1614
|
+
}
|
|
1615
|
+
else
|
|
1616
|
+
{
|
|
1617
|
+
usedHeadingIds[seoSafeId]++;
|
|
1618
|
+
seoSafeId = seoSafeId + "-" + usedHeadingIds[seoSafeId];
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
let linkUrl = currUrl.pathname + "#" + seoSafeId;
|
|
1622
|
+
item.id = seoSafeId;
|
|
1623
|
+
|
|
1624
|
+
view.docApp.tableOfContents.push({ele:item,eleText:strLabel,tagName:item.tagName,href:linkUrl});
|
|
1625
|
+
|
|
1626
|
+
//-- add link icon to header (using seo id). Set the href on the appended anchor
|
|
1627
|
+
//-- only - a find("a") here overwrote the href of any author-written link inside
|
|
1628
|
+
//-- the heading as well. //-- SG
|
|
1629
|
+
//-- built as an element rather than an HTML string. The label is heading text and
|
|
1630
|
+
//-- was interpolated straight into markup, which is why it needed the quote-stripping
|
|
1631
|
+
//-- hack beside it - and it would still have broken on an angle bracket.
|
|
1632
|
+
//-- setAttribute escapes, so the hack goes with it. //-- SG
|
|
1633
|
+
let anchorEl = document.createElement("a");
|
|
1634
|
+
anchorEl.className = "header-anchor";
|
|
1635
|
+
anchorEl.setAttribute("aria-label","Link to this section: " + strLabel);
|
|
1636
|
+
anchorEl.setAttribute("href",linkUrl);
|
|
1637
|
+
anchorEl.innerHTML = "<i class='bi bi-link' aria-hidden='true'></i>";
|
|
1638
|
+
item.appendChild(anchorEl);
|
|
1639
|
+
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
function formatBytes(val)
|
|
1647
|
+
{
|
|
1648
|
+
//-- the old <1024 special case divided anyway and hard-coded 'KB', so a 500-byte file
|
|
1649
|
+
//-- rendered as "0.49KB" and units[0] ('B') was unreachable //-- SG
|
|
1650
|
+
var units = ['B', 'KB', 'MB', 'GB', 'TB'],
|
|
1651
|
+
bytes = val,
|
|
1652
|
+
i;
|
|
1653
|
+
|
|
1654
|
+
for (i = 0; bytes >= 1024 && i < units.length-1; i++) {
|
|
1655
|
+
bytes /= 1024;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return bytes.toFixed(2) + units[i];
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
//-- one app per embedded component in the current article - see below for why
|
|
1662
|
+
let currentVueContentApps = [];
|
|
1663
|
+
function convertDocumentContentToVueSafeComponents(strHtml){
|
|
1664
|
+
|
|
1665
|
+
let vueComponentsSelector = "hdoc-approve"; //-- comma sep
|
|
1666
|
+
|
|
1667
|
+
//-- This used to compile the whole article as one app: blanket-apply v-pre to every element so
|
|
1668
|
+
//-- authored {{...}} in prose stayed literal, then removeAttr("v-pre") from the embedded
|
|
1669
|
+
//-- component tags to let those compile. That last step never actually worked. v-pre is
|
|
1670
|
+
//-- INHERITED by the whole subtree, so clearing it on a descendant does nothing once an
|
|
1671
|
+
//-- ancestor has stopped compilation - any embedded component that was not a direct child of
|
|
1672
|
+
//-- .vue-document-body was silently inert. Verified against Vue 2.7.16 as well as Vue 3: a
|
|
1673
|
+
//-- nested <hdoc-approve> rendered under neither. It was a live bug, not a migration casualty.
|
|
1674
|
+
//--
|
|
1675
|
+
//-- So we stop compiling the article altogether. Author HTML is left exactly as injected -
|
|
1676
|
+
//-- which makes {{...}} in prose literal by construction rather than by directive, and keeps
|
|
1677
|
+
//-- arbitrary author markup out of the template compiler - and we mount one small app per
|
|
1678
|
+
//-- embedded component element instead. Those work at any nesting depth, which also fixes the
|
|
1679
|
+
//-- inert-nested-component bug. //-- SG
|
|
1680
|
+
|
|
1681
|
+
//-- tear down last article's component apps (Vue 3 has no vm.$destroy(); teardown is an
|
|
1682
|
+
//-- app-level operation, so hbVue.unmount looks up the app that owns the vm)
|
|
1683
|
+
currentVueContentApps.forEach(function(vm){ hbVue.unmount(vm); });
|
|
1684
|
+
currentVueContentApps = [];
|
|
1685
|
+
|
|
1686
|
+
let vueContainer = vueContentContainer();
|
|
1687
|
+
if(!vueContainer) return;
|
|
1688
|
+
|
|
1689
|
+
vueContainer.innerHTML = strHtml;
|
|
1690
|
+
|
|
1691
|
+
vueContainer.querySelectorAll(vueComponentsSelector).forEach(function(el){
|
|
1692
|
+
|
|
1693
|
+
let componentName = el.tagName.toLowerCase();
|
|
1694
|
+
|
|
1695
|
+
//-- carry the element's attributes over as props. hdoc-approve declares no props, so they
|
|
1696
|
+
//-- arrive on $attrs, which is where it already reads reference/assignee from.
|
|
1697
|
+
let props = {};
|
|
1698
|
+
for(let x=0;x<el.attributes.length;x++){
|
|
1699
|
+
props[el.attributes[x].name] = el.attributes[x].value;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
//-- mounting on the element itself keeps the DOM position; a render function means Vue
|
|
1703
|
+
//-- ignores the element's innerHTML rather than treating it as a template. resolveComponent
|
|
1704
|
+
//-- has to be called during render so it sees this app's component registry.
|
|
1705
|
+
currentVueContentApps.push(hbMount({
|
|
1706
|
+
render:function(){
|
|
1707
|
+
return Vue.h(Vue.resolveComponent(componentName), props);
|
|
1708
|
+
}
|
|
1709
|
+
}, el));
|
|
1710
|
+
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
//-- processing to do after book content has been injected into view
|
|
1714
|
+
//-- Mermaid client-side rendering config — mirrors hdoc-tools templates/mermaid-theme.yaml.
|
|
1715
|
+
//-- (PDFs are still rendered to SVG server-side by hdoc build; this is HTML-only.)
|
|
1716
|
+
var HB_MERMAID_CONFIG = {
|
|
1717
|
+
startOnLoad: false,
|
|
1718
|
+
theme: "base",
|
|
1719
|
+
themeVariables: {
|
|
1720
|
+
background: "transparent",
|
|
1721
|
+
fontSize: "14px",
|
|
1722
|
+
primaryColor: "#85D2FF",
|
|
1723
|
+
primaryColorLight: "#85D2FF",
|
|
1724
|
+
primaryColorDark: "#0065CB",
|
|
1725
|
+
secondaryColor: "#219BFF",
|
|
1726
|
+
secondaryColorLight: "#85D2FF",
|
|
1727
|
+
secondaryColorDark: "#0C4C8C",
|
|
1728
|
+
tertiaryColor: "#219BFF",
|
|
1729
|
+
tertiaryColorLight: "#85D2FF",
|
|
1730
|
+
tertiaryColorDark: "#0C4C8C",
|
|
1731
|
+
lineColor: "#808080",
|
|
1732
|
+
border1: "#606060",
|
|
1733
|
+
textColor: "#A0A0A0",
|
|
1734
|
+
titleColor: "#A0A0A0",
|
|
1735
|
+
labelColor: "#404040",
|
|
1736
|
+
loopTextColor: "#A0A0A0",
|
|
1737
|
+
nodeTextColor: "#404040",
|
|
1738
|
+
labelTextColor: "#404040",
|
|
1739
|
+
pieSectionTextColor: "#404040",
|
|
1740
|
+
nodeBorder: "#219BFF",
|
|
1741
|
+
nodeBg: "transparent"
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
18
1744
|
|
|
19
1745
|
//-- Lazy-load the Mermaid bundle (~3MB) only when a page actually contains a
|
|
20
1746
|
//-- diagram. Resolves to window.mermaid, initialised once.
|
|
21
|
-
|
|
22
|
-
function hbLoadMermaid()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch (e) {
|
|
40
|
-
reject(e);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
el.onerror = () =>
|
|
44
|
-
reject(new Error("failed to load js/mermaid.min.js"));
|
|
45
|
-
document.head.appendChild(el);
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
return hbMermaidLoadPromise;
|
|
1747
|
+
var hbMermaidLoadPromise = null;
|
|
1748
|
+
function hbLoadMermaid()
|
|
1749
|
+
{
|
|
1750
|
+
if(window.mermaid) return Promise.resolve(window.mermaid);
|
|
1751
|
+
if(hbMermaidLoadPromise) return hbMermaidLoadPromise;
|
|
1752
|
+
hbMermaidLoadPromise = new Promise(function(resolve,reject)
|
|
1753
|
+
{
|
|
1754
|
+
var el = document.createElement("script");
|
|
1755
|
+
el.src = "js/mermaid.min.js"; //-- resolves against <base href> (site root)
|
|
1756
|
+
el.onload = function()
|
|
1757
|
+
{
|
|
1758
|
+
try{ window.mermaid.initialize(HB_MERMAID_CONFIG); resolve(window.mermaid); }
|
|
1759
|
+
catch(e){ reject(e); }
|
|
1760
|
+
};
|
|
1761
|
+
el.onerror = function(){ reject(new Error("failed to load js/mermaid.min.js")); };
|
|
1762
|
+
document.head.appendChild(el);
|
|
1763
|
+
});
|
|
1764
|
+
return hbMermaidLoadPromise;
|
|
49
1765
|
}
|
|
50
1766
|
|
|
1767
|
+
//-- Render any not-yet-processed <pre class="mermaid"> blocks in the current doc.
|
|
1768
|
+
//-- mermaid.run() tags each node with data-processed, so re-running after a
|
|
1769
|
+
//-- navigation only renders freshly-injected diagrams.
|
|
51
1770
|
//-- Inject the spinner styles once. While a <pre class="mermaid"> has no
|
|
52
1771
|
//-- data-processed attr (set by mermaid.run() once rendered) we collapse its raw
|
|
53
1772
|
//-- source text and show a spinner in its place.
|
|
54
|
-
function hbEnsureMermaidStyles()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1773
|
+
function hbEnsureMermaidStyles()
|
|
1774
|
+
{
|
|
1775
|
+
if(document.getElementById("hb-mermaid-style")) return;
|
|
1776
|
+
var style = document.createElement("style");
|
|
1777
|
+
style.id = "hb-mermaid-style";
|
|
1778
|
+
style.textContent =
|
|
1779
|
+
"pre.mermaid:not([data-processed]){font-size:0!important;line-height:0!important;min-height:140px;display:flex;align-items:center;justify-content:center;background:none!important;border:none!important;}" +
|
|
1780
|
+
'pre.mermaid:not([data-processed])::after{content:"";width:32px;height:32px;border:3px solid rgba(33,155,255,.25);border-top-color:#219BFF;border-radius:50%;animation:hbMermaidSpin .8s linear infinite;}' +
|
|
1781
|
+
"@keyframes hbMermaidSpin{to{transform:rotate(360deg);}}";
|
|
1782
|
+
document.head.appendChild(style);
|
|
63
1783
|
}
|
|
64
1784
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for (const n of nodes) n.setAttribute("data-processed", "true");
|
|
79
|
-
};
|
|
80
|
-
hbLoadMermaid()
|
|
81
|
-
.then((mermaid) => mermaid.run({ nodes }))
|
|
82
|
-
.catch(reveal);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
//-- Warm the Mermaid bundle (~4.5MB) immediately on first script run so it's
|
|
86
|
-
//-- downloading/parsing in the background regardless of whether THIS page has a
|
|
87
|
-
//-- diagram. By the time the user reaches a diagram page it's ready and the swap
|
|
88
|
-
//-- is instant. The bundle is served with a long cache header, so it downloads
|
|
89
|
-
//-- once per browser, not once per page.
|
|
90
|
-
hbEnsureMermaidStyles();
|
|
91
|
-
hbLoadMermaid().catch(() => {});
|
|
92
|
-
|
|
93
|
-
const docAppMethods = {
|
|
94
|
-
setSessionInfo: function (info) {
|
|
95
|
-
this.docApp.userSession = info;
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
showInlineDocumentList: function (ev) {
|
|
99
|
-
if (ev) {
|
|
100
|
-
ev.stopImmediatePropagation();
|
|
101
|
-
ev.preventDefault();
|
|
102
|
-
}
|
|
103
|
-
this.docApp.showInlineDocumentsMarkup = true;
|
|
104
|
-
this.$forceUpdate();
|
|
105
|
-
},
|
|
106
|
-
popupInlineDocument: function (ev, navSectionItem) {
|
|
107
|
-
if (ev) {
|
|
108
|
-
ev.stopImmediatePropagation();
|
|
109
|
-
ev.preventDefault();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
$("#inlineModal").modal("show");
|
|
113
|
-
this.docApp.inlinePopupDocumentHref = navSectionItem.link;
|
|
114
|
-
},
|
|
115
|
-
switchViewTheme: () => {
|
|
116
|
-
if (ThemePreference)
|
|
117
|
-
document.documentElement.classList.remove(ThemePreference);
|
|
118
|
-
|
|
119
|
-
if (ThemePreference === "dark") {
|
|
120
|
-
ThemePreference = "light";
|
|
121
|
-
} else {
|
|
122
|
-
ThemePreference = "dark";
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
localStorage.setItem("hdocbook-theme-appearance", ThemePreference);
|
|
126
|
-
document.documentElement.classList.add(ThemePreference);
|
|
127
|
-
},
|
|
128
|
-
renderNavigation: function () {
|
|
129
|
-
this.docApp.navSections = this.docApp.book.navigation.items; //-- show be an array of nav groups
|
|
130
|
-
},
|
|
131
|
-
toggleMobileMenu: () => {
|
|
132
|
-
const isOpen = $(".mobile-menu-btn").data("isopen");
|
|
133
|
-
if (isOpen) {
|
|
134
|
-
$(".DocSidebar").removeClass("open");
|
|
135
|
-
} else {
|
|
136
|
-
$(".DocSidebar").addClass("open");
|
|
137
|
-
}
|
|
138
|
-
//-- store new mode
|
|
139
|
-
$(".mobile-menu-btn").data("isopen", !isOpen);
|
|
140
|
-
},
|
|
141
|
-
resetMobileMenu: (ev) => {
|
|
142
|
-
if (
|
|
143
|
-
ev &&
|
|
144
|
-
($(ev.srcElement).hasClass("mobile-menu-btn")[0] ||
|
|
145
|
-
$(ev.srcElement).closest(".mobile-menu-btn")[0])
|
|
146
|
-
) {
|
|
147
|
-
//-- skp as have clikc no menu btn
|
|
148
|
-
} else {
|
|
149
|
-
$(".mobile-menu-btn").data("isopen", false);
|
|
150
|
-
$(".DocSidebar").removeClass("open");
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
};
|
|
1785
|
+
function hbRenderMermaidDiagrams()
|
|
1786
|
+
{
|
|
1787
|
+
hbEnsureMermaidStyles();
|
|
1788
|
+
var nodes = document.querySelectorAll("#DocContent pre.mermaid:not([data-processed])");
|
|
1789
|
+
if(!nodes.length) return;
|
|
1790
|
+
//-- On any failure, mark nodes processed so the spinner stops (reveals source
|
|
1791
|
+
//-- rather than spinning forever).
|
|
1792
|
+
var reveal = function(err){
|
|
1793
|
+
hslDocLog.log("Mermaid render failed",err);
|
|
1794
|
+
for(var i=0;i<nodes.length;i++) nodes[i].setAttribute("data-processed","true");
|
|
1795
|
+
};
|
|
1796
|
+
hbLoadMermaid().then(function(mermaid){ return mermaid.run({ nodes: nodes }); }).catch(reveal);
|
|
1797
|
+
}
|
|
154
1798
|
|
|
1799
|
+
//-- Warm the Mermaid bundle (~3.5MB) in the background so that by the time the
|
|
1800
|
+
//-- user reaches a diagram page it's ready and the swap is instant. The bundle
|
|
1801
|
+
//-- should be served with a long cache header, so it downloads once per browser,
|
|
1802
|
+
//-- not once per page.
|
|
155
1803
|
//--
|
|
156
|
-
//--
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1804
|
+
//-- Deferred to idle time: this used to fire at script-eval time, which put a
|
|
1805
|
+
//-- 3.5MB fetch + parse in direct competition with first render on every page,
|
|
1806
|
+
//-- the vast majority of which contain no diagram at all. requestIdleCallback
|
|
1807
|
+
//-- yields until the browser has nothing better to do; the timeout option caps
|
|
1808
|
+
//-- how long we'll wait on a permanently busy page. Safari <17 has no
|
|
1809
|
+
//-- requestIdleCallback, hence the setTimeout fallback. //-- SG
|
|
1810
|
+
//--
|
|
1811
|
+
//-- A diagram page does NOT depend on this: hbRenderMermaidDiagrams() calls
|
|
1812
|
+
//-- hbLoadMermaid() itself, and hbLoadMermaid() de-dupes on hbMermaidLoadPromise,
|
|
1813
|
+
//-- so an early navigation just promotes the same in-flight load.
|
|
1814
|
+
hbEnsureMermaidStyles();
|
|
1815
|
+
(function()
|
|
1816
|
+
{
|
|
1817
|
+
var warm = function(){ hbLoadMermaid().catch(function(){}); };
|
|
1818
|
+
if(window.requestIdleCallback) requestIdleCallback(warm,{timeout:5000});
|
|
1819
|
+
else setTimeout(warm,3000);
|
|
1820
|
+
})();
|
|
1821
|
+
|
|
1822
|
+
function postProcessBookContentRender(linkRef,frontmatterData,direct_BooksLinkRef)
|
|
1823
|
+
{
|
|
1824
|
+
//-- do we have a pdf file dl to put into header
|
|
1825
|
+
addPdfIconToHeader(direct_BooksLinkRef);
|
|
1826
|
+
|
|
1827
|
+
//-- get document body mark up - make a clone and add pre-v to all the elements apart from known vue components
|
|
1828
|
+
//-- test vue comps in doc
|
|
1829
|
+
//jqContentContainer().prepend(`<hdoc-approve reference="1234" assignee="gerrysw"></hdoc-approve><hdoc-approve reference="1234" assignee="neilwj"></hdoc-approve>`)
|
|
1830
|
+
|
|
1831
|
+
let bodyEl = contentContainer();
|
|
1832
|
+
convertDocumentContentToVueSafeComponents(bodyEl ? bodyEl.innerHTML : "");
|
|
1833
|
+
if(bodyEl) bodyEl.innerHTML = ""; //-- empty old body
|
|
1834
|
+
|
|
1835
|
+
revealApp();
|
|
1836
|
+
|
|
1837
|
+
//-- Layout class on the content container, from frontmatter - the CSS uses it to size
|
|
1838
|
+
//-- table/list views. One reactive value replaces an add/remove pair against a
|
|
1839
|
+
//-- Vue-rendered element plus global.lastLayoutClass, which was a hand-maintained second
|
|
1840
|
+
//-- copy of the same state purely so the previous class could be taken off again. Vue
|
|
1841
|
+
//-- swaps it for us, so there is nothing to remember. //-- SG
|
|
1842
|
+
view.docApp.layoutClass = frontmatterData.layout || "";
|
|
1843
|
+
|
|
1844
|
+
//--generate toc for all layouts that have h2/h3
|
|
1845
|
+
view.docApp.keepTocLayout = (frontmatterData.layout == "article-no-toc");
|
|
1846
|
+
if(!frontmatterData.layout || frontmatterData.layout==="article" || frontmatterData.layout==="article-toc")
|
|
1847
|
+
{
|
|
1848
|
+
view.docApp.keepTocLayout = true;
|
|
1849
|
+
generateTableOfContentsFromDoc();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
//-- set actual html document title - based on the first h1 found in content
|
|
1854
|
+
let docContentEl = document.getElementById("DocContent");
|
|
1855
|
+
let firstH1 = docContentEl ? docContentEl.querySelector("h1") : null;
|
|
1856
|
+
let docTitle = firstH1 ? firstH1.textContent.trim() : "";
|
|
1857
|
+
let fullTitle = view.docApp.title;
|
|
1858
|
+
if(docTitle){
|
|
1859
|
+
fullTitle += " - " + docTitle;
|
|
1860
|
+
}
|
|
1861
|
+
else{
|
|
1862
|
+
fullTitle += (view.docApp.frontmatterData && view.docApp.frontmatterData.title)? " - " + view.docApp.frontmatterData.title:"";
|
|
1863
|
+
}
|
|
1864
|
+
document.title = fullTitle;
|
|
1865
|
+
|
|
1866
|
+
//-- Set the canonical link - the url we want search engines to treat as authoritative.
|
|
1867
|
+
//-- Reuse the existing tag rather than appending a new one. This used to append on every
|
|
1868
|
+
//-- render, so a session that visited N articles left N <link rel="canonical"> tags in
|
|
1869
|
+
//-- <head>, all but the first stale. Crawlers take the first one, so every article
|
|
1870
|
+
//-- navigated to in-app advertised the canonical of whichever page was loaded first.
|
|
1871
|
+
//--
|
|
1872
|
+
//-- Also strip the query string and hash: ?theme=dark and #section are view state, not
|
|
1873
|
+
//-- distinct documents, and leaving them in invites duplicate-content splitting. //-- SG
|
|
1874
|
+
const canonicalUrl = document.location.origin + document.location.pathname;
|
|
1875
|
+
let linkTag = document.head.querySelector('link[rel="canonical"]');
|
|
1876
|
+
if(!linkTag)
|
|
1877
|
+
{
|
|
1878
|
+
linkTag = document.createElement('link');
|
|
1879
|
+
linkTag.setAttribute('rel', 'canonical');
|
|
1880
|
+
document.head.appendChild(linkTag);
|
|
1881
|
+
}
|
|
1882
|
+
linkTag.href = canonicalUrl;
|
|
1883
|
+
hslDocLog.log("Set canonical link to",canonicalUrl);
|
|
1884
|
+
|
|
1885
|
+
//-- find any <tabs> and andd a bootstrap tab item strip
|
|
1886
|
+
(document.getElementById("DocContent") || document).querySelectorAll("tabs").forEach((el) => {
|
|
1887
|
+
|
|
1888
|
+
let tabClass = el.getAttribute("tabstyle");
|
|
1889
|
+
let tabMarkup = "<ul class='nav nav-tabs'>";
|
|
1890
|
+
//-- for each tab we need to create a tab link
|
|
1891
|
+
el.querySelectorAll("tab").forEach((aTab,idx) => {
|
|
1892
|
+
|
|
1893
|
+
let activeClass=(idx===0)?" active":"";
|
|
1894
|
+
tabMarkup += `<li class='nav-item c-pointer' contentidx='${idx}'><span class='nav-link${activeClass}'>${aTab.getAttribute("name")}</span></li>`;
|
|
1895
|
+
|
|
1896
|
+
//-- hide the tab element if it is not the active one (first)
|
|
1897
|
+
if(idx>0)aTab.classList.add("d-none");
|
|
1898
|
+
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
tabMarkup += "</ul>";
|
|
1902
|
+
//-- inject markup
|
|
1903
|
+
el.insertAdjacentHTML("afterbegin",tabMarkup);
|
|
1904
|
+
let tabStrip = el.querySelector(".nav.nav-tabs");
|
|
1905
|
+
if(!tabStrip) return;
|
|
1906
|
+
if(tabClass) tabStrip.classList.add(tabClass);
|
|
1907
|
+
|
|
1908
|
+
//-- Handlers bound on the strip we just built, NOT on every <li> descendant of <tabs>.
|
|
1909
|
+
//-- The jQuery version used find("li"), which also matched list items inside the tab
|
|
1910
|
+
//-- CONTENT: clicking one of those ran this handler with contentidx null, which cleared
|
|
1911
|
+
//-- .active from every tab and highlighted none. //-- SG
|
|
1912
|
+
tabStrip.querySelectorAll("li").forEach((li) => {
|
|
1913
|
+
li.addEventListener("click",function()
|
|
1914
|
+
{
|
|
1915
|
+
let contentIdx = this.getAttribute("contentidx");
|
|
1916
|
+
let tabsContainer = this.closest("tabs");
|
|
1917
|
+
if(!tabsContainer) return;
|
|
1918
|
+
let tabs = tabsContainer.querySelectorAll("tab");
|
|
1919
|
+
|
|
1920
|
+
//-- hide other tab content and show this tabs content
|
|
1921
|
+
tabs.forEach(function(t){ t.classList.add("d-none"); });
|
|
1922
|
+
if(tabs[contentIdx-0]) tabs[contentIdx-0].classList.remove("d-none");
|
|
1923
|
+
|
|
1924
|
+
//-- set clicked tab item to active
|
|
1925
|
+
tabsContainer.querySelectorAll(".nav-link").forEach(function(nl){ nl.classList.remove("active"); });
|
|
1926
|
+
this.querySelectorAll(".nav-link").forEach(function(nl){ nl.classList.add("active"); });
|
|
1927
|
+
});
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
//-- do any code highlighting
|
|
1933
|
+
if(hljs)
|
|
1934
|
+
{
|
|
1935
|
+
//-- scoped to the document view rather than the whole page. Same set of blocks in
|
|
1936
|
+
//-- practice (both .document-body and .vue-document-body live under #DocContent), but
|
|
1937
|
+
//-- it stops the scan walking the shell, the sidebar and the search UI on every
|
|
1938
|
+
//-- render, and it means a code block added to the chrome later cannot be
|
|
1939
|
+
//-- accidentally highlighted. //-- SG
|
|
1940
|
+
let highlightRoot = document.getElementById("DocContent") || document;
|
|
1941
|
+
highlightRoot.querySelectorAll('pre code').forEach((el) => {
|
|
1942
|
+
|
|
1943
|
+
//-- Don't hand Mermaid blocks to highlight.js. There is no mermaid grammar, so it
|
|
1944
|
+
//-- logs "Could not find the language 'mermaid'" and no-highlights anyway; worse,
|
|
1945
|
+
//-- the build-time variant of these blocks holds a pre-rendered <img> rather than
|
|
1946
|
+
//-- text, and hljs 11 warns "code block includes unescaped HTML ... potentially
|
|
1947
|
+
//-- serious security risk" for any element it is asked to highlight that contains
|
|
1948
|
+
//-- child elements. Both were silent under hljs 9 but are pure console noise, and
|
|
1949
|
+
//-- highlighting an image was never meaningful. //-- SG
|
|
1950
|
+
if(el.classList.contains("language-mermaid") || el.querySelector("img")) return;
|
|
1951
|
+
|
|
1952
|
+
//-- Skip anything already highlighted. Content reaches this loop more than once per
|
|
1953
|
+
//-- navigation (it is highlighted in .document-body, then the HTML is copied into
|
|
1954
|
+
//-- .vue-document-body), and re-highlighting an element whose children are hljs's
|
|
1955
|
+
//-- own <span class="hljs-*"> output is what triggers the "unescaped HTML"
|
|
1956
|
+
//-- security warning in hljs 11. Test the class, not data-highlighted: the copied
|
|
1957
|
+
//-- markup keeps the "hljs" class but not always the attribute. //-- SG
|
|
1958
|
+
if(el.classList.contains("hljs") || el.getAttribute("data-highlighted") === "yes") return;
|
|
1959
|
+
|
|
1960
|
+
//-- highlightElement, not highlightBlock: deprecated in highlight.js 10 and
|
|
1961
|
+
//-- REMOVED in 11, so the old name is now undefined. //-- SG
|
|
1962
|
+
hljs.highlightElement(el);
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
else
|
|
1966
|
+
{
|
|
1967
|
+
hslDocLog.log("code highlighting js could not be loaded. code block highlighting will not be enabled");
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
//-- render any Mermaid diagrams client-side (lazy-loads the bundle)
|
|
1971
|
+
hbRenderMermaidDiagrams();
|
|
1972
|
+
|
|
1973
|
+
//-- show copy button by any code blocks
|
|
1974
|
+
var options = {
|
|
1975
|
+
contentSelector: ".injected-document-content",
|
|
1976
|
+
// Delay in ms used for `setTimeout` before badging is applied
|
|
1977
|
+
// Use if you need to time highlighting and badge application
|
|
1978
|
+
// since the badges need to be applied afterwards.
|
|
1979
|
+
// 0 - direct execution (ie. you handle timing
|
|
1980
|
+
loadDelay:0,
|
|
1981
|
+
|
|
1982
|
+
// CSS class(es) used to render the copy icon.
|
|
1983
|
+
copyIconClass: "bi bi-clipboard",
|
|
1984
|
+
// CSS class(es) used to render the done icon.
|
|
1985
|
+
checkIconClass: "bi bi-clipboard-check --htl-c-brand",
|
|
1986
|
+
|
|
1987
|
+
// intercept text copying - passed in text return text out
|
|
1988
|
+
onBeforeCodeCopied: function(text) {
|
|
1989
|
+
return text;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
highlightJsBadge(options);
|
|
1994
|
+
|
|
1995
|
+
|
|
1996
|
+
//-- THIS IS A SPECIAL CALL TO ANY TOP WHERE UI MIGHT EMBED DOC IN IFRAME - THIS ALLOWS THAT SRC TO RESIZE IFRAME
|
|
1997
|
+
if(parent && parent.postMessage)
|
|
1998
|
+
{
|
|
1999
|
+
let dataMessage = {};
|
|
2000
|
+
dataMessage.type = "HornbillHelpDocLoaded";
|
|
2001
|
+
dataMessage.info = {height: document.getElementById("DocContent").scrollHeight,
|
|
2002
|
+
width: document.getElementById("DocContent").scrollWidth,
|
|
2003
|
+
frontmatter:frontmatterData};
|
|
2004
|
+
|
|
2005
|
+
parent.postMessage(dataMessage,"*");
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
//-- highlight the nav link for this page and honour any #hash in the url. The same four
|
|
2009
|
+
//-- statements used to be duplicated across both arms below; only the $nextTick wrapper
|
|
2010
|
+
//-- differed. No updateCounter++ / $forceUpdate() in either path: the `expand = true`
|
|
2011
|
+
//-- writes in expandNavParentSectionBySelectedLinkHref go through the reactive proxy and
|
|
2012
|
+
//-- schedule the patch on their own, and nav "active" highlighting is applied with jQuery
|
|
2013
|
+
//-- classes in this app, not a Vue binding, so it never depended on a forced repaint. //-- SG
|
|
2014
|
+
let finishRender = function()
|
|
2015
|
+
{
|
|
2016
|
+
//-- find any navigation links that match url and highlight
|
|
2017
|
+
applyContentAccessibilityFixes();
|
|
2018
|
+
highlightNavigationLinkFromUrl(linkRef);
|
|
2019
|
+
|
|
2020
|
+
//-- scroll to element that matches hash (if have one)
|
|
2021
|
+
//-- this is scrolling the content view
|
|
2022
|
+
if(document.location.hash)
|
|
2023
|
+
{
|
|
2024
|
+
let gotoEle = resolveAnchorElement(document.location.hash);
|
|
2025
|
+
if(gotoEle) GotoAndHighlightAnchorContent(gotoEle,false); //-- hash present = user targeted a section, so always run the highlight animation
|
|
2026
|
+
}
|
|
2027
|
+
else if(!bInitialPageLoad)
|
|
2028
|
+
{
|
|
2029
|
+
//-- no target anchor: present the new page from the top. The article is swapped
|
|
2030
|
+
//-- inside the SAME scroll container, so the previous page's scroll offset
|
|
2031
|
+
//-- survived the swap and in-book navigation showed new pages part-scrolled-down.
|
|
2032
|
+
//-- Skipped on initial load/refresh - the browser owns scroll restoration there. //-- SG
|
|
2033
|
+
let scrollableParent = getScrollParent(document.getElementById("DocContent"));
|
|
2034
|
+
if(scrollableParent) scrollableParent.scrollTop = 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
bInitialPageLoad = false;
|
|
2038
|
+
};
|
|
2039
|
+
|
|
2040
|
+
//-- make sure selected link parents are set to .expand=true, then ALWAYS wait for Vue to
|
|
2041
|
+
//-- flush before finishRender runs.
|
|
2042
|
+
//--
|
|
2043
|
+
//-- This used to wait only when a nav group had expanded, and go straight through
|
|
2044
|
+
//-- otherwise. That was safe while everything finishRender depends on was applied by a
|
|
2045
|
+
//-- synchronous DOM write. It is not any more: the content reveal (contentLoading) and
|
|
2046
|
+
//-- the layout class above are both reactive now, and both land on the next tick -
|
|
2047
|
+
//-- while finishRender scrolls to an anchor and measures the section it highlights.
|
|
2048
|
+
//-- Measuring before the layout class applies means measuring against the wrong layout.
|
|
2049
|
+
//-- Waiting unconditionally costs one tick and removes the whole class of problem.
|
|
2050
|
+
//-- //-- SG
|
|
2051
|
+
expandNavParentSectionBySelectedLinkHref(linkRef);
|
|
2052
|
+
view.$nextTick(finishRender);
|
|
161
2053
|
|
|
162
|
-
//--- GLOBAL FUNCTIONS
|
|
163
|
-
function removeTrailingSlash(str) {
|
|
164
|
-
return str.endsWith("/") ? str.slice(0, -1) : str;
|
|
165
2054
|
}
|
|
166
|
-
function removeStartingSlash(str) {
|
|
167
|
-
return str.startsWith("/") ? str.slice(1, str.length) : str;
|
|
168
2055
|
|
|
169
|
-
|
|
2056
|
+
//-- return t/f is the element is actually visible on the screen
|
|
2057
|
+
function elementIsOnVisibleScreen(oEle)
|
|
2058
|
+
{
|
|
2059
|
+
|
|
2060
|
+
const item = oEle.getBoundingClientRect();
|
|
2061
|
+
return (
|
|
2062
|
+
item.top >= 0 &&
|
|
2063
|
+
item.left >= 0 &&
|
|
2064
|
+
item.bottom <= (
|
|
2065
|
+
window.innerHeight ||
|
|
2066
|
+
document.documentElement.clientHeight) &&
|
|
2067
|
+
item.right <= (
|
|
2068
|
+
window.innerWidth ||
|
|
2069
|
+
document.documentElement.clientWidth)
|
|
2070
|
+
);
|
|
170
2071
|
}
|
|
171
2072
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
view.resetMobileMenu();
|
|
198
|
-
|
|
199
|
-
//-- clicking on a hash tag header link element - so jsut scroll to element
|
|
200
|
-
if (`${checkUrl.origin}/` === siteBaseLocation) {
|
|
201
|
-
//-- if a url that is part of this doc site then handle loading content inline else let link do whatever (i.e. its an external link)
|
|
202
|
-
ev.stopImmediatePropagation();
|
|
203
|
-
ev.preventDefault();
|
|
204
|
-
|
|
205
|
-
//-- do we need to scroll into view element on this page
|
|
206
|
-
if (
|
|
207
|
-
checkUrl.hash &&
|
|
208
|
-
getAnchorFromHash(checkUrl.hash, "faq-toc-item")
|
|
209
|
-
) {
|
|
210
|
-
const eleAnc = getAnchorFromHash(checkUrl.hash, "faq-toc-item");
|
|
211
|
-
if (eleAnc[0]?.scrollIntoView) eleAnc[0].scrollIntoView();
|
|
212
|
-
|
|
213
|
-
if (checkUrl.pathname === "/") {
|
|
214
|
-
//-- going to element on current page so just update current browser url with new #
|
|
215
|
-
const currUrl = new URL(document.location.href);
|
|
216
|
-
currUrl.hash = checkUrl.hash;
|
|
217
|
-
if (currUrl.href !== document.location.href)
|
|
218
|
-
window.history.pushState(null, null, currUrl); //-- user has clicked a link so we want to add state to history so we can click back
|
|
219
|
-
} else if (ele.href !== document.location.href) {
|
|
220
|
-
window.history.pushState(null, null, this.href); //-- user has clicked a link so we want to add state to history so we can click back
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
loadContentUrl(this.getAttribute("href"), false, false);
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
//-- if not part of this website then always open in new tab as considered an external site
|
|
228
|
-
this.setAttribute("target", "_blank");
|
|
229
|
-
}
|
|
230
|
-
});
|
|
2073
|
+
function getScrollParent(node) {
|
|
2074
|
+
if (!node) {
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
if (node.scrollHeight > node.clientHeight) {
|
|
2079
|
+
return node;
|
|
2080
|
+
} else {
|
|
2081
|
+
return getScrollParent(node.parentNode);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
//-- pending teardown for the section-highlight overlay (item 23) //-- SG
|
|
2086
|
+
let sectionHighlightTimer = null;
|
|
2087
|
+
|
|
2088
|
+
//-- Remove the section-highlight overlay, if one is up. Safe to call at any time. //-- SG
|
|
2089
|
+
function removeSectionHighlightOverlay()
|
|
2090
|
+
{
|
|
2091
|
+
if(sectionHighlightTimer)
|
|
2092
|
+
{
|
|
2093
|
+
clearTimeout(sectionHighlightTimer);
|
|
2094
|
+
sectionHighlightTimer = null;
|
|
2095
|
+
}
|
|
2096
|
+
document.querySelectorAll(".hb-section-highlight").forEach(function(el){ el.remove(); });
|
|
231
2097
|
}
|
|
232
2098
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
2099
|
+
function GotoAndHighlightAnchorContent(currentActiveAnchor,boolScrollToOnly)
|
|
2100
|
+
{
|
|
2101
|
+
//-- remove any current highlighting
|
|
2102
|
+
removeSectionHighlightOverlay();
|
|
2103
|
+
|
|
2104
|
+
if(currentActiveAnchor && currentActiveAnchor.scrollIntoView){
|
|
2105
|
+
|
|
2106
|
+
//-- adjust scroll point for anchor so is not cut off at the top.
|
|
2107
|
+
//-- offsetTop was used here and is measured from the nearest POSITIONED ancestor, not
|
|
2108
|
+
//-- from the scroll container, so any heading inside a positioned wrapper (a video
|
|
2109
|
+
//-- embed, a layout div, a component) scrolled to the wrong place - usually to the top
|
|
2110
|
+
//-- of the document. Measure the real delta between the two boxes instead. //-- SG
|
|
2111
|
+
let scrollableParent = getScrollParent(document.getElementById("DocContent"));
|
|
2112
|
+
if(scrollableParent)
|
|
2113
|
+
{
|
|
2114
|
+
//-- a root scroller (documentElement/body) has no useful client rect of its own -
|
|
2115
|
+
//-- its content box origin IS the viewport origin, so the offset to compare against is 0
|
|
2116
|
+
let boolRootScroller = (scrollableParent === document.documentElement || scrollableParent === document.body);
|
|
2117
|
+
let containerTop = boolRootScroller ? 0 : (scrollableParent.getBoundingClientRect().top + scrollableParent.clientTop);
|
|
2118
|
+
let deltaToTop = currentActiveAnchor.getBoundingClientRect().top - containerTop;
|
|
2119
|
+
scrollableParent.scrollTop += deltaToTop - 30;
|
|
2120
|
+
}
|
|
2121
|
+
else
|
|
2122
|
+
{
|
|
2123
|
+
currentActiveAnchor.scrollIntoView({block:"center"});
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
if(boolScrollToOnly) return; //-- only want to scroll to
|
|
2127
|
+
|
|
2128
|
+
//-- highlight the section for any heading (H1-H6): the heading plus everything up to the
|
|
2129
|
+
//-- next heading of the same-or-higher level.
|
|
2130
|
+
//--
|
|
2131
|
+
//-- This used to WRAP those elements in a .highlight-container div and animate that. The
|
|
2132
|
+
//-- wrapper was never unwrapped (the timeout that did it was commented out), so every
|
|
2133
|
+
//-- hash click wrapped again - containers accumulated and nested, the sibling walk for
|
|
2134
|
+
//-- later clicks swallowed earlier containers whole, the section crept 2px left per
|
|
2135
|
+
//-- trigger, and docApp.tableOfContents[].ele ended up pointing at relocated nodes.
|
|
2136
|
+
//--
|
|
2137
|
+
//-- Now nothing in the article is moved or re-parented at all: we measure the section's
|
|
2138
|
+
//-- bounding box and draw one absolutely-positioned overlay over it, inside
|
|
2139
|
+
//-- .injected-document-container (already position:relative, in both the main and inline
|
|
2140
|
+
//-- shells). The overlay is pointer-events:none and removes itself. //-- SG
|
|
2141
|
+
let headingMatch = /^H([1-6])$/.exec(currentActiveAnchor.tagName || "");
|
|
2142
|
+
if(headingMatch)
|
|
2143
|
+
{
|
|
2144
|
+
let currentLevel = parseInt(headingMatch[1], 10);
|
|
2145
|
+
//-- loop next siblings - stop at the next heading of same-or-higher level (or end of collection)
|
|
2146
|
+
let arrSectionElements = [currentActiveAnchor];
|
|
2147
|
+
let nextSibling = currentActiveAnchor.nextElementSibling;
|
|
2148
|
+
while(nextSibling)
|
|
2149
|
+
{
|
|
2150
|
+
//-- exit - a heading of same-or-higher level starts a new section, so stop here
|
|
2151
|
+
let siblingMatch = /^H([1-6])$/.exec(nextSibling.tagName || "");
|
|
2152
|
+
if(siblingMatch && parseInt(siblingMatch[1], 10) <= currentLevel) break;
|
|
2153
|
+
arrSectionElements.push(nextSibling);
|
|
2154
|
+
nextSibling = nextSibling.nextElementSibling;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if(arrSectionElements.length > 1) showSectionHighlightOverlay(arrSectionElements,currentActiveAnchor.tagName);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
243
2160
|
}
|
|
244
2161
|
|
|
245
|
-
//--
|
|
246
|
-
function
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
2162
|
+
//-- Draw the highlight box over a section without touching the article DOM. //-- SG
|
|
2163
|
+
function showSectionHighlightOverlay(arrSectionElements,strHeadingTag)
|
|
2164
|
+
{
|
|
2165
|
+
let overlayHost = document.querySelector(".injected-document-container");
|
|
2166
|
+
if(!overlayHost) return;
|
|
2167
|
+
|
|
2168
|
+
//-- union of the section's boxes, expressed in the host's coordinate space
|
|
2169
|
+
let hostRect = overlayHost.getBoundingClientRect();
|
|
2170
|
+
let top = null, bottom = null;
|
|
2171
|
+
arrSectionElements.forEach(function(ele)
|
|
2172
|
+
{
|
|
2173
|
+
let rect = ele.getBoundingClientRect();
|
|
2174
|
+
if(!rect.height && !rect.width) return; //-- skip anything not laid out
|
|
2175
|
+
if(top === null || rect.top < top) top = rect.top;
|
|
2176
|
+
if(bottom === null || rect.bottom > bottom) bottom = rect.bottom;
|
|
2177
|
+
});
|
|
2178
|
+
if(top === null) return;
|
|
2179
|
+
|
|
2180
|
+
let overlay = document.createElement("div");
|
|
2181
|
+
overlay.className = "hb-section-highlight " + strHeadingTag;
|
|
2182
|
+
overlay.style.top = (top - hostRect.top + overlayHost.scrollTop - 4) + "px";
|
|
2183
|
+
overlay.style.height = (bottom - top + 8) + "px";
|
|
2184
|
+
overlayHost.appendChild(overlay);
|
|
2185
|
+
|
|
2186
|
+
//-- tear down after the animation. animationend is the accurate signal; the timeout is the
|
|
2187
|
+
//-- fallback for a hidden tab or a browser that skips the animation entirely.
|
|
2188
|
+
let cleanup = function(){ removeSectionHighlightOverlay(); };
|
|
2189
|
+
overlay.addEventListener("animationend",cleanup,{once:true});
|
|
2190
|
+
sectionHighlightTimer = setTimeout(cleanup,5000);
|
|
277
2191
|
}
|
|
278
2192
|
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
2193
|
+
async function loadContentUrl(linkRef,fromPageRefresh,fromPopState)
|
|
2194
|
+
{
|
|
2195
|
+
//-- empty vue enabled doc content
|
|
2196
|
+
let vueBody = vueContentContainer();
|
|
2197
|
+
if(vueBody) vueBody.innerHTML = "";
|
|
2198
|
+
|
|
2199
|
+
//-- the section-highlight overlay is a sibling of the content, so emptying the content does
|
|
2200
|
+
//-- not take it with it - drop it here or it hangs over the next document. //-- SG
|
|
2201
|
+
removeSectionHighlightOverlay();
|
|
2202
|
+
|
|
2203
|
+
|
|
2204
|
+
//-- reset search mode
|
|
2205
|
+
view.docApp.searchMode = false;
|
|
2206
|
+
view.docApp.mcpCatalogMode = false;
|
|
2207
|
+
view.docApp.searchResponse = null;
|
|
2208
|
+
|
|
2209
|
+
//-- clear table of contents
|
|
2210
|
+
view.docApp.tableOfContents = [];
|
|
2211
|
+
view.docApp.tocMobileOpen = false;
|
|
2212
|
+
//-- hide footer while loading
|
|
2213
|
+
view.docApp.contentLoading = true;
|
|
2214
|
+
|
|
2215
|
+
if(linkRef.indexOf(window.location.origin +"/")===0)
|
|
2216
|
+
{
|
|
2217
|
+
linkRef = linkRef.replace(window.location.origin +"/","");
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
//-- remove trailing / if has one
|
|
2221
|
+
linkRef = removeTrailingSlash(linkRef);
|
|
2222
|
+
//-- remove starting / if link starts with it
|
|
2223
|
+
linkRef = removeStartingSlash(linkRef);
|
|
2224
|
+
|
|
2225
|
+
let frontmatterData = {};
|
|
2226
|
+
let urlInfo = linkRef.split("#"); //-- remove data hash
|
|
2227
|
+
let hash = (urlInfo[1])?"#"+urlInfo[1] : "";
|
|
2228
|
+
|
|
2229
|
+
linkRef = urlInfo[0];
|
|
2230
|
+
let booksLinkRef = linkRef.includes('_books/') ? linkRef : "_books/"+linkRef;
|
|
2231
|
+
|
|
2232
|
+
//-- coming for initial browser load or refresh - so make sure have content load (which the http server should have already injected)
|
|
2233
|
+
let initialBody = contentContainer();
|
|
2234
|
+
if(bInitialPageLoad && initialBody && initialBody.textContent)
|
|
2235
|
+
{
|
|
2236
|
+
//-- content is already loaded - but we need to get the front matter data (for layout etc).
|
|
2237
|
+
//-- NOTE: assign to the OUTER frontmatterData - a shadowing `let` here used to mean the
|
|
2238
|
+
//-- meta-absent case passed "" (getMeta's miss value) into postProcessBookContentRender
|
|
2239
|
+
//-- instead of the normalised {} declared above. //-- SG
|
|
2240
|
+
let metaFrontmatter = getMeta("hdoc:frontmatter");
|
|
2241
|
+
if(metaFrontmatter)
|
|
2242
|
+
{
|
|
2243
|
+
frontmatterData = JSON.parse(atob(metaFrontmatter));
|
|
2244
|
+
let baseUrl = removeTrailingSlash(window.location.origin);
|
|
2245
|
+
let bookId = window.location.href.split(baseUrl+"/")[1].split("/")[0];
|
|
2246
|
+
|
|
2247
|
+
frontmatterData.formattedCommitDate = new Date(frontmatterData["last-commit"]).toLocaleDateString();
|
|
2248
|
+
frontmatterData.readTime = frontmatterData["reading-time"] + " minute to read";
|
|
2249
|
+
frontmatterData.contributorCount = frontmatterData["contributor-count"]-0;
|
|
2250
|
+
|
|
2251
|
+
view.docApp.book = frontmatterData;
|
|
2252
|
+
view.docApp.book.docId = bookId;
|
|
2253
|
+
view.docApp.inlineTitle =frontmatterData.title;
|
|
2254
|
+
|
|
2255
|
+
// fetch hdocbook.json to get book level fields (e.g. version) that are missing from frontmatter header
|
|
2256
|
+
fetchJsonFile(`_books/${bookId}/hdocbook.json`).then((hdocbook) => {
|
|
2257
|
+
if (hdocbook) {
|
|
2258
|
+
view.docApp.book = Object.assign({}, hdocbook, view.docApp.book);
|
|
2259
|
+
//-- the title band binds docApp.title/description, which only loadLibraryBook
|
|
2260
|
+
//-- used to set - so on a direct page load / refresh it rendered empty //-- SG
|
|
2261
|
+
view.docApp.title = hdocbook.title || "";
|
|
2262
|
+
view.docApp.description = hdocbook.description || "";
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
let buildData = getMeta("hdoc:build");
|
|
2268
|
+
if(buildData)
|
|
2269
|
+
{
|
|
2270
|
+
view.docApp.build = buildData;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
let nodeData = getMeta("hdoc:node");
|
|
2274
|
+
if(nodeData)
|
|
2275
|
+
{
|
|
2276
|
+
view.docApp.node = nodeData.split(".")[0];
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
//-- load the book info json
|
|
2280
|
+
view.docApp.bookInfo = null;
|
|
2281
|
+
let jsonFileUrl = booksLinkRef+ ".info";
|
|
2282
|
+
view.docApp.bookInfo = await fetchJsonFile(jsonFileUrl);
|
|
2283
|
+
if(!view.docApp.bookInfo)view.docApp.bookInfo = null;
|
|
2284
|
+
|
|
2285
|
+
view.docApp.frontmatterData = frontmatterData;
|
|
2286
|
+
postProcessBookContentRender(linkRef+hash,frontmatterData,booksLinkRef);
|
|
2287
|
+
}
|
|
2288
|
+
else
|
|
2289
|
+
{
|
|
2290
|
+
//-- we are doing a content load from A tag link or back btn - so ftch content and inject mark up
|
|
2291
|
+
//-- update url in browser
|
|
2292
|
+
setBrowserViewUrl(linkRef+hash,{},fromPageRefresh);
|
|
2293
|
+
|
|
2294
|
+
//-- destroy any existing content children and events properly
|
|
2295
|
+
let clearBody = contentContainer();
|
|
2296
|
+
if(clearBody) clearBody.innerHTML = "";
|
|
2297
|
+
|
|
2298
|
+
//-- Fire the .info JSON and the content HTML together instead of awaiting .info
|
|
2299
|
+
//-- first. They are independent requests, so serialising them cost a full extra
|
|
2300
|
+
//-- round-trip on every in-book navigation. postProcessBookContentRender still
|
|
2301
|
+
//-- needs bookInfo (addPdfIconToHeader reads it), so we join both before
|
|
2302
|
+
//-- injecting and the ordering it depends on is unchanged. //-- SG
|
|
2303
|
+
view.docApp.bookInfo = null;
|
|
2304
|
+
|
|
2305
|
+
//-- for now just load whatever the href prop is (we can add actions to perform specific processing and then load content based on element atts etc)
|
|
2306
|
+
view.docApp.build = 0;
|
|
2307
|
+
view.docApp.node = "N/A";
|
|
2308
|
+
|
|
2309
|
+
let infoPromise = fetchJsonFile(booksLinkRef + ".info");
|
|
2310
|
+
let contentPromise = fetch(booksLinkRef).then(response =>
|
|
2311
|
+
{
|
|
2312
|
+
//-- the server 301s renamed pages (book-declared redirects) on the _books
|
|
2313
|
+
//-- content endpoint and fetch follows it transparently, so the content is
|
|
2314
|
+
//-- already right - but linkRef/booksLinkRef and the history entry we pushed
|
|
2315
|
+
//-- above still point at the old page. Rewrite them from the final response
|
|
2316
|
+
//-- url so nav highlight, pdf link and the address bar show the new page.
|
|
2317
|
+
//-- replaceState (not push) - the old-url entry is dead, back should skip it. //-- SG
|
|
2318
|
+
if(response.redirected && response.url)
|
|
2319
|
+
{
|
|
2320
|
+
let redirectedPath = removeStartingSlash(new URL(response.url).pathname);
|
|
2321
|
+
if(redirectedPath.indexOf("_books/") === 0)
|
|
2322
|
+
{
|
|
2323
|
+
booksLinkRef = redirectedPath;
|
|
2324
|
+
linkRef = redirectedPath.substring("_books/".length);
|
|
2325
|
+
window.history.replaceState(null, null, window.location.origin + "/" + linkRef + hash);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
if(response.headers.has("X-esp-node"))
|
|
2330
|
+
{
|
|
2331
|
+
view.docApp.node = response.headers.get("X-esp-node").split(".")[0];
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
if(response.headers.has("X-esp-build"))
|
|
2335
|
+
{
|
|
2336
|
+
view.docApp.build = response.headers.get("X-esp-build");
|
|
2337
|
+
}
|
|
2338
|
+
if(response.headers.has("X-frontmatter"))
|
|
2339
|
+
{
|
|
2340
|
+
frontmatterData = response.headers.get("X-frontmatter");
|
|
2341
|
+
frontmatterData = JSON.parse(atob(frontmatterData));
|
|
2342
|
+
view.docApp.frontmatterData = frontmatterData;
|
|
2343
|
+
} else {
|
|
2344
|
+
//-- the fallback path used to parse into the local only and never write it back
|
|
2345
|
+
//-- to docApp, so the previous page's frontmatter stayed live - addPdfIconToHeader
|
|
2346
|
+
//-- reads docApp.frontmatterData['pdf-path'] and served the wrong PDF. getMeta
|
|
2347
|
+
//-- returns "" when the tag is absent, so normalise to an object. //-- SG
|
|
2348
|
+
let metaFrontmatter = getMeta("hdoc:frontmatter");
|
|
2349
|
+
frontmatterData = metaFrontmatter ? JSON.parse(atob(metaFrontmatter)) : {};
|
|
2350
|
+
view.docApp.frontmatterData = frontmatterData;
|
|
2351
|
+
}
|
|
283
2352
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
2353
|
+
return response.text();
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2356
|
+
try
|
|
2357
|
+
{
|
|
2358
|
+
let contentResults = await Promise.all([infoPromise,contentPromise]);
|
|
2359
|
+
view.docApp.bookInfo = contentResults[0] || null;
|
|
2360
|
+
let injectBody = contentContainer();
|
|
2361
|
+
if(injectBody) injectBody.innerHTML = contentResults[1]; //-- inject content if not from initial load
|
|
2362
|
+
postProcessBookContentRender(linkRef+hash,frontmatterData,booksLinkRef);
|
|
2363
|
+
}
|
|
2364
|
+
catch(e)
|
|
2365
|
+
{
|
|
2366
|
+
//-- previously an unhandled rejection - log it rather than failing silently. Same
|
|
2367
|
+
//-- visible outcome as before: nothing injected, no post-processing. //-- SG
|
|
2368
|
+
hslDocLog.log("Failed to load content for ["+booksLinkRef+"]",e);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
//console.log(view.docApp)
|
|
293
2372
|
}
|
|
294
2373
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
jqCurrHeader.addClass("faq-toc-item");
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const headersTocItem = container.getElementsByClassName("faq-toc-item");
|
|
313
|
-
for (let k = 0; k < headersTocItem.length; k++) {
|
|
314
|
-
const item = headersTocItem[k];
|
|
315
|
-
const strLabel = $.trim(item.innerText);
|
|
316
|
-
if (strLabel) {
|
|
317
|
-
const seoSafeId = toSeoUrl(strLabel); //-- will use SEO to set in url so can scroll to element from url
|
|
318
|
-
const linkUrl = `${currUrl.pathname}#${seoSafeId}`;
|
|
319
|
-
item.id = seoSafeId;
|
|
320
|
-
|
|
321
|
-
view.docApp.tableOfContents.push({
|
|
322
|
-
ele: item,
|
|
323
|
-
eleText: strLabel,
|
|
324
|
-
tagName: item.tagName,
|
|
325
|
-
href: linkUrl,
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
//-- add link icon to header (using seo id)
|
|
329
|
-
$(item).append(
|
|
330
|
-
`<a class="header-anchor" aria-label="Link to this section: ${strLabel}"><i class="bi bi-link" aria-hidden="true"></i></a>`,
|
|
331
|
-
);
|
|
332
|
-
$(item).find("a").attr("href", linkUrl);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function loadContentUrl(
|
|
339
|
-
linkRef,
|
|
340
|
-
fromPageRefresh,
|
|
341
|
-
fromPopState,
|
|
342
|
-
fromBook = true,
|
|
343
|
-
) {
|
|
344
|
-
let downloadFile = false;
|
|
345
|
-
if (linkRef.includes('_books/')) {
|
|
346
|
-
downloadFile = linkRef.split('').length > 1;
|
|
347
|
-
console.log('downloadFile', downloadFile);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
//-- clear table of contents
|
|
351
|
-
view.docApp.tableOfContents = [];
|
|
352
|
-
let clean_linkRef = linkRef;
|
|
353
|
-
if (clean_linkRef.indexOf(`${window.location.origin}/`) === 0) {
|
|
354
|
-
clean_linkRef = clean_linkRef.replace(`${window.location.origin}/`, "");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
//-- remove trailing / if has one
|
|
358
|
-
clean_linkRef = removeTrailingSlash(clean_linkRef);
|
|
359
|
-
//-- remove starting / if link starts with it
|
|
360
|
-
clean_linkRef = removeStartingSlash(clean_linkRef);
|
|
361
|
-
|
|
362
|
-
let booksLinkRef = clean_linkRef;
|
|
363
|
-
if (fromBook) {
|
|
364
|
-
booksLinkRef = clean_linkRef.includes("_books/")
|
|
365
|
-
? clean_linkRef
|
|
366
|
-
: `_books/${clean_linkRef}`;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (downloadFile) {
|
|
370
|
-
//-- open in new tab to download
|
|
371
|
-
window.open(booksLinkRef, '_blank');
|
|
372
|
-
} else {
|
|
373
|
-
//-- update url in browser
|
|
374
|
-
if (fromBook) {
|
|
375
|
-
setBrowserViewUrl(clean_linkRef, {}, fromPageRefresh, fromPopState);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
//-- destroy any existing content children and events properly
|
|
379
|
-
jqContentContainer.empty();
|
|
380
|
-
jqDocumentHeader.find(".document-title").empty();
|
|
381
|
-
|
|
382
|
-
//-- for now just load whatever the href prop is (we can add actions to perform specific processing and then load content based on element atts etc)
|
|
383
|
-
let frontmatterData = {};
|
|
384
|
-
fetch(booksLinkRef)
|
|
385
|
-
.then((response) => {
|
|
386
|
-
if (response.headers.has("X-frontmatter")) {
|
|
387
|
-
frontmatterData = response.headers.get("X-frontmatter");
|
|
388
|
-
frontmatterData = JSON.parse(atob(frontmatterData));
|
|
389
|
-
}
|
|
390
|
-
return response.text();
|
|
391
|
-
})
|
|
392
|
-
.then((html) => {
|
|
393
|
-
jqContentContainer.html(html);
|
|
394
|
-
|
|
395
|
-
//-- remove previous layoutclass if have one
|
|
396
|
-
if (global.lastLayoutClass) {
|
|
397
|
-
$("#DocContent").removeClass(global.lastLayoutClass);
|
|
398
|
-
global.lastLayoutClass = "";
|
|
2374
|
+
function addPdfIconToHeader(direct_BooksLinkRef)
|
|
2375
|
+
{
|
|
2376
|
+
//-- do we have a pdf
|
|
2377
|
+
if(view.docApp.bookInfo && view.docApp.bookInfo.pdf_size)
|
|
2378
|
+
{
|
|
2379
|
+
let tooltipSize = "Download (" + formatBytes(view.docApp.bookInfo.pdf_size) +")";
|
|
2380
|
+
let bodyForToolbar = contentContainer();
|
|
2381
|
+
let tbRight = bodyForToolbar ? bodyForToolbar.querySelector(".toolbar-right") : null;
|
|
2382
|
+
if(tbRight)
|
|
2383
|
+
{
|
|
2384
|
+
let pdfUrl = direct_BooksLinkRef + ".pdf";
|
|
2385
|
+
if (view.docApp.frontmatterData['pdf-path'] && view.docApp.frontmatterData['pdf-path'] !== '') {
|
|
2386
|
+
pdfUrl = '/_books/' + view.docApp.frontmatterData['pdf-path'];
|
|
399
2387
|
}
|
|
2388
|
+
|
|
2389
|
+
if(!tbRight.querySelector(".pdffile"))
|
|
2390
|
+
{
|
|
2391
|
+
tbRight.insertAdjacentHTML("afterbegin",`<span class="toolbar-action pdffile me-1" ><a target="_blank" title="${tooltipSize}" href="${pdfUrl}" download rel="nofollow"><span>Download</span><span>PDF</span></a></span>`);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
400
2396
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
2397
|
+
function findFirstClickableBookLink(findByLinkRef,currentNavSection)
|
|
2398
|
+
{
|
|
2399
|
+
if(currentNavSection === undefined)
|
|
2400
|
+
currentNavSection = view.docApp.navSections;
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
for(let x=0;x<currentNavSection.length;x++)
|
|
2404
|
+
{
|
|
2405
|
+
if(currentNavSection[x].items)
|
|
2406
|
+
{
|
|
2407
|
+
let clickableItem = findFirstClickableBookLink(findByLinkRef,currentNavSection[x].items);
|
|
2408
|
+
if(clickableItem)
|
|
2409
|
+
{
|
|
2410
|
+
return clickableItem;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
else if(currentNavSection[x].link)
|
|
2414
|
+
{
|
|
2415
|
+
if(findByLinkRef && findByLinkRef.indexOf("/")>-1)
|
|
2416
|
+
{
|
|
2417
|
+
if(removeStartingSlash(removeTrailingSlash(currentNavSection[x].link)) == findByLinkRef) return currentNavSection[x].link;
|
|
2418
|
+
}
|
|
2419
|
+
else
|
|
2420
|
+
{
|
|
2421
|
+
return currentNavSection[x].link;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
406
2426
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
2427
|
+
function expandNavParentSectionBySelectedLinkHref(findByLinkRef, currentNavSection)
|
|
2428
|
+
{
|
|
2429
|
+
findByLinkRef = findByLinkRef.split("#")[0];
|
|
2430
|
+
if(currentNavSection === undefined)
|
|
2431
|
+
{
|
|
2432
|
+
currentNavSection = view.docApp.navSections;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
for(let x=0;x<currentNavSection.length;x++)
|
|
2436
|
+
{
|
|
2437
|
+
if(currentNavSection[x].items)
|
|
2438
|
+
{
|
|
2439
|
+
let res = expandNavParentSectionBySelectedLinkHref(findByLinkRef,currentNavSection[x].items);
|
|
2440
|
+
if(res)
|
|
2441
|
+
{
|
|
2442
|
+
currentNavSection[x].expand=true;
|
|
2443
|
+
return res;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
else if(currentNavSection[x].link)
|
|
2447
|
+
{
|
|
2448
|
+
//-- check item to see if it matches url. The .link guard matches the one in
|
|
2449
|
+
//-- findFirstClickableBookLink - a nav leaf with no link used to throw here. //-- SG
|
|
2450
|
+
if(removeStartingSlash(removeTrailingSlash(currentNavSection[x].link)) == findByLinkRef)
|
|
2451
|
+
{
|
|
2452
|
+
return true;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
417
2456
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
//-- do any code highlighting
|
|
459
|
-
if (hljs) {
|
|
460
|
-
for (const el of document.querySelectorAll("pre code")) {
|
|
461
|
-
hljs.highlightBlock(el);
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
hslDocLog.log(
|
|
465
|
-
"code highlighting js could not be loaded. code block highlighting will not be enabled",
|
|
466
|
-
);
|
|
467
|
-
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function setBrowserViewUrl(strHrefToLoad,viewData,fromPageRefresh)
|
|
2460
|
+
{
|
|
2461
|
+
|
|
2462
|
+
let urlToSet = window.location.origin + "/" + strHrefToLoad;
|
|
2463
|
+
const url = new URL(urlToSet);
|
|
2464
|
+
|
|
2465
|
+
//-- remove any old params
|
|
2466
|
+
for(let strKey in global.stateParams)
|
|
2467
|
+
{
|
|
2468
|
+
url.searchParams.delete(strKey);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
//-- now set new data params in url
|
|
2472
|
+
for(let strKey in viewData)
|
|
2473
|
+
{
|
|
2474
|
+
url.searchParams.set(strKey,viewData[strKey]);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
//-- store global params
|
|
2478
|
+
global.stateParams = viewData;
|
|
2479
|
+
|
|
2480
|
+
//-- if havent clicked back or forwards, and this isn't a refresh/first paint (where the
|
|
2481
|
+
//-- browser already holds the right entry), record the navigation. Push only when the url
|
|
2482
|
+
//-- actually changes, compared against the LIVE location - the old "lastUrlDynamicallySet"
|
|
2483
|
+
//-- string went stale after back-button trips (home -> book -> back -> back -> same book
|
|
2484
|
+
//-- again matched the stale value and replaceState'd over the home entry, so the second
|
|
2485
|
+
//-- back skipped the book page). Same-url re-clicks now simply do nothing: the entry the
|
|
2486
|
+
//-- user is on is already correct. //-- SG
|
|
2487
|
+
if(!currentPopState && !fromPageRefresh)
|
|
2488
|
+
{
|
|
2489
|
+
if(url.href !== window.location.href)
|
|
2490
|
+
{
|
|
2491
|
+
window.history.pushState(null, null, url); //-- add entry
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
currentPopState = null;
|
|
2495
|
+
}
|
|
468
2496
|
|
|
469
|
-
//-- render any Mermaid diagrams client-side (lazy-loads the bundle)
|
|
470
|
-
hbRenderMermaidDiagrams();
|
|
471
|
-
|
|
472
|
-
//-- show copy button by any code blocks
|
|
473
|
-
const options = {
|
|
474
|
-
contentSelector: ".injected-document-content",
|
|
475
|
-
// Delay in ms used for `setTimeout` before badging is applied
|
|
476
|
-
// Use if you need to time highlighting and badge application
|
|
477
|
-
// since the badges need to be applied afterwards.
|
|
478
|
-
// 0 - direct execution (ie. you handle timing
|
|
479
|
-
loadDelay: 0,
|
|
480
|
-
|
|
481
|
-
// CSS class(es) used to render the copy icon.
|
|
482
|
-
copyIconClass: "bi bi-clipboard",
|
|
483
|
-
// CSS class(es) used to render the done icon.
|
|
484
|
-
checkIconClass: "bi bi-clipboard-check --htl-c-brand",
|
|
485
|
-
|
|
486
|
-
// intercept text copying - passed in text return text out
|
|
487
|
-
onBeforeCodeCopied: (text) => text,
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
highlightJsBadge(options);
|
|
491
|
-
|
|
492
|
-
//-- get first H1 and put it into our header section header
|
|
493
|
-
const docHeader = jqContentContainer.find("H1").eq(0);
|
|
494
|
-
jqDocumentHeader.find(".document-title").append(docHeader);
|
|
495
|
-
|
|
496
|
-
setGitHubBookUrl(clean_linkRef);
|
|
497
|
-
|
|
498
|
-
//-- make sure selected link parents are set to .expand=true
|
|
499
|
-
if (expandNavParentSectionBySelectedLinkHref(clean_linkRef)) {
|
|
500
|
-
view.updateCounter++;
|
|
501
|
-
view.$nextTick(() => {
|
|
502
|
-
//-- find any navigation links that match url and highlight
|
|
503
|
-
// listenForHrefClicks();
|
|
504
|
-
highlightNavigationLinkFromUrl(clean_linkRef);
|
|
505
|
-
|
|
506
|
-
//-- scroll to element that match hash (if have one)
|
|
507
|
-
if (document.location.hash) {
|
|
508
|
-
const gotoEle = getAnchorFromHash(document.location.hash);
|
|
509
|
-
if (gotoEle) gotoEle[0].scrollIntoView();
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
view.$forceUpdate();
|
|
513
|
-
}
|
|
514
2497
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
2498
|
+
//-- Decide whether the content-type filter has dead-ended the search.
|
|
2499
|
+
//--
|
|
2500
|
+
//-- The filter defaults to Documentation because reference material is ~85% of the
|
|
2501
|
+
//-- corpus. That is right for most searches and wrong for some: "userGetInfo" has no
|
|
2502
|
+
//-- documentation answer at all. Without this the default would turn a findable result
|
|
2503
|
+
//-- into a blank page, which is worse than the noise it was added to remove.
|
|
2504
|
+
//--
|
|
2505
|
+
//-- One outcome: shown === 0 -> widen automatically and say why. Never show an empty
|
|
2506
|
+
//-- page when the answer exists one filter away. A filter that is merely hiding
|
|
2507
|
+
//-- results is left alone - the Information type facet counts show what is hidden.
|
|
2508
|
+
//--
|
|
2509
|
+
//-- Returns true when the caller should re-run the search. Cannot loop: widening sets
|
|
2510
|
+
//-- the filter to "" and this returns early when no filter is active. //-- SG
|
|
2511
|
+
//-- Set for exactly one evaluation after an automatic widen. The widen triggers a
|
|
2512
|
+
//-- second search, and that response carries no filter - without this the explanation
|
|
2513
|
+
//-- would be cleared before the user ever read it. //-- SG
|
|
2514
|
+
let docTypeAutoWidened = false;
|
|
2515
|
+
|
|
2516
|
+
//-- The query an automatic widen was applied to. A widen is a rescue for the query that
|
|
2517
|
+
//-- needed it, not a new preference, so the next DIFFERENT query goes back to the
|
|
2518
|
+
//-- default. A filter the user picked themselves is left alone - that is an explicit
|
|
2519
|
+
//-- choice and should persist the way any other facet does. //-- SG
|
|
2520
|
+
let docTypeAutoWidenedForQuery = null;
|
|
2521
|
+
|
|
2522
|
+
//-- Content type shown when the user has not chosen otherwise: 0 = Documentation
|
|
2523
|
+
const docTypeDefaultFilter = "0";
|
|
2524
|
+
|
|
2525
|
+
//-- On the INTERNAL site only, the Information type the user last picked is remembered
|
|
2526
|
+
//-- across sessions, so the facet comes back the way they left it. Internal users
|
|
2527
|
+
//-- search the whole corpus (api/db reference included) and re-picking "All" on every
|
|
2528
|
+
//-- visit was pure friction; public visitors keep the Documentation default, which is
|
|
2529
|
+
//-- tuned for them (see the note on searchDocTypeFilter in the data block).
|
|
2530
|
+
//--
|
|
2531
|
+
//-- Stored in localStorage, NOT in the /search url - a pasted search link must keep
|
|
2532
|
+
//-- meaning exactly what its params say, whoever opens it. A dt in the url still wins
|
|
2533
|
+
//-- for that page load, and does not overwrite the stored preference.
|
|
2534
|
+
//--
|
|
2535
|
+
//-- Gated the same way as showAudienceFilter / showSearchMatchBadges: everything that is
|
|
2536
|
+
//-- not the public site, so docs-internal AND the dev boxes behave alike - behaviour that
|
|
2537
|
+
//-- cannot be reproduced on a dev host cannot be tested there either. //-- SG
|
|
2538
|
+
const docTypeFilterPersisted = window.location.hostname !== "docs.hornbill.com";
|
|
2539
|
+
const docTypeFilterStorageKey = "hdocbook-search-doctype";
|
|
2540
|
+
|
|
2541
|
+
//-- Transient "needs two characters" hint by the toolbar search box. Self-clearing: the hint
|
|
2542
|
+
//-- is feedback on one key press, so it must not outlive it and must never need dismissing.
|
|
2543
|
+
//-- Any search that actually runs cancels it. //-- SG
|
|
2544
|
+
let searchTooShortHintTimeoutId = null;
|
|
2545
|
+
function showSearchTooShortHint()
|
|
2546
|
+
{
|
|
2547
|
+
clearTimeout(searchTooShortHintTimeoutId);
|
|
2548
|
+
view.docApp.searchTooShortHint = true;
|
|
2549
|
+
searchTooShortHintTimeoutId = setTimeout(function(){ view.docApp.searchTooShortHint = false; },3500);
|
|
2550
|
+
}
|
|
2551
|
+
function hideSearchTooShortHint()
|
|
2552
|
+
{
|
|
2553
|
+
clearTimeout(searchTooShortHintTimeoutId);
|
|
2554
|
+
if(view.docApp.searchTooShortHint) view.docApp.searchTooShortHint = false;
|
|
519
2555
|
}
|
|
520
2556
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
2557
|
+
//-- localStorage access throws outright in some privacy modes, so every read/write is
|
|
2558
|
+
//-- guarded and a failure just means the plain default //-- SG
|
|
2559
|
+
function docTypeUserDefaultFilter()
|
|
2560
|
+
{
|
|
2561
|
+
if(!docTypeFilterPersisted) return docTypeDefaultFilter;
|
|
2562
|
+
try
|
|
2563
|
+
{
|
|
2564
|
+
let stored = localStorage.getItem(docTypeFilterStorageKey);
|
|
2565
|
+
//-- "" is a real stored choice (All); only absent or malformed falls back
|
|
2566
|
+
if(stored === "" || (typeof stored === "string" && /^[0-9]+$/.test(stored))) return stored;
|
|
2567
|
+
}
|
|
2568
|
+
catch(e){}
|
|
2569
|
+
return docTypeDefaultFilter;
|
|
527
2570
|
}
|
|
528
2571
|
|
|
529
|
-
|
|
530
|
-
|
|
2572
|
+
//-- Only ever called from an explicit user pick. An automatic widen is a rescue for one
|
|
2573
|
+
//-- query, not a preference, so it must never be written here. //-- SG
|
|
2574
|
+
function rememberDocTypeFilter(docType)
|
|
2575
|
+
{
|
|
2576
|
+
if(!docTypeFilterPersisted) return;
|
|
2577
|
+
try { localStorage.setItem(docTypeFilterStorageKey,docType); } catch(e){}
|
|
531
2578
|
}
|
|
532
2579
|
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
2580
|
+
function evaluateDocTypeRescue()
|
|
2581
|
+
{
|
|
2582
|
+
let app = view.docApp;
|
|
2583
|
+
|
|
2584
|
+
if(docTypeAutoWidened)
|
|
2585
|
+
{
|
|
2586
|
+
docTypeAutoWidened = false;
|
|
2587
|
+
return false;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
app.searchDocTypeRescue = null;
|
|
2591
|
+
|
|
2592
|
+
let resp = app.searchResponse;
|
|
2593
|
+
if(!resp || !resp.docTypesInfo || !resp.docTypesInfo.length || !app.searchDocTypeFilter) return false;
|
|
2594
|
+
|
|
2595
|
+
let shown = 0;
|
|
2596
|
+
let hidden = 0;
|
|
2597
|
+
let best = null;
|
|
2598
|
+
|
|
2599
|
+
for(let i=0;i<resp.docTypesInfo.length;i++)
|
|
2600
|
+
{
|
|
2601
|
+
let dt = resp.docTypesInfo[i];
|
|
2602
|
+
if(String(dt.docType) === app.searchDocTypeFilter)
|
|
2603
|
+
{
|
|
2604
|
+
shown = dt.foundCount;
|
|
2605
|
+
}
|
|
2606
|
+
else
|
|
2607
|
+
{
|
|
2608
|
+
hidden += dt.foundCount;
|
|
2609
|
+
if(best === null || dt.foundCount > best.foundCount) best = dt;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
if(hidden === 0) return false;
|
|
2614
|
+
|
|
2615
|
+
//-- only the dead-end case widens; a filter that is merely hiding results is
|
|
2616
|
+
//-- left alone. The old "Show all N results" offer promised the whole hidden
|
|
2617
|
+
//-- count but the server caps at 300 returned, so it could never deliver -
|
|
2618
|
+
//-- the Information type facet counts already show what is hidden. //-- SG
|
|
2619
|
+
if(shown === 0)
|
|
2620
|
+
{
|
|
2621
|
+
app.searchDocTypeFilter = "";
|
|
2622
|
+
app.searchDocTypeRescue = {auto:true, hidden:hidden, best:best};
|
|
2623
|
+
docTypeAutoWidened = true;
|
|
2624
|
+
docTypeAutoWidenedForQuery = app.altSearchText || app.searchText || "";
|
|
2625
|
+
return true;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
return false;
|
|
2629
|
+
}
|
|
536
2630
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
2631
|
+
//-- Decide whether the bid book filter names a book that does not exist - a mistyped or
|
|
2632
|
+
//-- stale pasted url (`/search?q=spf&bid=esp-confi`). A real book with zero matches keeps
|
|
2633
|
+
//-- its filter and shows as a selected zero-count entry; an unknown id would leave the
|
|
2634
|
+
//-- Books list with NOTHING selected and an unexplained empty result page. Same shape as
|
|
2635
|
+
//-- evaluateDocTypeRescue: clear the filter, say why, re-run once. //-- SG
|
|
2636
|
+
//-- One evaluation of grace after the automatic clear, or the re-run's own response
|
|
2637
|
+
//-- would wipe the explanation before the user read it. //-- SG
|
|
2638
|
+
let bookFilterAutoCleared = false;
|
|
2639
|
+
|
|
2640
|
+
function evaluateBookFilterRescue()
|
|
2641
|
+
{
|
|
2642
|
+
let app = view.docApp;
|
|
2643
|
+
|
|
2644
|
+
if(bookFilterAutoCleared)
|
|
2645
|
+
{
|
|
2646
|
+
bookFilterAutoCleared = false;
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
app.searchBookRescue = null;
|
|
2651
|
+
|
|
2652
|
+
if(!app.searchFilterBySpecificBookId) return false;
|
|
2653
|
+
|
|
2654
|
+
//-- only judge the id against a LOADED library - an empty book list means the
|
|
2655
|
+
//-- library fetch has not landed, not that every id is bad //-- SG
|
|
2656
|
+
if(!view.docApp.library || !Array.isArray(view.docApp.library.books) || !view.docApp.library.books.length) return false;
|
|
2657
|
+
|
|
2658
|
+
if(getBookById(app.searchFilterBySpecificBookId) !== undefined) return false;
|
|
2659
|
+
|
|
2660
|
+
app.searchBookRescue = {bid: app.searchFilterBySpecificBookId};
|
|
2661
|
+
app.searchFilterBySpecificBookId = "";
|
|
2662
|
+
app.searchFilterBySpecificBookTitle = "";
|
|
2663
|
+
bookFilterAutoCleared = true;
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function loadLibraryOrBookFromCurrentUrl(loadingUrl)
|
|
2668
|
+
{
|
|
2669
|
+
|
|
2670
|
+
//-- check if we are loading base url
|
|
2671
|
+
view.docApp.searchMode=false;
|
|
2672
|
+
view.docApp.mcpCatalogMode = false;
|
|
2673
|
+
view.docApp.searchFilteredProducts={};
|
|
2674
|
+
|
|
2675
|
+
let baseUrl = removeTrailingSlash(window.location.origin);
|
|
2676
|
+
let currentUrl = removeTrailingSlash(loadingUrl || window.location.href);
|
|
2677
|
+
if(currentUrl && currentUrl!==baseUrl && currentUrl.indexOf("/?mode=")===-1)
|
|
2678
|
+
{
|
|
2679
|
+
//-- we are trying to load a book
|
|
2680
|
+
let bookId = window.location.href.split(baseUrl+"/")[1].split("/")[0];
|
|
2681
|
+
if(bookId.indexOf("mcp?")===0 || bookId==="mcp")
|
|
2682
|
+
{
|
|
2683
|
+
//-- /mcp - the MCP server catalog. Mirrors the search branch below,
|
|
2684
|
+
//-- including the currentPopState clear: this branch never reaches
|
|
2685
|
+
//-- setBrowserViewUrl(), which is the only place that consumes and clears
|
|
2686
|
+
//-- it, and a stale value silently suppresses the NEXT link click's
|
|
2687
|
+
//-- pushState. //-- SG
|
|
2688
|
+
bInitialPageLoad = false;
|
|
2689
|
+
view.docApp.book = {};
|
|
2690
|
+
view.docApp.mcpCatalogMode = true;
|
|
2691
|
+
document.title = "Connect an AI assistant - Hornbill Documentation";
|
|
2692
|
+
|
|
2693
|
+
view.loadMcpCatalog();
|
|
2694
|
+
|
|
2695
|
+
revealApp();
|
|
2696
|
+
|
|
2697
|
+
currentPopState = null;
|
|
2698
|
+
}
|
|
2699
|
+
else if(bookId.indexOf("search?")===0 || bookId==="search")
|
|
2700
|
+
{
|
|
2701
|
+
//-- we are in search mode.
|
|
2702
|
+
bInitialPageLoad = false;
|
|
2703
|
+
view.docApp.book = {};
|
|
2704
|
+
|
|
2705
|
+
//-- do we have a search term to search for
|
|
2706
|
+
|
|
2707
|
+
view.docApp.searchFilterBySpecificBookId = "";
|
|
2708
|
+
|
|
2709
|
+
//-- parse the search criteria properly. The old split("?q=")/split("&bid=") parse
|
|
2710
|
+
//-- swallowed every OTHER param into the q value, so back-button onto a filtered
|
|
2711
|
+
//-- search (?q=workflow&dt=5) re-ran the search as q="workflow&dt=5" and the
|
|
2712
|
+
//-- rebuilt url showed q=workflow%26dt%3D5. //-- SG
|
|
2713
|
+
let searchUrlParams = new URL(window.location.href).searchParams;
|
|
2714
|
+
let initialSearch = searchUrlParams.get("q") || "";
|
|
2715
|
+
let initalBookIdFilter = searchUrlParams.get("bid") || "";
|
|
2716
|
+
|
|
2717
|
+
//-- restore the filters this history entry was made with. Only when present:
|
|
2718
|
+
//-- an absent dt is ambiguous (first load default vs auto-widened "everything"),
|
|
2719
|
+
//-- so leave the current filter state alone in that case //-- SG
|
|
2720
|
+
if(searchUrlParams.has("dt")) view.docApp.searchDocTypeFilter = searchUrlParams.get("dt");
|
|
2721
|
+
if(searchUrlParams.has("audience")) view.docApp.searchAudienceFilter = searchUrlParams.get("audience");
|
|
2722
|
+
|
|
2723
|
+
view.toolbarSearch(initialSearch,initalBookIdFilter,true);
|
|
2724
|
+
|
|
2725
|
+
//-- remove hidden class
|
|
2726
|
+
revealApp();
|
|
2727
|
+
|
|
2728
|
+
//-- this branch never reaches setBrowserViewUrl (the only place that consumes and
|
|
2729
|
+
//-- clears currentPopState), so clear it here - a stale value suppressed the
|
|
2730
|
+
//-- pushState of the NEXT normal link click, leaving the url stuck. //-- SG
|
|
2731
|
+
currentPopState = null;
|
|
2732
|
+
}
|
|
2733
|
+
else if(bookId)
|
|
2734
|
+
{
|
|
2735
|
+
|
|
2736
|
+
if(!findBookByIdAndLoad(bookId,loadingUrl))
|
|
2737
|
+
{
|
|
2738
|
+
//-- if we get here url was not found so goto base library
|
|
2739
|
+
view.loadLibraryHome();
|
|
2740
|
+
currentPopState = null; //-- see note in the search branch above //-- SG
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
else
|
|
2745
|
+
{
|
|
2746
|
+
//-- will reset view to home lib
|
|
2747
|
+
bInitialPageLoad = false;
|
|
2748
|
+
view.docApp.book = {};
|
|
2749
|
+
view.docApp.altSearchText = "";
|
|
2750
|
+
|
|
2751
|
+
//-- we have come from a back or forward browser button
|
|
2752
|
+
let startingLibraryProduct = urlGetParam(window.location.search,"product");
|
|
2753
|
+
let startingLibraryMode = urlGetParam(window.location.search,"mode");
|
|
2754
|
+
|
|
2755
|
+
let selectLayoutMode = getLibraryLayoutByModeParam(startingLibraryMode);
|
|
2756
|
+
organiseLibrary(selectLayoutMode,startingLibraryProduct);
|
|
2757
|
+
|
|
2758
|
+
//-- remove hidden class
|
|
2759
|
+
revealApp();
|
|
2760
|
+
|
|
2761
|
+
//-- see note in the search branch above: without this, back-button to the library
|
|
2762
|
+
//-- home left currentPopState set, and the next book click never updated the url
|
|
2763
|
+
//-- (then the click after that pushed onto the wrong base entry, so back skipped a
|
|
2764
|
+
//-- page). This was the reported "back button misses out the How-to page" defect. //-- SG
|
|
2765
|
+
currentPopState = null;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
function findBookByIdAndLoad(bookId,fromPopStateUrl)
|
|
2771
|
+
{
|
|
2772
|
+
//-- we have a book id fronm link like https://docs.hornbill.com/itom-config#itom-subscription-tiers instead of https://docs.hornbill.com/itom-config/index#itom-subscription-tiers
|
|
2773
|
+
if(bookId.indexOf("#")>0)
|
|
2774
|
+
{
|
|
2775
|
+
bookId = bookId.split("#",1)[0];
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
let book = getLibraryBooksIndex().get(bookId);
|
|
2779
|
+
if(book)
|
|
2780
|
+
{
|
|
2781
|
+
view.loadLibraryBook(book,fromPopStateUrl);
|
|
2782
|
+
return true;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
return false;
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
|
|
2789
|
+
function setLibraryGroupTitleAndDescription(aGroup)
|
|
2790
|
+
{
|
|
2791
|
+
//-- need to get the title and description somehow
|
|
2792
|
+
if(aGroup.mode==="audience")
|
|
2793
|
+
{
|
|
2794
|
+
aGroup.title = (aGroup.id==="public")?"Public Library":"Internal Library";
|
|
2795
|
+
//aGroup.description = (aGroup.id==="public")?"Explore all documents, guides and articles.":"Documents for internal user only, relating to internal processes, operations and technical information."
|
|
2796
|
+
aGroup.description = (aGroup.id==="public")?"Explore here. Use the toggle to the right to browse by audience or product.":"Documents for internal users only."
|
|
2797
|
+
}
|
|
2798
|
+
else if(aGroup.mode==="productFamilyId")
|
|
2799
|
+
{
|
|
2800
|
+
for(let x=0;x<view.docApp.products.length;x++)
|
|
2801
|
+
{
|
|
2802
|
+
if(aGroup.id == view.docApp.products[x].id)
|
|
2803
|
+
{
|
|
2804
|
+
aGroup.title = view.docApp.products[x].name;
|
|
2805
|
+
|
|
2806
|
+
|
|
2807
|
+
if(view.docApp.products[x].icon)
|
|
2808
|
+
{
|
|
2809
|
+
aGroup.icon = view.docApp.products[x].icon.replace("images/","images/products/");
|
|
2810
|
+
}
|
|
2811
|
+
else{
|
|
2812
|
+
aGroup.icon = "images/products/hornbill-square.svg";
|
|
2813
|
+
}
|
|
2814
|
+
return aGroup;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
return aGroup;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
function getLibraryLayoutByModeParam(groupByModeParam)
|
|
2823
|
+
{
|
|
2824
|
+
for(let x=0;x<view.docApp.groupModes.length;x++)
|
|
2825
|
+
{
|
|
2826
|
+
if(view.docApp.groupModes[x].mode===groupByModeParam)
|
|
2827
|
+
{
|
|
2828
|
+
return view.docApp.groupModes[x]
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
return view.docApp.groupModes[0];
|
|
665
2833
|
}
|
|
666
2834
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
2835
|
+
|
|
2836
|
+
function organiseLibrary(oGroupByMode,homeProductToShow)
|
|
2837
|
+
{
|
|
2838
|
+
view.docApp.selectedLibraryGroup = oGroupByMode;
|
|
2839
|
+
|
|
2840
|
+
//-- build the whole map in a local first, then assign it to the model in one go. Adding keys
|
|
2841
|
+
//-- straight onto view.docApp.libraryGroups leaves every group after "unknown" unobserved by
|
|
2842
|
+
//-- Vue 2 (it cannot see properties added to an already-reactive object), so in-place edits to
|
|
2843
|
+
//-- a group - adding/removing a book, changing a count - would never re-render. //-- SG
|
|
2844
|
+
let builtGroups = {"unknown":{title:"Other",description:"",icon : "images/products/hornbill-square.svg", books:[]}};
|
|
2845
|
+
|
|
2846
|
+
//-- loop library content and put into groups
|
|
2847
|
+
let groupByMode = oGroupByMode.name;
|
|
2848
|
+
for(let x=0;x<view.docApp.library.books.length;x++)
|
|
2849
|
+
{
|
|
2850
|
+
if(view.docApp.library.books[x].hasOwnProperty(groupByMode))
|
|
2851
|
+
{
|
|
2852
|
+
//-- 11//07/2023 - fix audience so either public or internal (because at moment we have some books where audience is set to private and private.dev etc)
|
|
2853
|
+
if(groupByMode==="audience" && view.docApp.library.books[x].audience!=="public")
|
|
2854
|
+
{
|
|
2855
|
+
view.docApp.library.books[x].audience = "internal";
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
let groupingName = view.docApp.library.books[x][groupByMode];
|
|
2859
|
+
if(!builtGroups.hasOwnProperty(groupingName))
|
|
2860
|
+
{
|
|
2861
|
+
let newGrouping = {id:groupingName,mode:groupByMode,title:groupingName,description:"",icon : "images/products/hornbill-square.svg",books:[]};
|
|
2862
|
+
builtGroups[groupingName] = setLibraryGroupTitleAndDescription(newGrouping);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
builtGroups[groupingName].books.push(view.docApp.library.books[x]);
|
|
2866
|
+
|
|
2867
|
+
}
|
|
2868
|
+
else
|
|
2869
|
+
{
|
|
2870
|
+
//-- needs to go into "uncagtegorised" or "other"
|
|
2871
|
+
builtGroups["unknown"].books.push(view.docApp.library.books[x]);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
//-- single assignment - Vue observes builtGroups in place, so every group is now reactive
|
|
2876
|
+
view.docApp.libraryGroups = builtGroups;
|
|
2877
|
+
|
|
2878
|
+
if(homeProductToShow && view.docApp.libraryGroups[homeProductToShow])
|
|
2879
|
+
{
|
|
2880
|
+
view.docApp.libraryHomeViewProduct = view.docApp.libraryGroups[homeProductToShow];
|
|
2881
|
+
}
|
|
2882
|
+
else
|
|
2883
|
+
{
|
|
2884
|
+
view.docApp.libraryHomeViewProduct = null;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
673
2887
|
}
|
|
674
2888
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
2889
|
+
|
|
2890
|
+
//-- fetchOptions is optional and passed straight to fetch() - used to hand in an
|
|
2891
|
+
//-- AbortController signal for the search request. //-- SG
|
|
2892
|
+
function fetchJsonFile(strFilePath,fetchOptions)
|
|
2893
|
+
{
|
|
2894
|
+
//-- the try/catch that used to sit around response.json() could never fire: json()
|
|
2895
|
+
//-- returns a promise and rejects asynchronously, long after the try block has exited,
|
|
2896
|
+
//-- so a malformed body became an unhandled rejection and the caller's await never
|
|
2897
|
+
//-- resolved usefully. Catch on the promise instead. //-- SG
|
|
2898
|
+
return fetch(strFilePath,fetchOptions).then(function(response)
|
|
2899
|
+
{
|
|
2900
|
+
if(response && response.ok)
|
|
2901
|
+
{
|
|
2902
|
+
return response.json().catch(function(e)
|
|
2903
|
+
{
|
|
2904
|
+
hslDocLog.log("fetchJsonFile: bad JSON body from ["+strFilePath+"]",e);
|
|
2905
|
+
return null;
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
return null;
|
|
2910
|
+
});
|
|
681
2911
|
}
|
|
682
2912
|
|
|
2913
|
+
|
|
683
2914
|
//-- THE INIT APP CALLED FROM index.html
|
|
684
|
-
//$(".document-body").attr("v-pre", "");
|
|
685
|
-
|
|
686
|
-
const view = new Vue({
|
|
687
|
-
el: "#vDocDevApp",
|
|
688
|
-
data: {
|
|
689
|
-
userSession: {},
|
|
690
|
-
updateCounter: 0,
|
|
691
|
-
bookId: "",
|
|
692
|
-
docApp: {
|
|
693
|
-
error: "",
|
|
694
|
-
inlinePopupDocumentHref: "",
|
|
695
|
-
showInlineDocumentsMarkup: false,
|
|
696
|
-
keepTocLayout: false,
|
|
697
|
-
book: {},
|
|
698
|
-
navSections: [],
|
|
699
|
-
inlineDocumentItems: [],
|
|
700
|
-
tableOfContents: [],
|
|
701
|
-
bookBreadCrumb: [],
|
|
702
|
-
},
|
|
703
|
-
},
|
|
704
|
-
methods: docAppMethods,
|
|
705
|
-
directives: {
|
|
706
|
-
somedirectivename: {
|
|
707
|
-
bind(el, binding, vnode) {
|
|
708
|
-
//-- do something when created on element
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
mounted: () => {},
|
|
713
|
-
});
|
|
714
2915
|
|
|
715
|
-
//--
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
2916
|
+
//-- here it is important to add v-pre to document-body content div so that vue does not process any vue type sytnax in the document content i.e. without this vue would try parse out {{dob}}
|
|
2917
|
+
//$(".injected-document-content").attr("v-pre","");
|
|
2918
|
+
document.querySelectorAll(".document-body").forEach(function(el){ el.setAttribute("v-pre",""); });
|
|
2919
|
+
|
|
2920
|
+
//-- repeatable conmponent for nav sections.
|
|
2921
|
+
//-- registered BEFORE the app is created: Vue 3 resolves components from the app instance, and
|
|
2922
|
+
//-- hbCreateApp copies the registry at creation time, so anything registered afterwards would
|
|
2923
|
+
//-- not be visible to this app. (Vue 2 had a global registry and did not care about order.)
|
|
2924
|
+
hbVue.component('nav-section-component', {
|
|
2925
|
+
props: ['asection'],
|
|
2926
|
+
template: '#nav-section-template',
|
|
2927
|
+
methods:{
|
|
2928
|
+
toggleNavCollapse:function(navSection)
|
|
2929
|
+
{
|
|
2930
|
+
//-- no $forceUpdate() needed: navSection reaches us through the reactive proxy, and
|
|
2931
|
+
//-- renderNavigation() seeds expand, so this assignment triggers a targeted re-render
|
|
2932
|
+
//-- rather than needing a forced repaint of the whole component. //-- SG
|
|
2933
|
+
navSection.expand=!navSection.expand;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
//-- `data` is a function in Vue 3 - an object literal is no longer accepted, because every app
|
|
2939
|
+
//-- created from the same options object would otherwise share one state object. //-- SG
|
|
2940
|
+
//-- Reveal the app once a view is ready.
|
|
2941
|
+
//--
|
|
2942
|
+
//-- hb-hidden is a PRE-VUE guard: visibility:hidden !important from a static <style> so the
|
|
2943
|
+
//-- uncompiled template - raw mustaches, v-if markup - is never painted while Vue boots. That
|
|
2944
|
+
//-- is exactly why it cannot be a Vue binding: before Vue runs, a :class attribute does nothing
|
|
2945
|
+
//-- and the flash comes back.
|
|
2946
|
+
//--
|
|
2947
|
+
//-- So it belongs on the mount host and nowhere else. The host is the one element Vue does not
|
|
2948
|
+
//-- own - it renders INTO it - which is what makes removing the class here safe. The same class
|
|
2949
|
+
//-- on elements INSIDE the template was redundant (visibility inherits) and was a latent copy of
|
|
2950
|
+
//-- the bug that killed the search Filters button: Vue rewrites the static class list on every
|
|
2951
|
+
//-- re-render and puts the flag straight back. //-- SG
|
|
2952
|
+
function revealApp()
|
|
2953
|
+
{
|
|
2954
|
+
//-- index.html loads its stylesheets non-render-blocking (media="print" flipped on load) so
|
|
2955
|
+
//-- the boot splash can paint without waiting for all 15 of them. The trade is that the app
|
|
2956
|
+
//-- must not be revealed before they have applied, or it appears unstyled - so the wait moved
|
|
2957
|
+
//-- from the browser's first paint to here. Re-entrant: queue once, and hbCssMarkReady() calls
|
|
2958
|
+
//-- us back.
|
|
2959
|
+
//-- Guarded on hbCssWaiters existing because _inline.html shares this file and does NOT set up
|
|
2960
|
+
//-- any of this - there, and anywhere else without the globals, reveal is immediate as before.
|
|
2961
|
+
if(window.hbCssWaiters && !window.hbCssReady)
|
|
2962
|
+
{
|
|
2963
|
+
if(window.hbCssWaiters.indexOf(revealApp) === -1) window.hbCssWaiters.push(revealApp);
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
let host = document.getElementById("vDocDevApp");
|
|
2968
|
+
if(host) host.classList.remove("hb-hidden");
|
|
2969
|
+
|
|
2970
|
+
//-- the old global $(".hb-hidden") sweep also cleared the content-loading hide, so this
|
|
2971
|
+
//-- keeps that coupling rather than making every caller remember two things. Guarded
|
|
2972
|
+
//-- because init calls this before `view` is assigned. //-- SG
|
|
2973
|
+
if(typeof view !== "undefined" && view && view.docApp) view.docApp.contentLoading = false;
|
|
2974
|
+
|
|
2975
|
+
hbHideBootSplash();
|
|
2976
|
+
}
|
|
726
2977
|
|
|
2978
|
+
//-- Tear down the #hbBootSplash boot state (markup and CSS both in index.html). Called from
|
|
2979
|
+
//-- revealApp(), which every success AND every failure path already funnels through - that matters
|
|
2980
|
+
//-- more than it looks: a splash that outlives a failed boot is a permanently spinning page, which
|
|
2981
|
+
//-- is worse than the blank one it replaced. The 20s CSS-only stall message is the last resort for
|
|
2982
|
+
//-- a boot that never reaches here at all.
|
|
2983
|
+
//--
|
|
2984
|
+
//-- Two-stage removal so the fade actually runs: class first (CSS transitions opacity to 0), then
|
|
2985
|
+
//-- display:none once it is done. The display:none is not cosmetic - the element is a fixed,
|
|
2986
|
+
//-- full-viewport layer, so leaving it at opacity 0 would swallow every click on the site. Belt
|
|
2987
|
+
//-- and braces on the teardown timer because transitionend does not fire if the element was still
|
|
2988
|
+
//-- inside its 250ms opacity hold (no transition to end) or if motion is reduced. //-- SG
|
|
2989
|
+
function hbHideBootSplash()
|
|
2990
|
+
{
|
|
2991
|
+
let splash = document.getElementById("hbBootSplash");
|
|
2992
|
+
if(!splash || splash.classList.contains("hbBootSplashGone")) return;
|
|
2993
|
+
|
|
2994
|
+
splash.classList.add("hbBootSplashGone");
|
|
2995
|
+
splash.setAttribute("aria-hidden","true");
|
|
2996
|
+
|
|
2997
|
+
let removed = false;
|
|
2998
|
+
let finish = function()
|
|
2999
|
+
{
|
|
3000
|
+
if(removed) return;
|
|
3001
|
+
removed = true;
|
|
3002
|
+
splash.style.display = "none";
|
|
3003
|
+
};
|
|
3004
|
+
|
|
3005
|
+
splash.addEventListener("transitionend", finish, { once:true });
|
|
3006
|
+
setTimeout(finish, 400);
|
|
3007
|
+
}
|
|
727
3008
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
3009
|
+
var view = hbMount({
|
|
3010
|
+
data: function(){ return {
|
|
3011
|
+
docApp:{
|
|
3012
|
+
//-- These 14 were previously never declared here, only assigned at runtime with a
|
|
3013
|
+
//-- plain view.docApp.x = ..., yet every one of them is bound in index.html or
|
|
3014
|
+
//-- _inline.html. Under Vue 2 that meant none of them were reactive and the app
|
|
3015
|
+
//-- only repainted because of the scattered $forceUpdate() calls; Vue 3's Proxy
|
|
3016
|
+
//-- tracks late-added properties, so declaring them is no longer a bug fix - it is
|
|
3017
|
+
//-- for discoverability, and it is what lets the $forceUpdate() calls go.
|
|
3018
|
+
//--
|
|
3019
|
+
//-- Initial values are deliberately falsy-equivalent to the undefined they replace,
|
|
3020
|
+
//-- so first render is byte-identical. loadingBookInfo especially MUST start false:
|
|
3021
|
+
//-- the toolbar renders on v-if="!docApp.loadingBookInfo", so seeding it true would
|
|
3022
|
+
//-- hide the toolbar until a book load finished. //-- SG
|
|
3023
|
+
ThemePreference:"",
|
|
3024
|
+
//-- content hidden while the next document loads - see revealApp() //-- SG
|
|
3025
|
+
contentLoading:false,
|
|
3026
|
+
//-- book navigation sidebar, open state on narrow viewports //-- SG
|
|
3027
|
+
bookNavOpen:false,
|
|
3028
|
+
//-- frontmatter layout class applied to #DocContent //-- SG
|
|
3029
|
+
layoutClass:"",
|
|
3030
|
+
userSession:null,
|
|
3031
|
+
products:[],
|
|
3032
|
+
title:"",
|
|
3033
|
+
description:"",
|
|
3034
|
+
node:"",
|
|
3035
|
+
build:"",
|
|
3036
|
+
inlineTitle:"",
|
|
3037
|
+
loadingBookInfo:false,
|
|
3038
|
+
altSearchText:"",
|
|
3039
|
+
currentResultsSearchText:"",
|
|
3040
|
+
validSearch:false, //-- TRUE = odd number of quotes, i.e. the search is INVALID (feeds the warning in index.html)
|
|
3041
|
+
searchFilterBySpecificBookTitle:"",
|
|
3042
|
+
|
|
3043
|
+
boolSearchInProgress:false,
|
|
3044
|
+
nextSearchCriteria:null,
|
|
3045
|
+
searchText:"",
|
|
3046
|
+
searchError:"",
|
|
3047
|
+
//-- TRUE while the box holds a single character: no search can run (see the
|
|
3048
|
+
//-- length gate in searchDocuments), so the results area says so rather than
|
|
3049
|
+
//-- showing nothing or the previous query's hits //-- SG
|
|
3050
|
+
searchTooShort:false,
|
|
3051
|
+
//-- transient hint under the toolbar search box, for the one case where the
|
|
3052
|
+
//-- length gate changes nothing on screen (Enter on a single character while
|
|
3053
|
+
//-- reading a document) - see showSearchTooShortHint() //-- SG
|
|
3054
|
+
searchTooShortHint:false,
|
|
3055
|
+
searchMode:false,
|
|
3056
|
+
//-- MCP catalog view (/mcp). Mutually exclusive with the search, library
|
|
3057
|
+
//-- and book views - cleared wherever searchMode is. //-- SG
|
|
3058
|
+
mcpCatalogMode:false,
|
|
3059
|
+
mcpCatalog:{servers:[],loaded:false,error:false},
|
|
3060
|
+
//-- key -> true/false while the "copied" tick shows on a connection field
|
|
3061
|
+
mcpCopied:{},
|
|
3062
|
+
searchResponse:null,
|
|
3063
|
+
hasFilterByProducts:false,
|
|
3064
|
+
searchFilteredProducts:{},
|
|
3065
|
+
searchFilterBySpecificBookId:"",
|
|
3066
|
+
searchResultFilteredItems:[],
|
|
3067
|
+
//-- diagnostic keyword/semantic/both badges on search results are internal-only:
|
|
3068
|
+
//-- hidden on the public site, visible on docs-internal and dev/localhost //-- SG
|
|
3069
|
+
showSearchMatchBadges: window.location.hostname !== "docs.hornbill.com",
|
|
3070
|
+
//-- audience filter is internal-only too: the public site hosts public books
|
|
3071
|
+
//-- only, so a picker there is meaningless. Session-only state, deliberately
|
|
3072
|
+
//-- not persisted to the /search url. Values mirror the hdocbook.json
|
|
3073
|
+
//-- audience enum (hdocbook.schema.json in hdoc-tools) //-- SG
|
|
3074
|
+
showAudienceFilter: window.location.hostname !== "docs.hornbill.com",
|
|
3075
|
+
searchAudienceFilter:"",
|
|
3076
|
+
//-- Content-type facet. Defaults to "0" (Documentation) because reference
|
|
3077
|
+
//-- material is ~85% of the corpus, so an unfiltered search buries plain
|
|
3078
|
+
//-- answers: "reset a password" returned four API pages before anything a
|
|
3079
|
+
//-- non-technical user could act on. "" means All.
|
|
3080
|
+
//-- On docs-internal only, the user's last explicit pick is restored from
|
|
3081
|
+
//-- localStorage instead - see docTypeUserDefaultFilter() //-- SG
|
|
3082
|
+
searchDocTypeFilter:docTypeUserDefaultFilter(),
|
|
3083
|
+
//-- set when the default filter is hiding better matches than it is showing;
|
|
3084
|
+
//-- drives the offer above the results so filtering never dead-ends
|
|
3085
|
+
searchDocTypeRescue:null,
|
|
3086
|
+
//-- set when the url's bid named a book that does not exist and the filter
|
|
3087
|
+
//-- was cleared automatically - drives the explanation above the results
|
|
3088
|
+
searchBookRescue:null,
|
|
3089
|
+
//-- narrow-viewport filter panel visibility
|
|
3090
|
+
searchFiltersOpen:false,
|
|
3091
|
+
searchAudienceOptions:[
|
|
3092
|
+
{value:"",label:"All"},
|
|
3093
|
+
{value:"public",label:"Public"},
|
|
3094
|
+
{value:"private",label:"Private"},
|
|
3095
|
+
{value:"private.cloud",label:"Private: Cloud"},
|
|
3096
|
+
{value:"private.dev.platform",label:"Private: Platform Dev"},
|
|
3097
|
+
{value:"private.dev.apps",label:"Private: Apps Dev"},
|
|
3098
|
+
{value:"private.hdocs",label:"Private: HDocs"},
|
|
3099
|
+
{value:"private.elearning",label:"Private: eLearning"}
|
|
3100
|
+
],
|
|
3101
|
+
frontmatterData:{}, //-- always an object, was initialised as [] //-- SG
|
|
3102
|
+
keepTocLayout:false,
|
|
3103
|
+
tocMobileOpen:false,
|
|
3104
|
+
library:{},
|
|
3105
|
+
libraryGroups:{},
|
|
3106
|
+
libraryHomeViewProduct:null,
|
|
3107
|
+
book:{},
|
|
3108
|
+
bookInfo:null,
|
|
3109
|
+
navSections:[],
|
|
3110
|
+
tableOfContents:[],
|
|
3111
|
+
selectedLibraryGroup : null,
|
|
3112
|
+
groupModes:[
|
|
3113
|
+
{name:"audience",mode:"audience",title:"By Audience",layout:"column-list"},
|
|
3114
|
+
{name:"productFamilyId",mode:"product",title:"By Product", layout:"tiles"}
|
|
3115
|
+
]
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
},
|
|
3119
|
+
methods:docAppMethods
|
|
3120
|
+
}, '#vDocDevApp');
|
|
3121
|
+
|
|
3122
|
+
//-- Vue 3 compiles the mount element's innerHTML and leaves the element itself alone, so the
|
|
3123
|
+
//-- v-on:click that used to sit on #vDocDevApp is no longer compiled. index.html declares it as
|
|
3124
|
+
//-- data-root-click and we attach it here; _inline.html omits the attribute and so gets no
|
|
3125
|
+
//-- handler, matching what it did before. //-- SG
|
|
3126
|
+
hbBindRootHandler(view, '#vDocDevApp', 'click');
|
|
3127
|
+
|
|
3128
|
+
//-- One delegated click handler for every link on the page, for the life of the session.
|
|
3129
|
+
//-- Bound here alongside the root handler so it is in place before any content is injected.
|
|
3130
|
+
bindDelegatedLinkHandler();
|
|
3131
|
+
|
|
3132
|
+
var hslDocLog={
|
|
3133
|
+
_entry:[],
|
|
3134
|
+
//-- ring buffer. This used to push forever, and search entries retain whole response objects
|
|
3135
|
+
//-- (300 results each), so a long-lived tab leaked steadily for output nobody reads unless
|
|
3136
|
+
//-- they call dump(). //-- SG
|
|
3137
|
+
_maxEntries:500,
|
|
3138
|
+
log:function(strMessage,varData)
|
|
3139
|
+
{
|
|
3140
|
+
//if(varData!==undefined) console.log(strMessage,varData)
|
|
3141
|
+
//else console.log(strMessage);
|
|
3142
|
+
|
|
3143
|
+
this._entry.push({msg:strMessage,data:varData})
|
|
3144
|
+
if(this._entry.length > this._maxEntries) this._entry.splice(0,this._entry.length - this._maxEntries);
|
|
3145
|
+
},
|
|
3146
|
+
dump:function()
|
|
3147
|
+
{
|
|
3148
|
+
for(let x=0;x<this._entry.length;x++)
|
|
3149
|
+
{
|
|
3150
|
+
console.log(this._entry[x].msg,this._entry[x].data || null);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
733
3154
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
3155
|
+
//-- Both return a DOM element or null. They returned jQuery collections, which no-op when
|
|
3156
|
+
//-- empty; callers must now null-check, and every one of them does. //-- SG
|
|
3157
|
+
function vueContentContainer(){
|
|
3158
|
+
return document.querySelector(".injected-document-content .vue-document-body");
|
|
3159
|
+
}
|
|
3160
|
+
function contentContainer(){
|
|
3161
|
+
return document.querySelector(".injected-document-content .document-body");
|
|
3162
|
+
}
|
|
742
3163
|
|
|
743
3164
|
async function intialiseApp() {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
if (newInitialLoadContentHref !== undefined) initialLoadContentHref = newInitialLoadContentHref;
|
|
795
|
-
if (initialLoadContentHref) {
|
|
796
|
-
//-- find the link we are going to show and make sure parent/s are set to .expand=true
|
|
797
|
-
loadContentUrl(initialLoadContentHref, true, false);
|
|
798
|
-
} else {
|
|
799
|
-
hslDocLog.log("initial first view link to load is not present");
|
|
800
|
-
}
|
|
801
|
-
$(".hb-hidden").removeClass("hb-hidden");
|
|
802
|
-
}, 200);
|
|
803
|
-
})
|
|
804
|
-
.catch((data) => {
|
|
805
|
-
view.docApp.error = data;
|
|
806
|
-
$(".hb-hidden").removeClass("hb-hidden");
|
|
807
|
-
});
|
|
808
|
-
});
|
|
809
|
-
}
|
|
3165
|
+
|
|
3166
|
+
document.title = "Hornbill Documentation";
|
|
3167
|
+
|
|
3168
|
+
view.docApp.ThemePreference = ThemePreference;
|
|
3169
|
+
|
|
3170
|
+
//-- get session info
|
|
3171
|
+
hslDocLog.log("Hornbill docs get session");
|
|
3172
|
+
await fetchJsonFile("_api/session/").then(function(sessionData)
|
|
3173
|
+
{
|
|
3174
|
+
hslDocLog.log("Hornbill docs get session response",sessionData);
|
|
3175
|
+
if(sessionData && sessionData.type==="user")
|
|
3176
|
+
{
|
|
3177
|
+
hslDocLog.log("Hornbill docs set session set expires date time to browser locale")
|
|
3178
|
+
sessionData.expiresDateTime = new Date(sessionData.validUntil).toLocaleDateString() + ' ' + new Date(sessionData.validUntil).toLocaleTimeString();
|
|
3179
|
+
hslDocLog.log("Hornbill docs update vue session object model",sessionData);
|
|
3180
|
+
//-- userSession is declared in data(), so assigning it re-renders the profile dropdown
|
|
3181
|
+
//-- and the admin gear on its own - no $forceUpdate() needed. //-- SG
|
|
3182
|
+
view.setSessionInfo(sessionData);
|
|
3183
|
+
}
|
|
3184
|
+
});
|
|
3185
|
+
//-- eof session stuff
|
|
3186
|
+
|
|
3187
|
+
|
|
3188
|
+
await fetchJsonFile("_books/products.json").then(function(data){
|
|
3189
|
+
|
|
3190
|
+
//-- alphabetical by display name - drives the Products filter list order; everything
|
|
3191
|
+
//-- else looks products up by id, so sort order is free to be presentational
|
|
3192
|
+
view.docApp.products = (data.products || []).sort(function(a,b)
|
|
3193
|
+
{
|
|
3194
|
+
return (a.name || "").localeCompare(b.name || "", undefined, {sensitivity:"base"});
|
|
3195
|
+
});
|
|
3196
|
+
|
|
3197
|
+
return fetchJsonFile("_books/library.json").then(function(data){
|
|
3198
|
+
|
|
3199
|
+
// Get docbook library list
|
|
3200
|
+
view.docApp.library = data;
|
|
3201
|
+
|
|
3202
|
+
//-- NOTE: book order is whatever the server returns; there is no client-side sort.
|
|
3203
|
+
|
|
3204
|
+
let startingLibraryProduct = urlGetParam(window.location.search,"product");
|
|
3205
|
+
let startingLibraryMode = urlGetParam(window.location.search,"mode");
|
|
3206
|
+
let selectLayoutMode = getLibraryLayoutByModeParam(startingLibraryMode);
|
|
3207
|
+
organiseLibrary(selectLayoutMode,startingLibraryProduct);
|
|
3208
|
+
|
|
3209
|
+
}).then(function(){
|
|
3210
|
+
|
|
3211
|
+
loadLibraryOrBookFromCurrentUrl()
|
|
3212
|
+
});
|
|
3213
|
+
});
|
|
3214
|
+
}
|