ixbrl-viewer 1.4.39__py3-none-any.whl → 1.4.40__py3-none-any.whl

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.

Potentially problematic release.


This version of ixbrl-viewer might be problematic. Click here for more details.

Files changed (95) hide show
  1. iXBRLViewerPlugin/__init__.py +25 -22
  2. iXBRLViewerPlugin/_version.py +2 -2
  3. iXBRLViewerPlugin/constants.py +86 -1
  4. iXBRLViewerPlugin/featureConfig.py +4 -1
  5. iXBRLViewerPlugin/iXBRLViewer.py +28 -14
  6. iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
  7. iXBRLViewerPlugin/viewer/i18next-parser.config.js +1 -1
  8. iXBRLViewerPlugin/viewer/src/html/fact-details.html +69 -38
  9. iXBRLViewerPlugin/viewer/src/html/footer-logo.html +4 -0
  10. iXBRLViewerPlugin/viewer/src/html/footnote-details.html +1 -1
  11. iXBRLViewerPlugin/viewer/src/html/inspector.html +318 -211
  12. iXBRLViewerPlugin/viewer/src/i18n/cy/balancetypes.json +1 -0
  13. iXBRLViewerPlugin/viewer/src/i18n/cy/currencies.json +13 -0
  14. iXBRLViewerPlugin/viewer/src/i18n/cy/datatypes.json +9 -0
  15. iXBRLViewerPlugin/viewer/src/i18n/cy/labelroles.json +24 -0
  16. iXBRLViewerPlugin/viewer/src/i18n/cy/referenceparts.json +10 -0
  17. iXBRLViewerPlugin/viewer/src/i18n/cy/scale.json +16 -0
  18. iXBRLViewerPlugin/viewer/src/i18n/cy/tooltips.json +17 -0
  19. iXBRLViewerPlugin/viewer/src/i18n/cy/translation.json +179 -0
  20. iXBRLViewerPlugin/viewer/src/i18n/en/balancetypes.json +4 -0
  21. iXBRLViewerPlugin/viewer/src/i18n/en/datatypes.json +10 -0
  22. iXBRLViewerPlugin/viewer/src/i18n/en/labelroles.json +4 -0
  23. iXBRLViewerPlugin/viewer/src/i18n/en/scale.json +16 -0
  24. iXBRLViewerPlugin/viewer/src/i18n/en/tooltips.json +17 -0
  25. iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +50 -25
  26. iXBRLViewerPlugin/viewer/src/i18n/es/balancetypes.json +4 -0
  27. iXBRLViewerPlugin/viewer/src/i18n/es/datatypes.json +10 -0
  28. iXBRLViewerPlugin/viewer/src/i18n/es/labelroles.json +24 -0
  29. iXBRLViewerPlugin/viewer/src/i18n/es/scale.json +16 -0
  30. iXBRLViewerPlugin/viewer/src/i18n/es/tooltips.json +17 -0
  31. iXBRLViewerPlugin/viewer/src/i18n/es/translation.json +66 -40
  32. iXBRLViewerPlugin/viewer/src/icons/dark-mode.svg +4 -0
  33. iXBRLViewerPlugin/viewer/src/img/arelle-dark.svg +179 -0
  34. iXBRLViewerPlugin/viewer/src/img/inline-viewer-dark.svg +59 -0
  35. iXBRLViewerPlugin/viewer/src/js/accordian.js +2 -1
  36. iXBRLViewerPlugin/viewer/src/js/aspect.js +18 -7
  37. iXBRLViewerPlugin/viewer/src/js/balance.js +14 -0
  38. iXBRLViewerPlugin/viewer/src/js/chart.js +10 -6
  39. iXBRLViewerPlugin/viewer/src/js/concept.js +28 -1
  40. iXBRLViewerPlugin/viewer/src/js/concept.test.js +23 -2
  41. iXBRLViewerPlugin/viewer/src/js/datatype.js +20 -0
  42. iXBRLViewerPlugin/viewer/src/js/datatype.test.js +62 -0
  43. iXBRLViewerPlugin/viewer/src/js/dialog.js +3 -1
  44. iXBRLViewerPlugin/viewer/src/js/fact.js +16 -0
  45. iXBRLViewerPlugin/viewer/src/js/fact.test.js +3 -0
  46. iXBRLViewerPlugin/viewer/src/js/index.js +11 -3
  47. iXBRLViewerPlugin/viewer/src/js/inspector.js +498 -120
  48. iXBRLViewerPlugin/viewer/src/js/inspector.test.js +1 -1
  49. iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.js +128 -30
  50. iXBRLViewerPlugin/viewer/src/js/ixbrlviewer.test.js +133 -20
  51. iXBRLViewerPlugin/viewer/src/js/menu.js +21 -3
  52. iXBRLViewerPlugin/viewer/src/js/outline.js +2 -2
  53. iXBRLViewerPlugin/viewer/src/js/report.js +60 -8
  54. iXBRLViewerPlugin/viewer/src/js/report.test.js +51 -5
  55. iXBRLViewerPlugin/viewer/src/js/reportset.js +20 -0
  56. iXBRLViewerPlugin/viewer/src/js/reportset.test.js +3 -3
  57. iXBRLViewerPlugin/viewer/src/js/search.js +23 -2
  58. iXBRLViewerPlugin/viewer/src/js/search.test.js +2 -2
  59. iXBRLViewerPlugin/viewer/src/js/summary.js +14 -0
  60. iXBRLViewerPlugin/viewer/src/js/tableExport.js +2 -1
  61. iXBRLViewerPlugin/viewer/src/js/taxonomynamer.js +34 -0
  62. iXBRLViewerPlugin/viewer/src/js/taxonomynamer.test.js +32 -0
  63. iXBRLViewerPlugin/viewer/src/js/theme.js +36 -0
  64. iXBRLViewerPlugin/viewer/src/js/unit.js +17 -2
  65. iXBRLViewerPlugin/viewer/src/js/util.js +16 -16
  66. iXBRLViewerPlugin/viewer/src/js/viewer.js +13 -7
  67. iXBRLViewerPlugin/viewer/src/less/accordian.less +8 -4
  68. iXBRLViewerPlugin/viewer/src/less/block-list.less +12 -6
  69. iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +2 -2
  70. iXBRLViewerPlugin/viewer/src/less/chart.less +8 -5
  71. iXBRLViewerPlugin/viewer/src/less/colours-dark-mode.less +40 -0
  72. iXBRLViewerPlugin/viewer/src/less/colours.less +28 -21
  73. iXBRLViewerPlugin/viewer/src/less/common.less +1 -1
  74. iXBRLViewerPlugin/viewer/src/less/components.less +3 -3
  75. iXBRLViewerPlugin/viewer/src/less/core.less +2 -0
  76. iXBRLViewerPlugin/viewer/src/less/dialog.less +13 -10
  77. iXBRLViewerPlugin/viewer/src/less/form-controls.less +33 -11
  78. iXBRLViewerPlugin/viewer/src/less/inspector.less +538 -299
  79. iXBRLViewerPlugin/viewer/src/less/loader.less +2 -2
  80. iXBRLViewerPlugin/viewer/src/less/menu.less +33 -15
  81. iXBRLViewerPlugin/viewer/src/less/summary.less +16 -6
  82. iXBRLViewerPlugin/viewer/src/less/tabs.less +5 -5
  83. iXBRLViewerPlugin/viewer/src/less/text-mixins.less +2 -1
  84. iXBRLViewerPlugin/viewer/src/less/validation-report.less +1 -1
  85. iXBRLViewerPlugin/viewer/src/less/viewer.less +30 -18
  86. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/METADATA +33 -5
  87. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/RECORD +95 -66
  88. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/WHEEL +1 -1
  89. tests/puppeteer/framework/page_objects/doc_frame.js +1 -1
  90. tests/puppeteer/tests/fact_properties.test.js +4 -4
  91. tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +69 -28
  92. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/LICENSE +0 -0
  93. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/NOTICE +0 -0
  94. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/entry_points.txt +0 -0
  95. {ixbrl_viewer-1.4.39.dist-info → ixbrl_viewer-1.4.40.dist-info}/top_level.txt +0 -0
@@ -159,7 +159,7 @@ describe("Scales filter options", () => {
159
159
  })
160
160
 
161
161
  test("Scales filter options with only monetary facts", () => {
162
- var insp = new TestInspector();
162
+ const insp = new TestInspector();
163
163
  const reportSet = testReport({
164
164
  ...monetaryFactData,
165
165
  }, ixData);
@@ -5,11 +5,17 @@ import $ from 'jquery'
5
5
  import { ReportSet } from "./reportset.js";
6
6
  import { Viewer, DocumentTooLargeError } from "./viewer.js";
7
7
  import { Inspector } from "./inspector.js";
8
+ import { initializeTheme } from './theme.js';
9
+ import { TaxonomyNamer } from './taxonomynamer.js';
10
+ import {FEATURE_GUIDE_LINK, FEATURE_REVIEW, FEATURE_SUPPORT_LINK, FEATURE_SURVEY_LINK} from "./util";
11
+
12
+ const featureFalsyValues = new Set([undefined, null, '', 'false', false]);
8
13
 
9
14
  export class iXBRLViewer {
10
15
 
11
16
  constructor(options) {
12
- this._features = new Set();
17
+ this._staticFeatures = {};
18
+ this._dynamicFeatures = {};
13
19
  this._plugins = [];
14
20
  this.inspector = new Inspector(this);
15
21
  this.viewer = null;
@@ -76,40 +82,68 @@ export class iXBRLViewer {
76
82
  });
77
83
  }
78
84
 
79
- setFeatures(featureNames, queryString) {
80
- const featureMap = {}
81
- // Enable given features initially
82
- featureNames.forEach(f => {
83
- featureMap[f] = true;
84
- });
85
+ setFeatures(features, queryString) {
86
+ this._staticFeatures = {}
87
+ for (const [key, value] of Object.entries(features)) {
88
+ this._staticFeatures[key] = value;
89
+ }
85
90
 
86
- // Enable/disable features based on query string
87
91
  const urlParams = new URLSearchParams(queryString);
92
+ this._dynamicFeatures = {}
88
93
  urlParams.forEach((value, key) => {
89
- // Do nothing if feature has already been disabled by query
90
- if (featureMap[key] !== false) {
91
- // Disable feature if value is exactly 'false', anything else enables the feature
92
- featureMap[key] = value !== 'false';
94
+ if (value === '') {
95
+ if (this._dynamicFeatures[key] === undefined) {
96
+ value = 'true'
97
+ } else {
98
+ return;
99
+ }
93
100
  }
101
+ this._dynamicFeatures[key] = value;
94
102
  });
103
+ }
95
104
 
96
- // Gather results in _features set
97
- if (this._features.size > 0) {
98
- this._features = new Set();
99
- }
100
- for (const [feature, enabled] of Object.entries(featureMap)) {
101
- if (enabled) {
102
- this._features.add(feature);
103
- }
105
+ getStaticFeatureValue(featureName) {
106
+ return this._staticFeatures[featureName];
107
+ }
108
+
109
+ getFeatureValue(featureName) {
110
+ if (this._dynamicFeatures[featureName]) {
111
+ return this._dynamicFeatures[featureName];
104
112
  }
113
+ return this.getStaticFeatureValue(featureName);
105
114
  }
106
115
 
107
116
  isFeatureEnabled(featureName) {
108
- return this._features.has(featureName);
117
+ return !featureFalsyValues.has(this.getFeatureValue(featureName));
118
+ }
119
+
120
+ isStaticFeatureEnabled(featureName) {
121
+ return !featureFalsyValues.has(this.getStaticFeatureValue(featureName));
109
122
  }
110
123
 
111
124
  isReviewModeEnabled() {
112
- return this.isFeatureEnabled('review');
125
+ return this.isFeatureEnabled(FEATURE_REVIEW);
126
+ }
127
+
128
+ getGuideLinkUrl() {
129
+ if (!this.isStaticFeatureEnabled(FEATURE_GUIDE_LINK)) {
130
+ return null;
131
+ }
132
+ return this.resolveRelativeUrl(this.getStaticFeatureValue(FEATURE_GUIDE_LINK));
133
+ }
134
+
135
+ getSupportLinkUrl() {
136
+ if (!this.isStaticFeatureEnabled(FEATURE_SUPPORT_LINK)) {
137
+ return null;
138
+ }
139
+ return this.resolveRelativeUrl(this.getStaticFeatureValue(FEATURE_SUPPORT_LINK));
140
+ }
141
+
142
+ getSurveyLinkUrl() {
143
+ if (!this.isStaticFeatureEnabled(FEATURE_SURVEY_LINK)) {
144
+ return null;
145
+ }
146
+ return this.resolveRelativeUrl(this.getStaticFeatureValue(FEATURE_SURVEY_LINK));
113
147
  }
114
148
 
115
149
  isViewerEnabled() {
@@ -117,16 +151,34 @@ export class iXBRLViewer {
117
151
  return (urlParams.get('disable-viewer') ?? 'false') === 'false';
118
152
  }
119
153
 
154
+ // Resolves URL relative to the config file
155
+ resolveRelativeUrl(url) {
156
+ if (this.options.configUrl === undefined) {
157
+ return url;
158
+ }
159
+ const resolvedUrl = new URL(url, this.options.configUrl.href);
160
+ return resolvedUrl.href;
161
+ }
162
+
120
163
  _loadInspectorHTML() {
121
164
  /* Insert HTML and CSS styles into body */
122
- $(require('../html/inspector.html')).prependTo('body');
165
+ const footerLogoHtml = this.runtimeConfig.skin?.footerLogoHtml ?? require("../html/footer-logo.html");
166
+ $(require('../html/inspector.html'))
167
+ .prependTo('body')
168
+ .find("#footer-logo").html(footerLogoHtml);
123
169
  const inspector_css = require('../less/inspector.less').toString();
124
170
  $('<style id="ixv-style"></style>')
125
171
  .prop("type", "text/css")
126
172
  .text(inspector_css)
127
173
  .appendTo('head');
174
+ if (this.runtimeConfig.skin?.stylesheetUrl !== undefined) {
175
+ $('<link rel="stylesheet" id="ixv-style-skin" />')
176
+ .attr("href", this.resolveRelativeUrl(this.runtimeConfig.skin.stylesheetUrl))
177
+ .appendTo('head');
178
+ }
179
+ const favIconUrl = this.runtimeConfig.skin?.faviconUrl !== undefined ? this.resolveRelativeUrl(this.runtimeConfig.skin.faviconUrl) : require("../img/favicon.ico");
128
180
  $('<link id="ixv-favicon" type="image/x-icon" rel="shortcut icon" />')
129
- .attr('href', require('../img/favicon.ico'))
181
+ .attr('href', favIconUrl)
130
182
  .appendTo('head');
131
183
 
132
184
  try {
@@ -140,7 +192,7 @@ export class iXBRLViewer {
140
192
  _reparentDocument() {
141
193
  const iframeContainer = $('#ixv #iframe-container');
142
194
 
143
- const iframe = $('<iframe title="iXBRL document view"/>')
195
+ const iframe = $('<iframe title="iXBRL document view" tabindex="0"/>')
144
196
  .data("report-index", 0)
145
197
  .appendTo(iframeContainer)[0];
146
198
 
@@ -160,7 +212,8 @@ export class iXBRLViewer {
160
212
  $('html').attr("lang", "en-US");
161
213
  }
162
214
 
163
- $('head').children().not("script").not("style#ixv-style").not("link#ixv-favicon").appendTo($(iframe).contents().find('head'));
215
+ $('head')
216
+ .children().not("script, style#ixv-style, link#ixv-style-skin, link#ixv-favicon").appendTo($(iframe).contents().find('head'));
164
217
 
165
218
  $('<title>').text(docTitle).appendTo($('head'));
166
219
 
@@ -197,11 +250,41 @@ export class iXBRLViewer {
197
250
  }
198
251
  }
199
252
 
253
+ _loadRuntimeConfig() {
254
+ return new Promise((resolve, reject) => {
255
+ if (this.options.configUrl === undefined) {
256
+ resolve({});
257
+ }
258
+ else {
259
+ fetch(this.options.configUrl)
260
+ .then((resp) => {
261
+ if (resp.status == 404) {
262
+ return Promise.resolve({});
263
+ }
264
+ else if (resp.status != 200) {
265
+ return Promise.reject("Fetch failed: " + resp.status);
266
+ }
267
+ return resp.json();
268
+ })
269
+ .then((data) => {
270
+ resolve(data);
271
+ })
272
+ .catch((err) => {
273
+ console.log(err);
274
+ resolve({});
275
+ });
276
+ }
277
+ });
278
+ }
279
+
200
280
  load() {
201
281
  const iv = this;
202
282
  const inspector = this.inspector;
283
+
284
+ this._loadRuntimeConfig().then((runtimeConfig) => {
285
+ this.runtimeConfig = runtimeConfig;
286
+ initializeTheme();
203
287
 
204
- setTimeout(function () {
205
288
  const stubViewer = $('body').hasClass('ixv-stub-viewer');
206
289
 
207
290
  // If viewer is disabled, but not in stub viewer mode, just abort
@@ -217,9 +300,25 @@ export class iXBRLViewer {
217
300
  // We need to parse JSON first so that we can determine feature enablement before loading begins.
218
301
  const taxonomyData = iv._getTaxonomyData();
219
302
  const parsedTaxonomyData = taxonomyData && JSON.parse(taxonomyData);
220
- iv.setFeatures((parsedTaxonomyData && parsedTaxonomyData["features"]) || [], window.location.search);
303
+ let features = parsedTaxonomyData && parsedTaxonomyData["features"];
304
+ if (!features) {
305
+ features = {};
306
+ }
307
+ // `features` was previously an array of flag values
308
+ // Support this for backwards compatability
309
+ else if (Array.isArray(features)) {
310
+ features = features.reduce((obj, val) => {
311
+ obj[val] = true;
312
+ return obj;
313
+ }, {});
314
+ }
315
+ if (this.runtimeConfig.features !== undefined) {
316
+ features = {...this.runtimeConfig.features, features};
317
+ }
318
+ iv.setFeatures(features, window.location.search);
221
319
 
222
320
  const reportSet = new ReportSet(parsedTaxonomyData);
321
+ reportSet.taxonomyNamer = new TaxonomyNamer(new Map(Object.entries(this.runtimeConfig.taxonomyNames ?? {})));
223
322
 
224
323
  // Viewer disabled in stub viewer mode => redirect to first iXBRL document
225
324
  if (!iv.isViewerEnabled()) {
@@ -239,7 +338,7 @@ export class iXBRLViewer {
239
338
  const ds = reportSet.reportFiles();
240
339
  let hasExternalIframe = false;
241
340
  for (let i = stubViewer ? 0 : 1; i < ds.length; i++) {
242
- const iframe = $("<iframe />").attr("src", ds[i].file).data("report-index", ds[i].index).appendTo("#ixv #iframe-container");
341
+ const iframe = $('<iframe tabindex="0" />').attr("src", ds[i].file).data("report-index", ds[i].index).appendTo("#ixv #iframe-container");
243
342
  iframes = iframes.add(iframe);
244
343
  hasExternalIframe = true;
245
344
  }
@@ -292,7 +391,6 @@ export class iXBRLViewer {
292
391
  $('#ixv .loader').remove();
293
392
 
294
393
  /* Focus on fact specified in URL fragment, if any */
295
- inspector.handleFactDeepLink();
296
394
  if (iv.options.showValidationWarningOnStart) {
297
395
  inspector.showValidationWarning();
298
396
  }
@@ -1,6 +1,12 @@
1
1
  // See COPYRIGHT.md for copyright information
2
2
 
3
3
  import {iXBRLViewer} from "./ixbrlviewer";
4
+ import {
5
+ FEATURE_GUIDE_LINK,
6
+ FEATURE_REVIEW,
7
+ FEATURE_SUPPORT_LINK,
8
+ FEATURE_SURVEY_LINK
9
+ } from "./util";
4
10
  describe("Feature enablement", () => {
5
11
  var viewer = null;
6
12
  beforeAll(() => {
@@ -8,58 +14,99 @@ describe("Feature enablement", () => {
8
14
  });
9
15
 
10
16
  test("Query parameter with no value results in enablement", () => {
11
- viewer.setFeatures([], 'a')
12
- expect(viewer._features).toEqual(new Set(['a']));
17
+ viewer.setFeatures({}, 'a')
18
+ expect(viewer._staticFeatures).toEqual({});
19
+ expect(viewer._dynamicFeatures).toEqual({'a': 'true'});
13
20
  expect(viewer.isFeatureEnabled('a')).toBeTruthy();
14
21
  });
15
22
 
16
23
  test("Excluded from JSON, excluded from query", () => {
17
- viewer.setFeatures([], '')
18
- expect(viewer._features).toEqual(new Set([]));
24
+ viewer.setFeatures({}, '')
25
+ expect(viewer._staticFeatures).toEqual({});
26
+ expect(viewer._dynamicFeatures).toEqual({});
19
27
  expect(viewer.isFeatureEnabled('a')).toBeFalsy();
20
28
  });
21
29
 
22
30
  test("Excluded from JSON, enabled in query", () => {
23
- viewer.setFeatures([], 'a=true')
24
- expect(viewer._features).toEqual(new Set(['a']));
31
+ viewer.setFeatures({}, 'a=true')
32
+ expect(viewer._staticFeatures).toEqual({});
33
+ expect(viewer._dynamicFeatures).toEqual({'a': 'true'});
25
34
  expect(viewer.isFeatureEnabled('a')).toBeTruthy();
26
35
  });
27
36
 
28
37
  test("Excluded from JSON, disabled in query", () => {
29
- viewer.setFeatures([], 'a=false')
30
- expect(viewer._features).toEqual(new Set([]));
38
+ viewer.setFeatures({}, 'a=false')
39
+ expect(viewer._staticFeatures).toEqual({});
40
+ expect(viewer._dynamicFeatures).toEqual({'a': 'false'});
31
41
  expect(viewer.isFeatureEnabled('a')).toBeFalsy();
32
42
  });
33
43
 
34
44
  test("Excluded from JSON, enabled and disabled in query", () => {
35
- viewer.setFeatures([], 'a=true&a=false&a')
36
- expect(viewer._features).toEqual(new Set([]));
45
+ viewer.setFeatures({}, 'a=true&a=false&a')
46
+ expect(viewer._staticFeatures).toEqual({});
47
+ expect(viewer._dynamicFeatures).toEqual({'a': 'false'});
37
48
  expect(viewer.isFeatureEnabled('a')).toBeFalsy();
38
49
  });
39
50
 
40
51
  test("Included in JSON, excluded from query", () => {
41
- viewer.setFeatures(['a'], '')
42
- expect(viewer._features).toEqual(new Set(['a']));
52
+ viewer.setFeatures({'a': true}, '')
53
+ expect(viewer._staticFeatures).toEqual({'a': true});
54
+ expect(viewer._dynamicFeatures).toEqual({});
43
55
  expect(viewer.isFeatureEnabled('a')).toBeTruthy();
44
56
  });
45
57
 
46
58
  test("Included in JSON, enabled in query", () => {
47
- viewer.setFeatures(['a'], 'a=true')
48
- expect(viewer._features).toEqual(new Set(['a']));
59
+ viewer.setFeatures({'a': true}, 'a=true')
60
+ expect(viewer._staticFeatures).toEqual({'a': true});
61
+ expect(viewer._dynamicFeatures).toEqual({'a': 'true'});
49
62
  expect(viewer.isFeatureEnabled('a')).toBeTruthy();
50
63
  });
51
64
 
52
65
  test("Included in JSON, disabled in query", () => {
53
- viewer.setFeatures(['a'], 'a=false')
54
- expect(viewer._features).toEqual(new Set([]));
66
+ viewer.setFeatures({'a': true}, 'a=false')
67
+ expect(viewer._staticFeatures).toEqual({'a': true});
68
+ expect(viewer._dynamicFeatures).toEqual({'a': 'false'});
55
69
  expect(viewer.isFeatureEnabled('a')).toBeFalsy();
56
70
  });
57
71
 
58
72
  test("Included in JSON, enabled and disabled in query", () => {
59
- viewer.setFeatures(['a'], 'a=true&a=false&a')
60
- expect(viewer._features).toEqual(new Set([]));
73
+ viewer.setFeatures({'a': true}, 'a=true&a=false&a')
74
+ expect(viewer._staticFeatures).toEqual({'a': true});
75
+ expect(viewer._dynamicFeatures).toEqual({'a': 'false'});
61
76
  expect(viewer.isFeatureEnabled('a')).toBeFalsy();
62
77
  });
78
+
79
+ test("Value set in JSON, not included in query", () => {
80
+ viewer.setFeatures({'a': '1'}, '')
81
+ expect(viewer._staticFeatures).toEqual({'a': '1'});
82
+ expect(viewer._dynamicFeatures).toEqual({});
83
+ expect(viewer.getFeatureValue('a')).toEqual('1');
84
+ expect(viewer.isFeatureEnabled('a')).toBeTruthy();
85
+ });
86
+
87
+ test("Value not set in JSON, set in query", () => {
88
+ viewer.setFeatures({}, 'a=1')
89
+ expect(viewer._staticFeatures).toEqual({});
90
+ expect(viewer._dynamicFeatures).toEqual({'a': '1'});
91
+ expect(viewer.getFeatureValue('a')).toEqual('1');
92
+ expect(viewer.isFeatureEnabled('a')).toBeTruthy();
93
+ });
94
+
95
+ test("Value set in JSON, overwritten in query", () => {
96
+ viewer.setFeatures({'a': '1'}, 'a=2')
97
+ expect(viewer._staticFeatures).toEqual({'a': '1'});
98
+ expect(viewer._dynamicFeatures).toEqual({'a': '2'});
99
+ expect(viewer.getFeatureValue('a')).toEqual('2');
100
+ expect(viewer.isFeatureEnabled('a')).toBeTruthy();
101
+ });
102
+
103
+ test("Value set in JSON, overwritten in query with blank", () => {
104
+ viewer.setFeatures({'a': '1'}, 'a')
105
+ expect(viewer._staticFeatures).toEqual({'a': '1'});
106
+ expect(viewer._dynamicFeatures).toEqual({'a': 'true'});
107
+ expect(viewer.getFeatureValue('a')).toEqual('true');
108
+ expect(viewer.isFeatureEnabled('a')).toBeTruthy();
109
+ });
63
110
  });
64
111
 
65
112
  describe("Review mode enablement", () => {
@@ -69,12 +116,78 @@ describe("Review mode enablement", () => {
69
116
  });
70
117
 
71
118
  test("Review mode enabled", () => {
72
- viewer.setFeatures(['review'], '')
119
+ viewer.setFeatures({[FEATURE_REVIEW]: true}, '')
73
120
  expect(viewer.isReviewModeEnabled()).toBeTruthy();
74
121
  });
75
122
 
76
123
  test("Review mode disabled", () => {
77
- viewer.setFeatures(['a'], '')
124
+ viewer.setFeatures({'a': true}, '')
78
125
  expect(viewer.isReviewModeEnabled()).toBeFalsy();
79
126
  });
80
127
  });
128
+
129
+ describe("Support link enablement", () => {
130
+ var viewer = null;
131
+ beforeAll(() => {
132
+ viewer = new iXBRLViewer({})
133
+ });
134
+
135
+ test("Support link enabled by JSON", () => {
136
+ viewer.setFeatures({[FEATURE_SUPPORT_LINK]: '/help'}, '')
137
+ expect(viewer.getSupportLinkUrl()).toEqual('/help');
138
+ });
139
+
140
+ test("Support link enabled by query", () => {
141
+ viewer.setFeatures({}, FEATURE_SUPPORT_LINK + '=/help')
142
+ expect(viewer.getSupportLinkUrl()).toEqual(null);
143
+ });
144
+
145
+ test("Support link disabled by query", () => {
146
+ viewer.setFeatures({[FEATURE_SUPPORT_LINK]: '/help'}, FEATURE_SUPPORT_LINK + '=false')
147
+ expect(viewer.getSupportLinkUrl()).toEqual('/help');
148
+ });
149
+ });
150
+
151
+ describe("Survey link enablement", () => {
152
+ var viewer = null;
153
+ beforeAll(() => {
154
+ viewer = new iXBRLViewer({})
155
+ });
156
+
157
+ test("Survey link enabled by JSON", () => {
158
+ viewer.setFeatures({[FEATURE_SURVEY_LINK]: '/survey'}, '')
159
+ expect(viewer.getSurveyLinkUrl()).toEqual('/survey');
160
+ });
161
+
162
+ test("Survey link enabled by query", () => {
163
+ viewer.setFeatures({}, FEATURE_SURVEY_LINK + '=/survey')
164
+ expect(viewer.getSurveyLinkUrl()).toEqual(null);
165
+ });
166
+
167
+ test("Survey link disabled by query", () => {
168
+ viewer.setFeatures({[FEATURE_SURVEY_LINK]: '/survey'}, FEATURE_SURVEY_LINK + '=false')
169
+ expect(viewer.getSurveyLinkUrl()).toEqual('/survey');
170
+ });
171
+ });
172
+
173
+ describe("Guide link enablement", () => {
174
+ var viewer = null;
175
+ beforeAll(() => {
176
+ viewer = new iXBRLViewer({})
177
+ });
178
+
179
+ test("Guide link enabled by JSON", () => {
180
+ viewer.setFeatures({[FEATURE_GUIDE_LINK]: '/guide'}, '')
181
+ expect(viewer.getGuideLinkUrl()).toEqual('/guide');
182
+ });
183
+
184
+ test("Guide link enabled by query", () => {
185
+ viewer.setFeatures({}, FEATURE_GUIDE_LINK + '=/guide')
186
+ expect(viewer.getGuideLinkUrl()).toEqual(null);
187
+ });
188
+
189
+ test("Guide link disabled by query", () => {
190
+ viewer.setFeatures({[FEATURE_GUIDE_LINK]: '/guide'}, FEATURE_GUIDE_LINK + '=false')
191
+ expect(viewer.getGuideLinkUrl()).toEqual('/guide');
192
+ });
193
+ });
@@ -1,6 +1,7 @@
1
1
  // See COPYRIGHT.md for copyright information
2
2
 
3
3
  import $ from 'jquery'
4
+ import i18next from "i18next";
4
5
 
5
6
  export class Menu {
6
7
  constructor(elt, attr) {
@@ -65,14 +66,14 @@ export class Menu {
65
66
  $('<input type="checkbox"></input>')
66
67
  .prop("checked", onByDefault)
67
68
  .change(function () {
68
- callback($(this).prop("checked"));
69
+ callback($(this).prop("checked"), true);
69
70
  menu.close();
70
71
  })
71
72
  )
72
73
  .append($("<span></span>").addClass("checkmark"));
73
74
  this._add(item, after);
74
75
  if (onByDefault) {
75
- callback(true);
76
+ callback(true, false);
76
77
  }
77
78
  }
78
79
 
@@ -91,7 +92,6 @@ export class Menu {
91
92
  .attr({ "name": name, "value": v})
92
93
  .on("change", function () {
93
94
  callback($(this).val())
94
- menu.close();
95
95
  })
96
96
  )
97
97
  .append($("<span></span>").addClass("checkmark"))
@@ -102,4 +102,22 @@ export class Menu {
102
102
  }
103
103
  }
104
104
  }
105
+
106
+ addLabel(name) {
107
+ const item = $('<div></div>')
108
+ .addClass("label")
109
+ .text(name)
110
+ this._add(item);
111
+ }
112
+
113
+ addLink(label, href) {
114
+ const item = $("<a></a>")
115
+ .attr("href", href)
116
+ .attr("target", "_blank")
117
+ .attr("title", label)
118
+ .attr("aria-label", label)
119
+ .addClass("item")
120
+ .text(label);
121
+ this._add(item);
122
+ }
105
123
  }
@@ -171,9 +171,9 @@ export class DocumentOutline {
171
171
  sortedSections() {
172
172
  const sections = Object.keys(this.sections);
173
173
  const re = /\(parenthetical\)\s*$/i;
174
- const filteredSections = sections.filter(s => !re.test(this.report.getRoleLabel(s)));
174
+ const filteredSections = sections.filter(s => !re.test(this.report.getRoleLabelOrURI(s)));
175
175
  return filteredSections
176
- .sort((a, b) => this.report.getRoleLabel(a).localeCompare(this.report.getRoleLabel(b)))
176
+ .sort((a, b) => this.report.getRoleLabelOrURI(a).localeCompare(this.report.getRoleLabelOrURI(b)))
177
177
  .map(elr => ({ report: this.report, fact: this.sections[elr], elr: elr }));
178
178
  }
179
179
  }
@@ -165,7 +165,39 @@ export class XBRLReport {
165
165
  return label;
166
166
  }
167
167
  }
168
- return this.reportSet.roleMap()[rolePrefix];
168
+ return undefined;
169
+ }
170
+
171
+ getRoleLabelOrURI(rolePrefix) {
172
+ return this.getRoleLabel(rolePrefix) ?? this.reportSet.roleMap()[rolePrefix];
173
+ }
174
+
175
+ getLabelRoleLabel(rolePrefix) {
176
+ const roleURI = this.reportSet.roleMap()[rolePrefix];
177
+ if (roleURI === undefined) {
178
+ return undefined;
179
+ }
180
+
181
+ // Built-in label roles don't have a definition in the taxonomy. Do an
182
+ // i18n look-up on the last part of the URI. For "en" the camel-case
183
+ // splitter will do what we want for everything except standard label
184
+ // and documentation label
185
+ const suffix = roleURI.split("/").pop();
186
+ if (roleURI.startsWith("http://www.xbrl.org/2003/role/") && i18next.exists(`labelRoles:${suffix}`)) {
187
+ return i18next.t(`labelRoles:${suffix}`);
188
+ }
189
+
190
+ // Attempt to get a label from the role definition
191
+ const label = this.getRoleLabel(rolePrefix);
192
+ if (label !== undefined) {
193
+ return label;
194
+ }
195
+
196
+ // Fall back on de-camel-casing the last part of the URI
197
+ return suffix
198
+ .replaceAll(/([A-Z][a-z]+)/g, ' $1')
199
+ .trim()
200
+ .replace(/^./, s => s.toUpperCase());
169
201
  }
170
202
 
171
203
  localDocuments() {
@@ -180,7 +212,7 @@ export class XBRLReport {
180
212
  }
181
213
 
182
214
  getScaleLabel(scale, unit) {
183
- let label = i18next.t(`scale.${scale}`, {defaultValue:"noName"});
215
+ let label = i18next.t(`scale:${scale}`, {defaultValue:"noName"});
184
216
  if (unit && unit.isMonetary() && scale === -2) {
185
217
  let measure = unit.value() ?? '';
186
218
  if (measure) {
@@ -199,35 +231,47 @@ export class XBRLReport {
199
231
  }
200
232
 
201
233
  getLabel(c, rolePrefix, showPrefix) {
234
+ return this.getLabelAndLang(c, rolePrefix, showPrefix).label;
235
+ }
236
+
237
+ getLabelAndLang(c, rolePrefix, showPrefix) {
202
238
  rolePrefix = rolePrefix || 'std';
203
239
  const lang = this.reportSet.viewerOptions.language;
204
240
  const concept = this._reportData.concepts[c];
205
241
  if (concept === undefined) {
206
242
  console.log("Attempt to get label for undefined concept: " + c);
207
- return "<no label>";
243
+ return { label: "<no label>" };
208
244
  }
209
245
  const labels = concept.labels[rolePrefix]
210
246
  if (labels === undefined || Object.keys(labels).length == 0) {
211
- return undefined;
247
+ return { label: undefined };
212
248
  }
213
249
  else {
214
250
  let label;
251
+ let actualLang;
215
252
  if (lang && labels[lang]) {
216
253
  label = labels[lang];
254
+ actualLang = lang;
217
255
  }
218
256
  else {
219
257
  // Fall back on English, then any label deterministically.
220
- label = labels["en"] || labels["en-us"] || labels[Object.keys(labels).sort()[0]];
258
+ for (const l of ["en", "en-us", Object.keys(labels).sort()[0]]) {
259
+ if (labels[l] !== undefined) {
260
+ label = labels[l];
261
+ actualLang = l;
262
+ break;
263
+ }
264
+ }
221
265
  }
222
266
  if (label === undefined) {
223
- return undefined;
267
+ return {label: undefined};
224
268
  }
225
269
  let s = '';
226
270
  if (showPrefix && this.reportSet.viewerOptions.showPrefixes) {
227
- s = "(" + this.qname(c).prefix + ") ";
271
+ s = "(" + this.reportSet.taxonomyNamer.fromQName(this.qname(c)).prefix + ") ";
228
272
  }
229
273
  s += label;
230
- return s;
274
+ return { label: s, lang: actualLang };
231
275
  }
232
276
  }
233
277
 
@@ -239,6 +283,14 @@ export class XBRLReport {
239
283
  return label;
240
284
  }
241
285
 
286
+ getLabelOrNameAndLang(c, rolePrefix, showPrefix) {
287
+ const labelLang = this.getLabelAndLang(c, rolePrefix, showPrefix);
288
+ if (labelLang.label === undefined) {
289
+ return { label: c };
290
+ }
291
+ return labelLang;
292
+ }
293
+
242
294
  isCalculationContributor(c) {
243
295
  if (this._calculationContributors === undefined) {
244
296
  if (this._reportData.rels?.calc) {