Repository: mar10/wunderbaum Branch: main Commit: 6cb02df61ccd Files: 305 Total size: 4.9 MB Directory structure: gitextract_w7u9tx_n/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── SECURITY.md │ └── workflows/ │ ├── codeql.yml │ ├── mkdocs.yml │ ├── node.js.yml │ └── stale.yml ├── .gitignore ├── .hintrc ├── .prettierrc ├── .python-version ├── .vscode/ │ ├── launch.json │ └── settings.json ├── .yarnrc.yml ├── CHANGELOG.md ├── Gruntfile.ts ├── LICENSE ├── Pipfile ├── README.md ├── dist/ │ ├── wunderbaum.css │ ├── wunderbaum.d.ts │ ├── wunderbaum.esm.js │ └── wunderbaum.umd.js ├── docs/ │ ├── .nojekyll │ ├── _config.yml │ ├── api/ │ │ ├── .nojekyll │ │ ├── assets/ │ │ │ ├── hierarchy.js │ │ │ ├── highlight.css │ │ │ ├── icons.js │ │ │ ├── main.js │ │ │ ├── navigation.js │ │ │ ├── search.js │ │ │ └── style.css │ │ ├── classes/ │ │ │ ├── util.Deferred.html │ │ │ ├── util.ValidationError.html │ │ │ ├── wb_node.WunderbaumNode.html │ │ │ └── wunderbaum.Wunderbaum.html │ │ ├── enums/ │ │ │ ├── types.ChangeType.html │ │ │ ├── types.NavModeEnum.html │ │ │ ├── types.NodeRegion.html │ │ │ ├── types.NodeStatusType.html │ │ │ └── types.RenderFlag.html │ │ ├── functions/ │ │ │ ├── common.decompressSourceData.html │ │ │ ├── common.makeNodeTitleMatcher.html │ │ │ ├── common.makeNodeTitleStartMatcher.html │ │ │ ├── common.nodeTitleSorter.html │ │ │ ├── util.adaptiveThrottle.html │ │ │ ├── util.assert.html │ │ │ ├── util.debounce.html │ │ │ ├── util.documentReady.html │ │ │ ├── util.documentReadyPromise.html │ │ │ ├── util.each.html │ │ │ ├── util.elemFromHtml.html │ │ │ ├── util.elemFromSelector.html │ │ │ ├── util.error.html │ │ │ ├── util.escapeHtml.html │ │ │ ├── util.escapeRegex.html │ │ │ ├── util.escapeTooltip.html │ │ │ ├── util.eventToString.html │ │ │ ├── util.extend.html │ │ │ ├── util.extractHtmlText.html │ │ │ ├── util.getOption.html │ │ │ ├── util.getValueFromElem.html │ │ │ ├── util.intToBool.html │ │ │ ├── util.isArray.html │ │ │ ├── util.isEmptyObject.html │ │ │ ├── util.isFunction.html │ │ │ ├── util.isPlainObject.html │ │ │ ├── util.murmurHash3.html │ │ │ ├── util.noop.html │ │ │ ├── util.onEvent.html │ │ │ ├── util.overrideMethod.html │ │ │ ├── util.rotate.html │ │ │ ├── util.setElemDisplay.html │ │ │ ├── util.setTimeoutPromise.html │ │ │ ├── util.setValueToElem.html │ │ │ ├── util.sleep.html │ │ │ ├── util.throttle.html │ │ │ ├── util.toBool.html │ │ │ ├── util.toPixel.html │ │ │ ├── util.toSet.html │ │ │ ├── util.toggleCheckbox.html │ │ │ ├── util.type.html │ │ │ └── util.unsafeCast.html │ │ ├── hierarchy.html │ │ ├── index.html │ │ ├── interfaces/ │ │ │ ├── types.AddChildrenOptions.html │ │ │ ├── types.ApplyCommandOptions.html │ │ │ ├── types.ColumnDefinition.html │ │ │ ├── types.ColumnEventInfo.html │ │ │ ├── types.DragEventType.html │ │ │ ├── types.DropEventType.html │ │ │ ├── types.ExpandAllOptions.html │ │ │ ├── types.FilterConnectType.html │ │ │ ├── types.FilterNodesOptions.html │ │ │ ├── types.GetStateOptions.html │ │ │ ├── types.IconMapType.html │ │ │ ├── types.LoadLazyNodesOptions.html │ │ │ ├── types.MakeVisibleOptions.html │ │ │ ├── types.NavigateOptions.html │ │ │ ├── types.NodeTypeDefinition.html │ │ │ ├── types.ReloadOptions.html │ │ │ ├── types.RenderOptions.html │ │ │ ├── types.ResetOrderOptions.html │ │ │ ├── types.ScrollIntoViewOptions.html │ │ │ ├── types.ScrollToOptions.html │ │ │ ├── types.SetActiveOptions.html │ │ │ ├── types.SetColumnOptions.html │ │ │ ├── types.SetExpandedOptions.html │ │ │ ├── types.SetSelectedOptions.html │ │ │ ├── types.SetStateOptions.html │ │ │ ├── types.SetStatusOptions.html │ │ │ ├── types.SortOptions.html │ │ │ ├── types.SourceAjaxType.html │ │ │ ├── types.SourceObjectType.html │ │ │ ├── types.TreeStateDefinition.html │ │ │ ├── types.UpdateOptions.html │ │ │ ├── types.VisitRowsOptions.html │ │ │ ├── types.WbActivateEventType.html │ │ │ ├── types.WbButtonClickEventType.html │ │ │ ├── types.WbChangeEventType.html │ │ │ ├── types.WbClickEventType.html │ │ │ ├── types.WbDeactivateEventType.html │ │ │ ├── types.WbEditApplyEventType.html │ │ │ ├── types.WbEditEditEventType.html │ │ │ ├── types.WbErrorEventType.html │ │ │ ├── types.WbEventInfo.html │ │ │ ├── types.WbExpandEventType.html │ │ │ ├── types.WbFocusEventType.html │ │ │ ├── types.WbIconBadgeEventResultType.html │ │ │ ├── types.WbIconBadgeEventType.html │ │ │ ├── types.WbInitEventType.html │ │ │ ├── types.WbKeydownEventType.html │ │ │ ├── types.WbModifyChildEventType.html │ │ │ ├── types.WbNodeData.html │ │ │ ├── types.WbNodeEventType.html │ │ │ ├── types.WbReceiveEventType.html │ │ │ ├── types.WbRenderEventType.html │ │ │ ├── types.WbSelectEventType.html │ │ │ ├── types.WbTreeEventType.html │ │ │ ├── wb_options.InitWunderbaumOptions.html │ │ │ └── wb_options.WunderbaumOptions.html │ │ ├── modules/ │ │ │ ├── common.html │ │ │ ├── types.html │ │ │ ├── util.html │ │ │ ├── wb_node.html │ │ │ ├── wb_options.html │ │ │ └── wunderbaum.html │ │ ├── modules.html │ │ ├── types/ │ │ │ ├── types.ApplyCommandType.html │ │ │ ├── types.BoolOptionResolver.html │ │ │ ├── types.BoolOrStringOptionResolver.html │ │ │ ├── types.CheckboxOption.html │ │ │ ├── types.ColumnDefinitionList.html │ │ │ ├── types.ColumnEventInfoMap.html │ │ │ ├── types.DeprecationOptions.html │ │ │ ├── types.DndOptionsType.html │ │ │ ├── types.DropEffectAllowedType.html │ │ │ ├── types.DropEffectType.html │ │ │ ├── types.DropRegionType.html │ │ │ ├── types.DropRegionTypeList.html │ │ │ ├── types.DropRegionTypeSet.html │ │ │ ├── types.DynamicBoolOption.html │ │ │ ├── types.DynamicBoolOrStringOption.html │ │ │ ├── types.DynamicCheckboxOption.html │ │ │ ├── types.DynamicIconOption.html │ │ │ ├── types.DynamicStringOption.html │ │ │ ├── types.DynamicTooltipOption.html │ │ │ ├── types.EditOptionsType.html │ │ │ ├── types.FilterModeType.html │ │ │ ├── types.FilterOptionsType.html │ │ │ ├── types.GridOptionsType.html │ │ │ ├── types.IconOption.html │ │ │ ├── types.InsertNodeType.html │ │ │ ├── types.KeynavOptionsType.html │ │ │ ├── types.LoggerOptionsType.html │ │ │ ├── types.MatcherCallback.html │ │ │ ├── types.NavigationType.html │ │ │ ├── types.NodeAnyCallback.html │ │ │ ├── types.NodeFilterCallback.html │ │ │ ├── types.NodeFilterResponse.html │ │ │ ├── types.NodePropertyGetterCallback.html │ │ │ ├── types.NodeSelectCallback.html │ │ │ ├── types.NodeStringCallback.html │ │ │ ├── types.NodeToDictCallback.html │ │ │ ├── types.NodeTypeDefinitionMap.html │ │ │ ├── types.NodeVisitCallback.html │ │ │ ├── types.NodeVisitResponse.html │ │ │ ├── types.SelectModeType.html │ │ │ ├── types.SortByPropertyOptions.html │ │ │ ├── types.SortCallback.html │ │ │ ├── types.SortKeyCallback.html │ │ │ ├── types.SortOrderType.html │ │ │ ├── types.SourceListType.html │ │ │ ├── types.SourceType.html │ │ │ ├── types.TooltipOption.html │ │ │ ├── types.TranslationsType.html │ │ │ ├── types.TristateType.html │ │ │ ├── types.WbCancelableEventResultType.html │ │ │ ├── types.WbIconBadgeCallback.html │ │ │ ├── util.EventCallbackType.html │ │ │ ├── util.FunctionType.html │ │ │ └── util.NotPromise.html │ │ └── variables/ │ │ ├── common.DEFAULT_DEBUGLEVEL.html │ │ ├── common.DEFAULT_MIN_COL_WIDTH.html │ │ ├── common.DEFAULT_ROW_HEIGHT.html │ │ ├── common.ICON_WIDTH.html │ │ ├── common.INPUT_KEYS.html │ │ ├── common.KEY_NODATA.html │ │ ├── common.KEY_TO_COMMAND_MAP.html │ │ ├── common.KEY_TO_NAVIGATION_MAP.html │ │ ├── common.NODE_TYPE_FOLDER.html │ │ ├── common.RENDER_MAX_PREFETCH.html │ │ ├── common.RENDER_MIN_PREFETCH.html │ │ ├── common.RESERVED_TREE_SOURCE_KEYS.html │ │ ├── common.TEST_FILE_PATH.html │ │ ├── common.TEST_HTML.html │ │ ├── common.TITLE_SPAN_PAD_Y.html │ │ ├── common.defaultIconMaps.html │ │ ├── util.MAX_INT.html │ │ ├── util.MOUSE_BUTTONS.html │ │ └── util.isMac.html │ ├── assets/ │ │ ├── favicon/ │ │ │ └── site.webmanifest │ │ ├── json/ │ │ │ ├── ajax-lazy-products.json │ │ │ ├── ajax-tree-products.json │ │ │ ├── ajax-tree-products_t.json │ │ │ ├── fixture_department_1k_3_6_p.json │ │ │ ├── fixture_department_1k_3_6_t_c.json │ │ │ └── tree_department_M_t_c.json │ │ └── skeleton.html │ ├── demo/ │ │ ├── demo-custom.js │ │ ├── demo-editable.js │ │ ├── demo-fixedcol.js │ │ ├── demo-grid.js │ │ ├── demo-large.js │ │ ├── demo-minimal.js │ │ ├── demo-plain.js │ │ ├── demo-readonly.js │ │ ├── demo-select.js │ │ ├── demo-welcome.js │ │ ├── index.html │ │ ├── navigation.js │ │ └── style.css │ ├── googlecc7a2a5f2bb40f68.html │ ├── index.md │ ├── tutorial/ │ │ ├── concepts.md │ │ ├── contribute.md │ │ ├── migrate.md │ │ ├── quick_start.md │ │ ├── tutorial_api.md │ │ ├── tutorial_dnd.md │ │ ├── tutorial_edit.md │ │ ├── tutorial_events.md │ │ ├── tutorial_filter.md │ │ ├── tutorial_grid.md │ │ ├── tutorial_initialize.md │ │ ├── tutorial_keyboard.md │ │ ├── tutorial_render.md │ │ ├── tutorial_select.md │ │ ├── tutorial_source.md │ │ └── tutorial_styling.md │ └── unittest/ │ ├── ajax-simple-sub.json │ ├── ajax-simple.json │ ├── test-core-fixture1.js │ ├── test-core.js │ ├── test-dev.html │ └── test-dist.html ├── eslint.config.mjs ├── mkdocs.yml ├── package.json ├── rollup.config.mjs ├── src/ │ ├── common.ts │ ├── debounce.ts │ ├── deferred.ts │ ├── drag_observer.ts │ ├── types.ts │ ├── util.ts │ ├── wb_ext_dnd.ts │ ├── wb_ext_edit.ts │ ├── wb_ext_filter.ts │ ├── wb_ext_grid.ts │ ├── wb_ext_keynav.ts │ ├── wb_ext_logger.ts │ ├── wb_extension_base.ts │ ├── wb_node.ts │ ├── wb_options.ts │ ├── wunderbaum.scss │ └── wunderbaum.ts ├── test/ │ ├── generator/ │ │ ├── README.txt │ │ ├── __init__.py │ │ ├── generator.py │ │ └── make_fixture.py │ ├── gui_test.ods │ ├── playground.html │ ├── playground.js │ └── unit/ │ ├── ajax-simple-sub.json │ ├── ajax-simple.json │ ├── test-core-fixture1.js │ ├── test-core.js │ ├── test-dev.html │ └── test-dist.html ├── tsconfig.json └── typedoc.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ ; EditorConfig is awesome: http://EditorConfig.org ; top-most EditorConfig file root = true ; Unix-style newlines with a newline ending every file [*] ; end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ; 4 space indentation [*.py] indent_style = space indent_size = 4 ; Tab indentation (no size specified) [*.js] ; indent_style = tab indent_style = space indent_size = 2 ; Indentation override for all JS under lib directory [lib/**.js] indent_style = space indent_size = 2 ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] custom: ['https://www.paypal.com/donate/?hosted_button_id=RA6G29AZRUD44'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: bug, need-triage assignees: "" --- Thank you for your contribution. See [here](https://mar10.github.io/wunderbaum/index.html#/tutorial/contribute) for the contribution guidelines. **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Minimal Reproducible Example** See here for an details and an [HTML template](https://mar10.github.io/wunderbaum/index.html#/tutorial/contribute) or create one at [JS BIN](https://jsbin.com/lecasinava/edit?html,js,output) **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- Thank you for your contribution. See [here](https://mar10.github.io/wunderbaum/index.html#/tutorial/contribute) for the contribution guidelines. **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy ## Supported Versions These project versions are currently being supported with security updates: | Version | Supported | | ------- | ------------------ | | 1.0.x | :white_check_mark: | ## Reporting a Vulnerability Thank you for reporting a security related issue using a private channel instead of opening a public issue! The security team (i.e. me) will try to acknowledge and respond as quick as possible. To report a security issue, please email > security(at)wwwendt.de and, to your best knowledge, please - Include your name and affiliation (if any). - Include the scope of the vulnerability. Let us know who could use this exploit. - Mention the affected versions. - Document steps to identify the vulnerability. It is important that we can reproduce your findings. - Describe how to exploit vulnerability, give us an attack scenario. - If known, describe mitigations for the issue. This project follows a 90 day disclosure timeline. (See also [Vulnerability Disclosure Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#initial-report).) ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "main" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] schedule: - cron: '40 8 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'javascript' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support paths: - src # paths-ignore: # - src/node_modules # - '**/*.test.js' steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/mkdocs.yml ================================================ name: mkdocs on: push: branches: - master - main permissions: contents: write jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure Git Credentials run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - uses: actions/setup-python@v5 with: python-version: 3.x - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - uses: actions/cache@v4 with: key: mkdocs-material-${{ env.cache_id }} path: .cache restore-keys: | mkdocs-material- - run: pip install mkdocs-material - run: mkdocs gh-deploy --force - name: Commit 'docs/unittest' folder to 'gh-pages' run: | git fetch git checkout gh-pages git checkout main -- docs/unittest mv docs/unittest unittest git add unittest git commit -m "Add '/docs/unittest' folder from 'main' branch to /unittest" git push origin gh-pages ================================================ FILE: .github/workflows/node.js.yml ================================================ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs name: Node.js CI on: push: branches: ["main"] pull_request: branches: ["main"] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Use Node.js 20.x uses: actions/setup-node@v4 with: node-version: "20.x" cache: "yarn" - name: Install dependencies run: yarn install # run: yarn install --immutable - run: yarn add grunt-cli - name: Run lint run: yarn lint - name: Run tests run: yarn test # - name: Run build # run: yarn build ================================================ FILE: .github/workflows/stale.yml ================================================ # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. # # You can adjust the behavior by modifying this file. # For more information, see: # https://github.com/actions/stale name: Mark stale issues and pull requests on: schedule: - cron: '26 14 * * *' jobs: stale: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 days-before-close: 14 exempt-all-milestones: true operations-per-run: 5 stale-issue-label: 'no-issue-activity' exempt-issue-labels: 'pinned,security' stale-issue-message: | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. close-issue-reason: 'not_planned' stale-pr-label: 'no-pr-activity' exempt-pr-labels: 'pinned,security' stale-pr-message: | This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. ================================================ FILE: .gitignore ================================================ __pycache__ .DS_Store .pyftpsync-meta.json .venv/ *.log /.settings /archive /build /node_modules /site/ /test/triage docs/tutorial/FT_*.md playwright-report/ test-results/ /test/fixtures test/issue_*.* /.yarn .pnp.cjs .pnp.loader.mjs ================================================ FILE: .hintrc ================================================ { "extends": [ "development" ] } ================================================ FILE: .prettierrc ================================================ # Style guide rationale: # Width 80 is default (and explicitly recommended) by prettier # - 2 space indentation and trailing semicolons seem to be most popular # https://hackernoon.com/what-javascript-code-style-is-the-most-popular-5a3f5bec1f6f # It is also the prettier's default # - Double quotes are default in prettier and mandatory in Black # - Trailing comma produces smaller diffs # BUT: # As a first step, we keep the current whitespace setting: # - use tabs # - tabWitdh 4 printWidth: 80 useTabs: false tabWidth: 2 semi: true singleQuote: false trailingComma: "es5" bracketSpacing: true # because it's prettier's default #requirePragma: true endOfLine: "auto" # don't rewrite all lines with `yarn format` on Windows #overrides: # - files: "*.test.js" # options: # semi: true ================================================ FILE: .python-version ================================================ 3.12 ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Python Debugger: Current File with Arguments", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "args": ["${command:pickArgs}"] }, { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "python.formatting.provider": "black", "files.exclude": { "**/build": true, "**/dist": true, "**/docs/api": true }, "explorer.excludeGitIgnore": false, "editor.formatOnSave": true, "black-formatter.importStrategy": "fromEnvironment", "eslint.useFlatConfig": true } ================================================ FILE: .yarnrc.yml ================================================ nodeLinker: node-modules ================================================ FILE: CHANGELOG.md ================================================ # 1.0.0 / Unreleased First release. # Beta-Changes since v0.2.0 > This section will be removed after the beta phase.
> Note that semantic versioning rules are not strictly followed during this phase. - v0.14.1: Fix checkbox assignment bug in wb_node.ts where the value was not being assigned to this.checkbox. - v0.14.0: Refactor sorting: - Deprecate `sortChildren()` and `sortByProperty()` in favor of `sort()`. - Add `key` callback argument and deprecate `cmp`. - Add `tree.options.sortFoldersFirst` option. - Add `tree.resort()`. - v0.14.0: Add `tree.reload()`. - v0.14.0: Add tree option `autoKeys`. - v0.14.0: Don't open file in browser when dropped in empty area. - v0.14.0: Fix icon when expanding an empty lazy node. - v0.14.0: Fix [#129](https://github.com/mar10/wunderbaum/issues/129) Focus on click. - v0.14.0: Fix [#136)(https://github.com/mar10/wunderbaum/issues/136) Fix behavior of setSelected in "single" selectMode. - v0.14.0: Add `dataTransfer` to `DropEventType`. - v0.14.0: Add `tree.children` and `tree.parent` getters. - v0.14.0: Add `tree.getRefKeys(selected)` and `node.getRefKeys(selected)` methods. - v0.14.0: Support HTML string for `iconMap.expander...` properties. - v0.14.0: Expose `Wunderbaum.iconMaps` to be used as defaults for `iconMap` when passing partial maps. Deprecate `Wunderbaum.iconMap` getter in favor of `iconMaps`. - v0.13.0: Add `tree.saveState()` and `tree.restoreState()` methods (experimental). - v0.13.0: Breadcrumb: - Show node icons - Parts can be clicked to activate parent nodes - `connectTopBreadcrumb` can also be a selector - v0.13.0: Filter: - BREAKING: Filter: changed `filter.connectInput` to `filter.connect: {...}`. - Add support for prev/next-match buttons and an info string. - New mode 'mark' (like 'dim' but does not gray out) - v0.13.0: Add `tree.countUnique()` method. - v0.12.1: Fix flat source format for positional args. - v0.12.0: Add `deep`, `resetLazy`, and `collapseOthers` options to `node.` and `tree.expandAll()`. - v0.12.0: Add `resetLazy` options to `tree.setExpanded(false, ...)`. - v0.12.0: Fix [#107](https://github.com/mar10/wunderbaum/issues/107) Double click on expander is calling .setExpanded() twice. - v0.12.0: Fix [#108](https://github.com/mar10/wunderbaum/issues/108) Exception when clicking a cell in rowmode. - v0.12.0: Fix [#109](https://github.com/mar10/wunderbaum/issues/109) expandAll: 'depth' option ignored when flag is set to false. - v0.11.1: Accept `0`/`1` for known boolean node property values (allowing for more compact JSON sources). - v0.11.1: Fix passing `checkbox: 'radio'`. - v0.11.1: Fix sorting boolean and undefined column values. - v0.11.1: Prevent editing a status node title. - v0.11.1: Improve random data generation in demos. - v0.11.1: Fix drop effect on dragenter. - v0.11.0: BREAKING Renamed `tree.options.resizableColumns` to `columnsResizable`. - v0.11.0: Add support for icon buttons in column headers (menu, sort, filter). - v0.11.0: Add `tree.options.columnsSortable` and `ColumnDefinition.sortable`. - v0.11.0: Add `tree.options.columnsFilerable` and `ColumnDefinition.filterable`. - v0.11.0: Add `tree.options.columnsMenu` and `ColumnDefinition.menu`. - v0.11.0: Add `tree.sortByProperty()`. - v0.11.0: Add `util.rotate()`. - v0.11.0: Migrate documentation from docsify to MkDocs. - v0.10.1: Fix column resizer for Edge. - v0.10.0: Add `tree.options.resizableColumns` and `ColumnDefinition.resizable`. - v0.10.0: Add `tree.resetColumns()`. - v0.9.0: DEPRECATE `tree.filterBranches()`: use `tree.filterNodes(..., {matchBranch: true})` instead. - v0.9.0: Allow to pass a RegEx to filterNodes() - v0.9.0: Add `tree.countMatches()` method. - v0.9.0: Add `node.hasFocus()` method. - v0.9.0: Fix `tree.filter.hideExpanders` option. - v0.9.0: Drop `tree.filter.count` and `hideExpandedCounter` options in favor of `tree.iconBadge` (see tutorial for an example). - v0.8.4: Fix #90: Add tooltip functionality to node titles. - v0.8.3: Fix #84: After editing a node title a javascript error "Cannot read properties of null (reading '\_render')" occurs (Chromium). - v0.8.2: Fix #82: Drag/Drop of nodes does not work any more. - v0.8.1: Distinguish between `.log()` and `.logDebug()`, which now call `console.log()` and `console.debug()` respectively. - v0.8.1: Fix #72: DragEnter event does not provide the source node, only the target. - v0.8.1: Fix #73: Fix typing of `tree.options.dnd.dropEffect` and others. - v0.8.1: Fix #75: After reloading nodes with "resetLazy", wunderbaum is broken. - v0.8.1: Fix #76: Lazyload could be called for the same node multiple times. - v0.8.1: Fix #78: Add JS Bin template for reporting issues. - v0.8.1: Fix #79: Improve logging when drop operation is prevented. - Thanks to @jogibear9988 for the testing, opening, and contributing to most of these issues. - v0.8.0: Add `expand(e)` and `beforeExpand(e)` events. - v0.8.0: Add `tree.findByRefKey()`, `node.getCloneList()`, and `node.isClone()`. - v0.8.0: Add `node.startEditTitle()`. - v0.8.0: Fix #70: resetLazy does not remove sub childrens "key"s, so error occurs when re-adding. - v0.8.0: Allow to `throw new ValidationError()` in `tree.change(e)` event. - v0.8.0: `node.setActive()` allows to focus or edit a cell. - v0.8.0: Rename `tree.isEditing()` to `.isEditingTitle()` and add `tree.isEditing()` to check for title _and_ grid cell editing. - v0.7.0: #68 Always reset 'loading' status. - v0.7.0: #69 prevent iOS browser from opening links on drop. - v0.7.0: BREAKING CHANGE: Changed syntax format for compressed formats: - `_keyMap: {"t": "title", ...}` -> `_keyMap: {"title": "t", ...}`. - `_typeList: [...]` -> `_valueMap: {"type": [...]}`. This allows to compress values of other properties than `type`. - v0.6.0: #61 Avoid race conditions in debounced 'change' event handler. - v0.6.0: #63 Don't overwrite dataTransfer 'text/plain' on drag'n'drop. - v0.6.0: #64 Accept Promise in `tree.load()`. - v0.5.5: Rename `defaultDropMode` -> `suggestedDropMode`; add `suggestedDropEffect` - v0.5.5: Work on drag'n'drop (dropEffects, cross-window, ...) - v0.5.4: Use eslint - v0.5.4: Removed deprecated `setModified()` - v0.5.2: Improved typing and drag'n'drop behavior (@jogibear9988) - v0.5.2: Add `tree.options.adjustHeight` (default: `true`) - v0.5.2: Add `tree.options.scrollIntoViewOnExpandClick` (default: `true`) - v0.5.2: Add `tree.options.dnd.serializeClipboardData` (default: `true`) - v0.5.1: Add EXPERIMENTAL support for `tree.iconBadge` (allows to implement counter badges, ...) - v0.5.0: Add support for Font Awesome icons (`options.iconMap: "fontawesome6"`) - v0.4.0: Add support for hierarchical selection (`selectMode: "hier"`) - v0.4.0: Add support for radio-groups - v0.3.6: Remove import of wunderbaum.scss into TS modules - v0.3.6: Renamed `setModified()` to `update()` - v0.3.6: Removed `node.render()`: use `node.update()` instead - v0.3.6: #26: New option `autoCollapse` and methods `node.collapseSiblings()` - v0.3.5: #23: New `columns` option `headerClasses` - v0.3.4: Add `tree.toDictArray()` - v0.3.4: #21: Use CSS variables to allow dynamic custom themes - v0.3.4: #22: Set box-sizing: border-box - v0.3.4: #23: wb-helper-end is not applied to column header - v0.3.2, v0.3.3: Improve distribution - v0.3.1: #20: Fix sourcemaps - v0.3.1: #19: Fix missing icons in deploymet by inlining - v0.3.1: #19: Make options optional: debugLevel, edit, filter, grid - v0.3.1: Update typescript to 5.x - v0.3.0: new option `tree.options.emptyChildListExpandable` controls if parent nodes with `node.children === []` are still expandable (like macOS Finder). Defaults to false. - v0.3.0: `load()`, `source` and `lazyLoad` now support an extended sytax : ```js source: { url: "path/to/request", params: {}, // key/value pairs converted to URL parameters body: {}, // key/value pairs converted to JSON body (defaults to method POST) options: {}, // passed to `fetch(url, OPTIONS)` } ``` - v0.3.0: Add `node.setIcon()`. - v0.3.0: Add `tree.` and `node.sortChildren()`. - v0.3.0: Removed `tree.updateColumns()`. Use `tree.setModified(ChangeType.colStructure)` instead. ================================================ FILE: Gruntfile.ts ================================================ /* * Build scripts for Wunderbaum */ module.exports = (grunt: any) => { grunt.initConfig({ pkg: grunt.file.readJSON("package.json"), exec: { build: { stdin: true, // Allow interactive console cmd: "npm run build", }, copy_dist: { stdin: true, // Allow interactive console cmd: "rm dist/*.* ; cp build/*.* dist", }, make_docs: { stdin: true, // Allow interactive console cmd: "npm run api_docs", }, make_dist: { stdin: true, // Allow interactive console cmd: "npm run make_dist", }, }, connect: { dev: { options: { port: 8088, base: "./", keepalive: false, // pass on, so subsequent tasks (like watch or qunit) can start }, }, }, qunit: { options: { httpBase: "http://localhost:8088", // timeout: 20000, // "--cookies-file": "misc/cookies.txt", }, dist: ["test/unit/test_dist.html"], // build: ["test/unit/test-build.html"], develop: ["test/unit/test-dev.html"], }, yabs: { release: { common: { // defaults for all tools manifests: ["package.json"], }, // The following tools are run in order: check: { branch: ["main"], canPush: true, clean: true, cmpVersion: "gte", }, run_test: { tasks: ["test_dev"], always: true }, bump: {}, // 'bump' also uses the increment mode `yabs:release:MODE` run_build: { tasks: ["exec:build"], always: true }, // TODO 'always' NYI run_copy_dist: { tasks: ["exec:copy_dist"] }, run_make_docs: { tasks: ["exec:make_docs"] }, run_test_dist: { tasks: ["test_dist"] }, commit: { add: "." }, tag: {}, push: { tags: true, useFollowTags: true }, githubRelease: { repo: "mar10/wunderbaum", draft: false, }, npmPublish: {}, bump_develop: { inc: "prepatch" }, commit_develop: { message: "Bump prerelease ({%= version %}) [ci skip]", }, push_develop: {}, }, }, }); // ---------------------------------------------------------------------------- // Load "grunt*" dependencies for (const key in grunt.file.readJSON("package.json").devDependencies) { if (key !== "grunt" && key.indexOf("grunt") === 0) { grunt.loadNpmTasks(key); } } // Register tasks grunt.registerTask("test_dev", [ "connect:dev", // start server "qunit:develop", ]); grunt.registerTask("test_dist", [ "connect:dev", // start server "qunit:dist", ]); grunt.registerTask("ci", ["test_dev"]); // Called by 'npm test' grunt.registerTask("default", ["test_dev"]); }; ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-2025 Martin Wendt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pipfile ================================================ [[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] # fabulist = "*" "nutree[all]" = ">=0.9" mkdocs = "*" mkdocs-material = "*" [dev-packages] black = "24.8" [requires] python_version = "3.12" [pipenv] # allow_prereleases = true ================================================ FILE: README.md ================================================ # ![](docs/assets/tree_logo_32.png) wunderbaum [![GitHub version](https://img.shields.io/github/v/release/mar10/wunderbaum?display_name=tag&sort=semver)](https://github.com/mar10/wunderbaum/releases/latest) [![Node.js CI](https://github.com/mar10/wunderbaum/actions/workflows/node.js.yml/badge.svg)](https://github.com/mar10/wunderbaum/actions/workflows/node.js.yml) [![npm](https://img.shields.io/npm/dm/wunderbaum.svg)](https://www.npmjs.com/package/wunderbaum) [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/wunderbaum/badge)](https://www.jsdelivr.com/package/npm/wunderbaum) [![Released with: grunt-yabs](https://img.shields.io/badge/released%20with-grunt--yabs-yellowgreen)](https://github.com/mar10/grunt-yabs) [![StackOverflow: wunderbaum](https://img.shields.io/badge/StackOverflow-wunderbaum-blue.svg)](https://stackoverflow.com/questions/tagged/wunderbaum) > A modern tree/treegrid control for the web. Designated successor of [Fancytree](https://github.com/mar10/fancytree).
See the [upgrade guide](https://mar10.github.io/wunderbaum/tutorial/migrate/#what-has-changed) for details. [![Demo](https://mar10.github.io/wunderbaum/assets/teaser_2.png)](https://mar10.github.io/wunderbaum/demo/) - Supports drag and drop, editing, filtering, sorting, and multi-selection. - Written in TypeScript, transpiled to ES6 (esm & umd). - Performant handling of big data structures. - Provide an object oriented API. - Framework agnostic. - Zero dependencies. - Keyboard support. ### Details - [Online Demo](https://mar10.github.io/wunderbaum/demo/) - [Documentation](https://mar10.github.io/wunderbaum/) - [API Reference](https://mar10.github.io/wunderbaum/api/index.html) - [Quick Start](https://mar10.github.io/wunderbaum/#/tutorial/quick_start) ================================================ FILE: dist/wunderbaum.css ================================================ @charset "UTF-8"; /*! * Wunderbaum style sheet (generated from wunderbaum.scss) * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ :root, :host { --wb-font-stack: Helvetica, sans-serif; --wb-error-color: #b5373b; --wb-node-text-color: #56534c; --wb-border-color: #56534c; --wb-bg-highlight-color: #26a0da; --wb-header-color: #dedede; --wb-background-color: #ffffff; --wb-alternate-row-color: #f7f7f7; --wb-alternate-row-color-hover: #f3f3f3; --wb-focus-border-color: #275dc5; --wb-drop-source-color: rgb(211.3703703704, 209.6481481481, 205.6296296296); --wb-drop-target-color: rgb(212.2834645669, 236.2992125984, 247.7165354331); --wb-dim-color: rgb(139.5925925926, 135.037037037, 124.4074074074); --wb-error-background-color: rgb(244.6292372881, 220.8707627119, 221.625); --wb-hover-color: rgb(247.1401574803, 251.5590551181, 253.6598425197); --wb-hover-border-color: rgb(247.1401574803, 251.5590551181, 253.6598425197); --wb-grid-color: #dedede; --wb-active-color: #e5f3fb; --wb-active-cell-color: rgb(125.1417322835, 198.1496062992, 232.8582677165); --wb-active-border-color: #70c0e7; --wb-active-hover-color: #dceff8; --wb-active-hover-border-color: #26a0da; --wb-active-column-color: rgb(247.1401574803, 251.5590551181, 253.6598425197); --wb-active-header-column-color: rgb(196.5, 196.5, 196.5); --wb-active-color-grayscale: #f0f0f0; --wb-active-border-color-grayscale: rgb(171.5, 171.5, 171.5); --wb-active-hover-color-grayscale: #eaeaea; --wb-active-cell-color-grayscale: #b3b3b3; --wb-grid-color-grayscale: #dedede; --wb-filter-dim-color: #dedede; --wb-filter-submatch-color: #868581; --wb-row-outer-height: 22px; --wb-row-inner-height: 20px; --wb-row-padding-y: 1px; --wb-col-padding-x: 2px; --wb-icon-outer-height: 20px; --wb-icon-outer-width: 20px; --wb-icon-height: 16px; --wb-icon-width: 16px; --wb-icon-padding-y: 2px; --wb-icon-padding-x: 2px; --wb-header-height: 22px; } div.wunderbaum * { box-sizing: border-box; } div.wunderbaum { height: 100%; min-height: 4px; background-color: var(--wb-background-color); margin: 0; padding: 0; font-family: var(--wb-font-stack); font-size: 14px; color: var(--wb-node-text-color); border: 2px solid var(--wb-border-color); border-radius: 4px; background-clip: content-box; overflow-x: auto; overflow-y: scroll; } div.wunderbaum:focus, div.wunderbaum:focus-within { border-color: var(--wb-focus-border-color); } div.wunderbaum.wb-disabled { opacity: 0.7; pointer-events: none; } div.wunderbaum div.wb-list-container { position: relative; min-height: 4px; } div.wunderbaum { /* --- FIXED-COLUMN --- */ } div.wunderbaum div.wb-header { position: sticky; top: 0; z-index: 2; -webkit-user-select: none; /* Safari */ user-select: none; } div.wunderbaum div.wb-header, div.wunderbaum div.wb-list-container { overflow: unset; } div.wunderbaum div.wb-row { position: absolute; width: 100%; height: var(--wb-row-outer-height); line-height: var(--wb-row-outer-height); border: 1px solid transparent; } div.wunderbaum { /* Fixed column must be opaque, i.e. have the bg color set. */ } div.wunderbaum.wb-fixed-col span.wb-col:first-of-type { position: sticky; left: 0; z-index: 1; background-color: var(--wb-background-color); } div.wunderbaum.wb-fixed-col div.wb-header span.wb-col:first-of-type { background-color: var(--wb-header-color); } div.wunderbaum.wb-fixed-col div.wb-node-list div.wb-row.wb-active span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col div.wb-node-list div.wb-row.wb-selected span.wb-col:first-of-type { background-color: var(--wb-active-color); } div.wunderbaum.wb-fixed-col div.wb-node-list div.wb-row.wb-active:hover span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col div.wb-node-list div.wb-row.wb-selected:hover span.wb-col:first-of-type { background-color: var(--wb-active-hover-color); } div.wunderbaum.wb-fixed-col div.wb-node-list div.wb-row:hover span.wb-col:first-of-type { background-color: var(--wb-hover-color); } div.wunderbaum.wb-fixed-col:not(:focus-within) div.wb-node-list div.wb-row.wb-active span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus-within) div.wb-node-list div.wb-row.wb-selected span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus) div.wb-node-list div.wb-row.wb-active span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus) div.wb-node-list div.wb-row.wb-selected span.wb-col:first-of-type { background-color: var(--wb-active-color-grayscale); border-color: var(--wb-active-border-color-grayscale); } div.wunderbaum.wb-fixed-col:not(:focus-within) div.wb-node-list div.wb-row.wb-active span.wb-col:first-of-type:hover span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus-within) div.wb-node-list div.wb-row.wb-selected span.wb-col:first-of-type:hover span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus) div.wb-node-list div.wb-row.wb-active span.wb-col:first-of-type:hover span.wb-col:first-of-type, div.wunderbaum.wb-fixed-col:not(:focus) div.wb-node-list div.wb-row.wb-selected span.wb-col:first-of-type:hover span.wb-col:first-of-type { background-color: var(--wb-active-hover-color-grayscale); } div.wunderbaum:not(:focus-within) div.wb-node-list div.wb-row.wb-active, div.wunderbaum:not(:focus-within) div.wb-node-list div.wb-row.wb-selected, div.wunderbaum:not(:focus) div.wb-node-list div.wb-row.wb-active, div.wunderbaum:not(:focus) div.wb-node-list div.wb-row.wb-selected { background-color: var(--wb-active-color-grayscale); border-color: var(--wb-active-border-color-grayscale); } div.wunderbaum:not(:focus-within) div.wb-node-list div.wb-row.wb-active:hover, div.wunderbaum:not(:focus-within) div.wb-node-list div.wb-row.wb-selected:hover, div.wunderbaum:not(:focus) div.wb-node-list div.wb-row.wb-active:hover, div.wunderbaum:not(:focus) div.wb-node-list div.wb-row.wb-selected:hover { background-color: var(--wb-active-hover-color-grayscale); } div.wunderbaum.wb-alternate div.wb-node-list div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected) { background-color: var(--wb-alternate-row-color); } div.wunderbaum.wb-alternate div.wb-node-list div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected):hover { background-color: var(--wb-alternate-row-color-hover); } div.wunderbaum div.wb-node-list div.wb-row:hover { background-color: var(--wb-hover-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-active, div.wunderbaum div.wb-node-list div.wb-row.wb-selected { background-color: var(--wb-active-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-active:hover, div.wunderbaum div.wb-node-list div.wb-row.wb-selected:hover { background-color: var(--wb-active-hover-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-focus:not(.wb-active) { border-style: dotted; border-color: var(--wb-active-border-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-active { border-style: solid; border-color: var(--wb-active-border-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-active:hover { border-color: var(--wb-active-hover-border-color); } div.wunderbaum div.wb-node-list div.wb-row.wb-loading { font-style: italic; } div.wunderbaum div.wb-node-list div.wb-row.wb-busy, div.wunderbaum div.wb-node-list div.wb-row i.wb-busy, div.wunderbaum div.wb-node-list div.wb-row .wb-col.wb-busy { font-style: italic; background: repeating-linear-gradient(45deg, transparent, transparent 3.88px, var(--wb-grid-color) 3.88px, var(--wb-grid-color) 7.78px); animation: wb-busy-animation 2s linear infinite; } div.wunderbaum div.wb-node-list div.wb-row.wb-error, div.wunderbaum div.wb-node-list div.wb-row.wb-status-error { color: var(--wb-error-color); } div.wunderbaum div.wb-header { position: sticky; height: var(--wb-header-height); border-bottom: 1px solid var(--wb-border-color); padding: 0; background-color: var(--wb-header-color); } div.wunderbaum div.wb-header span.wb-col { font-weight: bold; overflow: visible; } div.wunderbaum div.wb-header span.wb-col-title { width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } div.wunderbaum div.wb-header span.wb-col-resizer { position: absolute; top: 0; right: -1px; width: 3px; border: none; border-right: 2px solid var(--wb-border-color); height: 100%; -webkit-user-select: none; user-select: none; } div.wunderbaum div.wb-header span.wb-col-resizer.wb-col-resizer-active { cursor: col-resize; } div.wunderbaum div.wb-header i.wb-col-icon { float: inline-end; padding-left: 2px; } div.wunderbaum div.wb-header i.wb-col-icon:hover { cursor: pointer; color: var(--wb-focus-border-color); } div.wunderbaum span.wb-col { position: absolute; display: inline-block; overflow: hidden; height: var(--wb-row-inner-height); line-height: var(--wb-row-inner-height); padding: 0 var(--wb-col-padding-x); border-right: 1px solid var(--wb-grid-color); white-space: nowrap; } div.wunderbaum span.wb-col:last-of-type { border-right: none; } div.wunderbaum span.wb-node { -webkit-user-select: none; user-select: none; } div.wunderbaum span.wb-node i.wb-checkbox, div.wunderbaum span.wb-node i.wb-expander, div.wunderbaum span.wb-node i.wb-icon, div.wunderbaum span.wb-node i.wb-indent { height: var(--wb-icon-outer-height); width: var(--wb-icon-outer-width); padding: var(--wb-icon-padding-y) var(--wb-icon-padding-x); display: inline-block; } div.wunderbaum span.wb-node i.wb-expander, div.wunderbaum span.wb-node i.wb-icon { background-repeat: no-repeat; background-size: contain; } div.wunderbaum span.wb-node { /* Fix Bootstrap Icon alignment */ } div.wunderbaum span.wb-node i.bi::before { vertical-align: baseline; } div.wunderbaum span.wb-node img.wb-icon { width: var(--wb-icon-width); height: var(--wb-icon-height); padding: var(--wb-icon-padding-y) var(--wb-icon-padding-x); } div.wunderbaum span.wb-node i.wb-indent::before { content: " "; } div.wunderbaum span.wb-node i.wb-expander.wb-spin, div.wunderbaum span.wb-node i.wb-icon.wb-spin { height: unset; width: unset; padding: 0 3px; animation: wb-spin-animation 2s linear infinite; } div.wunderbaum span.wb-node span.wb-title { min-width: 1em; vertical-align: top; overflow-x: hidden; display: inline-block; white-space: nowrap; text-overflow: ellipsis; } div.wunderbaum { /* --- GRID --- */ } div.wunderbaum.wb-grid div.wb-header div.wb-row span.wb-col:hover { background-color: var(--wb-active-header-column-color); } div.wunderbaum.wb-grid.wb-cell-mode div.wb-header div.wb-row span.wb-col.wb-active { background-color: var(--wb-active-hover-color); } div.wunderbaum.wb-grid div.wb-node-list div.wb-row { border-bottom-color: var(--wb-grid-color); } div.wunderbaum.wb-grid div.wb-node-list div.wb-row:hover:not(.wb-active):not(.wb-selected) { background-color: var(--wb-hover-color); } div.wunderbaum.wb-grid div.wb-node-list div.wb-row.wb-active { border-bottom-color: var(--wb-active-border-color); } div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col { border-right: 1px solid var(--wb-grid-color); } div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col input.wb-input-edit, div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=color], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=date], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=datetime], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=datetime-local], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=email], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=month], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=number], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=password], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=search], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=tel], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=text], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=time], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=url], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input[type=week], div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > select { width: 100%; max-height: var(--wb-row-inner-height); border: none; } div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > input:focus, div.wunderbaum.wb-grid div.wb-node-list div.wb-row span.wb-col > select:focus { border: 1px dashed #70c0e7; } div.wunderbaum.wb-grid.wb-cell-mode div.wb-row:not(.wb-colspan).wb-active span.wb-col.wb-active { background-color: var(--wb-active-cell-color-grayscale); } div.wunderbaum.wb-grid.wb-cell-mode:focus-within div.wb-row:not(.wb-colspan):not(.wb-selected) span.wb-col.wb-active, div.wunderbaum.wb-grid.wb-cell-mode:focus div.wb-row:not(.wb-colspan):not(.wb-selected) span.wb-col.wb-active { background-color: var(--wb-active-column-color); } div.wunderbaum.wb-grid.wb-cell-mode:focus-within div.wb-row:not(.wb-colspan):not(.wb-selected).wb-active, div.wunderbaum.wb-grid.wb-cell-mode:focus div.wb-row:not(.wb-colspan):not(.wb-selected).wb-active { background-color: var(--wb-active-column-color); } div.wunderbaum.wb-grid.wb-cell-mode:focus-within div.wb-row:not(.wb-colspan):not(.wb-selected).wb-active span.wb-col.wb-active, div.wunderbaum.wb-grid.wb-cell-mode:focus div.wb-row:not(.wb-colspan):not(.wb-selected).wb-active span.wb-col.wb-active { background-color: var(--wb-active-cell-color); } div.wunderbaum.wb-grid.wb-alternate div.wb-node-list div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected) { background-color: var(--wb-alternate-row-color); } div.wunderbaum.wb-grid.wb-alternate div.wb-node-list div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected):hover { background-color: var(--wb-alternate-row-color-hover); } div.wunderbaum.wb-grid:not(:focus-within) div.wb-node-list div.wb-row, div.wunderbaum.wb-grid:not(:focus) div.wb-node-list div.wb-row { border-bottom-color: var(--wb-grid-color-grayscale); } div.wunderbaum { /* --- FILTER --- */ } div.wunderbaum.wb-ext-filter-dim div.wb-node-list div.wb-row, div.wunderbaum.wb-ext-filter-hide div.wb-node-list div.wb-row { color: var(--wb-filter-dim-color); } div.wunderbaum.wb-ext-filter-dim div.wb-node-list div.wb-row.wb-submatch, div.wunderbaum.wb-ext-filter-hide div.wb-node-list div.wb-row.wb-submatch { color: var(--wb-filter-submatch-color); } div.wunderbaum.wb-ext-filter-dim div.wb-node-list div.wb-row.wb-match, div.wunderbaum.wb-ext-filter-hide div.wb-node-list div.wb-row.wb-match { color: var(--wb-node-text-color); } div.wunderbaum.wb-ext-filter-hide.wb-ext-filter-hide-expanders div.wb-node-list div.wb-row:not(.wb-submatch) i.wb-expander { visibility: hidden; } div.wunderbaum { /* --- DND --- */ } div.wunderbaum div.wb-row.wb-drag-source { opacity: 0.5; } div.wunderbaum div.wb-row.wb-drag-source .wb-node { background-color: var(--wb-drop-source-color); } div.wunderbaum div.wb-row.wb-drop-target { overflow: visible; } div.wunderbaum div.wb-row.wb-drop-target .wb-node { background-color: var(--wb-drop-target-color); overflow: visible; } div.wunderbaum div.wb-row.wb-drop-target .wb-node .wb-icon { position: relative; overflow: visible; } div.wunderbaum div.wb-row.wb-drop-target .wb-node .wb-icon::after { position: absolute; z-index: 1000; content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAMAAABA3o1rAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKUExURe/v9/f39+//7+f35+f/79bW5wgIawwYd97e55Tnpc731rjA2d7350LOY1LWa7Xvvf///wAQcyAze97e773vxnuczgA5pQBCpdb33rXvxu//9whjxgBaxlKU1oOz5ABz3gB73tbn99bW1rXe/wCM9xiU997v/97e3gCc/xil/9bv/wic/+/3/wAAALM9X5QAAAAudFJOU////////////////////////////////////////////////////////////wCCj3NVAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAqUlEQVQoU6WQ2w6CMAxA54agsCHq1HlFBREv/f/fs1tHAoaoiedlbXrWtGXwhV8FNqAXuAi4DwkShmE0cgGIcSwCCgkSkrAxpEonot0DhQxJptFsbnOpdNdgsFh6VtYwyqzTmG+oijDY7hr22E4qY7QybeGQe46nsxP0Wwc3Q1GWl+qKec8MlqKubxX+xzV7tkDuD1+3d+heigT2zGx/hCMUeUj4wL8CwAsW1kqCTugMCwAAAABJRU5ErkJggg==); left: 0; top: calc((22px - var(--wb-icon-height)) / 2); } div.wunderbaum div.wb-row.wb-drop-target.wb-drop-before .wb-node .wb-icon::after, div.wunderbaum div.wb-row.wb-drop-target.wb-drop-after .wb-node .wb-icon::after { content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAMAAACROYkbAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACNUExURe/v9/f39+//7+f35+f/79bW5wgIawwYd97e55Tnpc731rjA2d7350LOY1LWa7Xvvf///wAQcyAze97e773vxgAAAHuczgA5pQBCpdb33rXvxu//9whjxgBaxlKU1oOz5ABz3gB73tbn99bW1rXe/wCM9xiU997v/97e3gCc/xil/9bv/wic/+/3/wAAAParqS4AAAAvdFJOU/////////////////////////////////////////////////////////////8AWqU49wAAAAlwSFlzAAAOwwAADsMBx2+oZAAAALlJREFUOE/FktsSgiAQhglMS8WstKLzQTM77Ps/XguL16I208cFyzB8/LPAYCC/ErARzcCFx23pBgnGfjAxBYhpKDwq3SBB5DeGWCYz0SUDClIkmgeLpV7HMiNDbrbbYbBaWzbaoKTaJiHfQe5oYLA/NBwxTiyVyqTSghYwox4MTmfL5XozgqxjAtODoizv1QPXPXqgKer6WeH9+Iw9XgF5ve15/Q+6/SQSsE+q8yMcocoREgzg3wKAL4vrpBIKREShAAAAAElFTkSuQmCC); left: 0; top: calc((22px - var(--wb-icon-height)) / 2 - 11px); } div.wunderbaum div.wb-row.wb-drop-target.wb-drop-after .wb-node .wb-icon::after { top: calc((22px - var(--wb-icon-height)) / 2 + 11px); } div.wunderbaum { /* --- SPECIAL EFFECTS --- */ /* Colorize indentation levels. */ } div.wunderbaum.wb-rainbow i.wb-expander:nth-child(4n+1), div.wunderbaum.wb-rainbow i.wb-indent:nth-child(4n+1) { background: rgb(255, 255, 201); } div.wunderbaum.wb-rainbow i.wb-expander:nth-child(4n+2), div.wunderbaum.wb-rainbow i.wb-indent:nth-child(4n+2) { background: rgb(218, 255, 218); } div.wunderbaum.wb-rainbow i.wb-expander:nth-child(4n+3), div.wunderbaum.wb-rainbow i.wb-indent:nth-child(4n+3) { background: rgb(255, 217, 254); } div.wunderbaum.wb-rainbow i.wb-expander:nth-child(4n+4), div.wunderbaum.wb-rainbow i.wb-indent:nth-child(4n+4) { background: rgb(204, 250, 250); } div.wunderbaum { /* Fade out expanders, when container is not hovered or active */ } div.wunderbaum.wb-fade-expander i.wb-expander { transition: color 1.5s; color: rgba(86, 83, 76, 0); } div.wunderbaum.wb-fade-expander div.wb-row.wb-loading i.wb-expander, div.wunderbaum.wb-fade-expander:hover i.wb-expander, div.wunderbaum.wb-fade-expander:focus i.wb-expander, div.wunderbaum.wb-fade-expander:focus-within i.wb-expander, div.wunderbaum.wb-fade-expander [class*=wb-statusnode-] i.wb-expander { transition: color 0.6s; color: var(--wb-node-text-color); } div.wunderbaum { /* Skeleton */ } div.wunderbaum div.wb-row.wb-skeleton span.wb-title, div.wunderbaum div.wb-row.wb-skeleton i.wb-icon { animation: wb-skeleton-animation 1s linear infinite alternate; border-radius: 0.25em; color: transparent; opacity: 0.7; } div.wunderbaum { /* Auto-hide checkboxes unless selected or hovered */ } div.wunderbaum.wb-checkbox-auto-hide i.wb-checkbox { visibility: hidden; } div.wunderbaum.wb-checkbox-auto-hide .wb-row:hover i.wb-checkbox, div.wunderbaum.wb-checkbox-auto-hide .wb-row.wb-selected i.wb-checkbox { visibility: unset; } div.wunderbaum.wb-checkbox-auto-hide:focus .wb-row.wb-active i.wb-checkbox, div.wunderbaum.wb-checkbox-auto-hide:focus-within .wb-row.wb-active i.wb-checkbox { visibility: unset; } /* --- TOOL CLASSES --- */ a.wb-breadcrumb { cursor: pointer; text-decoration: none; } .wb-helper-center { text-align: center; } .wb-helper-disabled { color: var(--wb-dim-color); } .wb-helper-hidden { display: none; } .wb-helper-invalid { color: var(--wb-error-color); } .wb-helper-lazy-expander { color: var(--wb-bg-highlight-color); } .wb-helper-link { cursor: pointer; } .wb-no-select { -webkit-user-select: none; user-select: none; } .wb-no-select span.wb-title { -webkit-user-select: contain; user-select: contain; } button.wb-filter-hide { font-weight: bolder; } /* RTL support */ .wb-helper-start, .wb-helper-start > input { text-align: left; } .wb-helper-end, .wb-helper-end > input { text-align: right; } .wb-rtl .wb-helper-start, .wb-rtl .wb-helper-start > input { text-align: right; } .wb-rtl .wb-helper-end, .wb-rtl .wb-helper-end > input { text-align: left; } i.wb-icon { position: relative; } i.wb-icon > span.wb-badge { position: absolute; display: inline-block; top: 0; left: -0.6rem; color: white; background-color: var(--wb-bg-highlight-color); padding: 0.2em 0.3rem 0.1em 0.3rem; font-size: 60%; font-weight: 200; line-height: 1; text-align: center; white-space: nowrap; border-radius: 0.5rem; pointer-events: none; } /* Class 'wb-tristate' is used to mark checkboxes that should toggle like * indeterminate -> checked -> unchecked -> indeterminate ... */ .wb-col input[type=checkbox]:indeterminate { color: var(--wb-dim-color); background-color: red; } .wb-col input:invalid { background-color: var(--wb-error-background-color); } .wb-col.wb-invalid { border: 1px dotted var(--wb-error-color); } @keyframes wb-spin-animation { 0% { transform: rotate(0deg); } to { transform: rotate(1turn); } } @keyframes wb-skeleton-animation { 0% { background-color: hsl(200, 20%, 70%); } 100% { background-color: hsl(200, 20%, 95%); } } @keyframes wb-busy-animation { 0% { background-position: 0 0; } 100% { background-position: 0 22px; } } /*# sourceMappingURL=wunderbaum.css.map */ ================================================ FILE: dist/wunderbaum.d.ts ================================================ declare module "debounce" { /*! * Wunderbaum - debounce.ts * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ type Procedure = (...args: any[]) => any; type DebounceOptions = { /** Specify invoking on the leading edge of the timeout. @default false */ leading?: boolean; /** The maximum time `func` is allowed to be delayed before it's invoked.*/ maxWait?: number; /** Specify invoking on the trailing edge of the timeout. @default true */ trailing?: boolean; }; type ThrottleOptions = { /** Specify invoking on the leading edge of the timeout. @default true */ leading?: boolean; /** Specify invoking on the trailing edge of the timeout. @default true */ trailing?: boolean; }; export interface DebouncedFunction { (this: ThisParameterType, ...args: Parameters): ReturnType; cancel: () => void; flush: () => any; pending: () => boolean; } /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', debounce(calculateLayout, 150)) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })) * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * jQuery(source).on('message', debounced) * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ export function debounce(func: F, wait?: number, options?: DebounceOptions): DebouncedFunction; /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds (or once per browser frame). The throttled function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `throttle` and `debounce`. * * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] * The number of milliseconds to throttle invocations to; if omitted, * `requestAnimationFrame` is used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', throttle(updatePosition, 100)) * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * const throttled = throttle(renewToken, 300000, { 'trailing': false }) * jQuery(element).on('click', throttled) * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel) */ export function throttle(func: F, wait?: number, options?: ThrottleOptions): DebouncedFunction; } declare module "util" { /*! * Wunderbaum - util * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ /** @module util */ import { DebouncedFunction, debounce, throttle } from "debounce"; export { debounce, throttle }; /** Readable names for `MouseEvent.button` */ export const MOUSE_BUTTONS: { [key: number]: string; }; export const MAX_INT = 9007199254740991; /**True if the client is using a macOS platform. */ export const isMac: boolean; export type NotPromise = T extends Promise ? never : T; export type FunctionType = (...args: any[]) => any; export type EventCallbackType = (e: Event) => boolean | void; /** A generic error that can be thrown to indicate a validation error when * handling the `apply` event for a node title or the `change` event for a * grid cell. */ export class ValidationError extends Error { constructor(message: string); } /** * A ES6 Promise, that exposes the resolve()/reject() methods. * * TODO: See [Promise.withResolvers()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers#description) * , a proposed standard, but not yet implemented in any browser. */ export class Deferred { private thens; private catches; private status; private resolvedValue; private rejectedError; constructor(); resolve(value?: any): void; reject(error?: any): void; then(cb: any): void; catch(cb: any): void; promise(): { then: (cb: any) => void; catch: (cb: any) => void; }; } /**Throw an `Error` if `cond` is falsey. */ export function assert(cond: any, msg: string): void; /** Run `callback` when document was loaded. */ export function documentReady(callback: () => void): void; /** Resolve when document was loaded. */ export function documentReadyPromise(): Promise; /** * Iterate over Object properties or array elements. * * @param obj `Object`, `Array` or null * @param callback called for every item. * `this` also contains the item. * Return `false` to stop the iteration. */ export function each(obj: any, callback: (index: number | string, item: any) => void | boolean): any; /** Shortcut for `throw new Error(msg)`. */ export function error(msg: string): void; /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */ export function escapeHtml(s: string): string; /**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */ export function escapeRegex(s: string): string; /** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */ export function escapeTooltip(s: string): string; /** TODO */ export function extractHtmlText(s: string): string; /** * Read the value from an HTML input element. * * If a `` is passed, the first child input is used. * Depending on the target element type, `value` is interpreted accordingly. * For example for a checkbox, a value of true, false, or null is returned if * the element is checked, unchecked, or indeterminate. * For datetime input control a numerical value is assumed, etc. * * Common use case: store the new user input in a `change` event handler: * * ```ts * change: (e) => { * const tree = e.tree; * const node = e.node; * // Read the value from the input control that triggered the change event: * let value = tree.getValueFromElem(e.element); * // and store it to the node model (assuming the column id matches the property name) * node.data[e.info.colId] = value; * }, * ``` * @param elem `` or `` or ` control, and should * *not* be passed to tree navigation handler in cell-edit mode. */ export const INPUT_KEYS: { [key: string]: Array; }; /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */ export const RESERVED_TREE_SOURCE_KEYS: Set; /** Map `KeyEvent.key` to navigation action. */ export const KEY_TO_NAVIGATION_MAP: { [key: string]: NavigationType; }; /** Map `KeyEvent.key` to navigation action. */ export const KEY_TO_COMMAND_MAP: { [key: string]: ApplyCommandType; }; /** Return a callback that returns true if the node title matches the string * or regular expression. * @see {@link WunderbaumNode.findAll} */ export function makeNodeTitleMatcher(match: string | RegExp): MatcherCallback; /** Return a callback that returns true if the node title starts with a string (case-insensitive). */ export function makeNodeTitleStartMatcher(s: string): MatcherCallback; /** Compare two nodes by title (case-insensitive). * @deprecated Use `key` option instead of `cmp` in sort methods. */ export function nodeTitleSorter(a: WunderbaumNode, b: WunderbaumNode): number; /** * Decompresses the source data by * - converting from 'flat' to 'nested' format * - expanding short alias names to long names (if defined in _keyMap) * - resolving value indexes to value strings (if defined in _valueMap) * * @param source - The source object to be decompressed. * @returns void */ export function decompressSourceData(source: SourceObjectType): void; } declare module "deferred" { /*! * Wunderbaum - deferred * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ type PromiseCallbackType = (val: any) => void; type finallyCallbackType = () => void; /** * Implement a ES6 Promise, that exposes a resolve() and reject() method. * * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}. * Example: * ```js * function foo() { * let dfd = new Deferred(), * ... * dfd.resolve('foo') * ... * return dfd.promise(); * } * ``` */ export class Deferred { private _promise; protected _resolve: any; protected _reject: any; constructor(); /** Resolve the Promise. */ resolve(value?: any): void; /** Reject the Promise. */ reject(reason?: any): void; /** Return the native Promise instance.*/ promise(): Promise; /** Call Promise.then on the embedded promise instance.*/ then(cb: PromiseCallbackType): Promise; /** Call Promise.catch on the embedded promise instance.*/ catch(cb: PromiseCallbackType): Promise; /** Call Promise.finally on the embedded promise instance.*/ finally(cb: finallyCallbackType): Promise; } } declare module "wb_node" { import { Wunderbaum } from "wunderbaum"; import { AddChildrenOptions, ApplyCommandOptions, ApplyCommandType, ChangeType, CheckboxOption, ExpandAllOptions, IconOption, InsertNodeType, MakeVisibleOptions, MatcherCallback, NavigateOptions, NavigationType, NodeAnyCallback, NodeStatusType, NodeStringCallback, NodeToDictCallback, NodeVisitCallback, NodeVisitResponse, RenderOptions, ResetOrderOptions, ScrollIntoViewOptions, SetActiveOptions, SetExpandedOptions, SetSelectedOptions, SetStatusOptions, SortByPropertyOptions, SortCallback, SortOptions, SourceType, TooltipOption, TristateType, WbNodeData } from "types"; /** * A single tree node. * * **NOTE:**
* Generally you should not modify properties directly, since this may break * the internal bookkeeping. */ export class WunderbaumNode { static sequence: number; /** Reference to owning tree. */ tree: Wunderbaum; /** Parent node (null for the invisible root node `tree.root`). */ parent: WunderbaumNode; /** Name of the node. * @see Use {@link setTitle} to modify. */ title: string; /** Unique key. Passed with constructor or defaults to `SEQUENCE`. * @see Use {@link setKey} to modify. */ readonly key: string; /** Reference key. Unlike {@link key}, a `refKey` may occur multiple * times within a tree (in this case we have 'clone nodes'). * @see Use {@link setKey} to modify. */ readonly refKey: string | undefined; /** * Array of child nodes (null for leaf nodes). * For lazy nodes, this is `null` or ùndefined` until the children are loaded * and leaf nodes may be `[]` (empty array). * @see {@link hasChildren}, {@link addChildren}, {@link lazy}. */ children: WunderbaumNode[] | null; /** Render a checkbox or radio button @see {@link selected}. */ checkbox?: CheckboxOption; /** If true, this node's children are considerd radio buttons. * @see {@link isRadio}. */ radiogroup?: boolean; /** If true, (in grid mode) no cells are rendered, except for the node title.*/ colspan?: boolean; /** Icon definition. */ icon?: IconOption; /** Lazy loading flag. * @see {@link isLazy}, {@link isLoaded}, {@link isUnloaded}. */ lazy?: boolean; /** Expansion state. * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */ expanded?: boolean; /** Selection state. * @see {@link isSelected}, {@link setSelected}, {@link toggleSelected}. */ selected?: boolean; unselectable?: boolean; /** Node type (used for styling). * @see {@link Wunderbaum.types}. */ type?: string; /** Tooltip definition (`true`: use node's title). */ tooltip?: TooltipOption; /** Icon tooltip definition (`true`: use node's title). */ iconTooltip?: TooltipOption; /** Additional classes added to `div.wb-row`. * @see {@link hasClass}, {@link setClass}. */ classes: Set | null; /** Custom data that was passed to the constructor */ data: any; statusNodeType?: NodeStatusType; _isLoading: boolean; _requestId: number; _errorInfo: any | null; _partsel: boolean; _partload: boolean; /** * > 0 if matched (-1 to keep system nodes visible); * Added and removed by filter code. */ match?: number; subMatchCount?: number; /** @internal */ titleWithHighlight?: string; _filterAutoExpanded?: boolean; _rowIdx: number | undefined; _rowElem: HTMLDivElement | undefined; constructor(tree: Wunderbaum, parent: WunderbaumNode, data: WbNodeData); /** * Return readable string representation for this instance. * @internal */ toString(): string; /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link WunderbaumNode.visit}. * * Example: * ```js * for(const n of node) { * ... * } * ``` */ [Symbol.iterator](): IterableIterator; /** Call event handler if defined in tree.options. * Example: * ```js * node._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type: string, extra?: any): any; /** * Append (or insert) a list of child nodes. * * Tip: pass `{ before: 0 }` to prepend new nodes as first children. * * @returns first child added */ addChildren(nodeData: WbNodeData | WbNodeData[], options?: AddChildrenOptions): WunderbaumNode; /** * Append or prepend a node, or append a child node. * * This a convenience function that calls addChildren() * * @param nodeData node definition * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') * @returns new node */ addNode(nodeData: WbNodeData, mode?: InsertNodeType): WunderbaumNode; /** * Apply a modification (or navigation) operation. * * @see {@link Wunderbaum.applyCommand} */ applyCommand(cmd: ApplyCommandType, options: ApplyCommandOptions): any; /** * Collapse all expanded sibling nodes if any. * (Automatically called when `autoCollapse` is true.) */ collapseSiblings(options?: SetExpandedOptions): any; /** * Add/remove one or more classes to `
`. * * This also maintains `node.classes`, so the class will survive a re-render. * * @param className one or more class names. Multiple classes can be passed * as space-separated string, array of strings, or set of strings. */ setClass(className: string | string[] | Set, flag?: boolean): void; /** Start editing this node's title. */ startEditTitle(): void; /** * Call `setExpanded()` on all descendant nodes. * * @param flag true to expand, false to collapse. * @param options Additional options. * @see {@link Wunderbaum.expandAll} * @see {@link WunderbaumNode.setExpanded} */ expandAll(flag?: boolean, options?: ExpandAllOptions): Promise; /** * Find all descendant nodes that match condition (excluding self). * * If `match` is a string, search for exact node title. * If `match` is a RegExp expression, apply it to node.title, using * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test). * If `match` is a callback, match all nodes for that the callback(node) returns true. * * Returns an empty array if no nodes were found. * * Examples: * ```js * // Match all node titles that match exactly 'Joe': * nodeList = node.findAll("Joe") * // Match all node titles that start with 'Joe' case sensitive: * nodeList = node.findAll(/^Joe/) * // Match all node titles that contain 'oe', case insensitive: * nodeList = node.findAll(/oe/i) * // Match all nodes with `data.price` >= 99: * nodeList = node.findAll((n) => { * return n.data.price >= 99; * }) * ``` */ findAll(match: string | RegExp | MatcherCallback): WunderbaumNode[]; /** Return the direct child with a given key, index or null. */ findDirectChild(ptr: number | string | WunderbaumNode): WunderbaumNode | null; /** * Find first descendant node that matches condition (excluding self) or null. * * @see {@link WunderbaumNode.findAll} for examples. */ findFirst(match: string | RegExp | MatcherCallback): WunderbaumNode | null; /** Find a node relative to self. * * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()} */ findRelatedNode(where: NavigationType, includeHidden?: boolean): any; /** * Iterator version of {@link WunderbaumNode.format}. */ format_iter(name_cb?: NodeStringCallback, connectors?: string[]): IterableIterator; /** * Return a multiline string representation of a node/subnode hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.getActiveNode().format((n)=>n.title)); * ``` * logs * ``` * Books * ├─ Art of War * ╰─ Don Quixote * ``` * @see {@link WunderbaumNode.format_iter} */ format(name_cb?: NodeStringCallback, connectors?: string[]): string; /** Return the `` element with a given index or id. * @returns {WunderbaumNode | null} */ getColElem(colIdx: number | string): HTMLSpanElement; /** * Return all nodes with the same refKey. * * @param includeSelf Include this node itself. * @see {@link Wunderbaum.findByRefKey} */ getCloneList(includeSelf?: boolean): WunderbaumNode[]; /** Return the first child node or null. * @returns {WunderbaumNode | null} */ getFirstChild(): WunderbaumNode; /** Return the last child node or null. * @returns {WunderbaumNode | null} */ getLastChild(): WunderbaumNode; /** Return node depth (starting with 1 for top level nodes). */ getLevel(): number; /** Return the successive node (under the same parent) or null. */ getNextSibling(): WunderbaumNode | null; /** Return the parent node (null for the system root node). */ getParent(): WunderbaumNode | null; /** Return an array of all parent nodes (top-down). * @param includeRoot Include the invisible system root node. * @param includeSelf Include the node itself. */ getParentList(includeRoot?: boolean, includeSelf?: boolean): any[]; /** Return a string representing the hierarchical node path, e.g. "a/b/c". * @param includeSelf * @param part property name or callback * @param separator */ getPath(includeSelf?: boolean, part?: keyof WunderbaumNode | NodeAnyCallback, separator?: string): string; /** Return the preceding node (under the same parent) or null. */ getPrevSibling(): WunderbaumNode | null; /** Return true if node has children. * Return undefined if not sure, i.e. the node is lazy and not yet loaded. */ hasChildren(): boolean; /** Return true if node has className set. */ hasClass(className: string): boolean; /** Return true if node is the currently focused node. @since 0.9.0 */ hasFocus(): boolean; /** Return true if this node is the currently active tree node. */ isActive(): boolean; /** Return true if this node is a direct or indirect parent of `other`. * @see {@link WunderbaumNode.isParentOf} */ isAncestorOf(other: WunderbaumNode): boolean; /** Return true if this node is a **direct** subnode of `other`. * @see {@link WunderbaumNode.isDescendantOf} */ isChildOf(other: WunderbaumNode): boolean; /** Return true if this node's refKey is used by at least one other node. */ isClone(): boolean; /** Return true if this node's title spans all columns, i.e. the node has no * grid cells. */ isColspan(): boolean; /** Return true if this node is a direct or indirect subnode of `other`. * @see {@link WunderbaumNode.isChildOf} */ isDescendantOf(other: WunderbaumNode): boolean; /** Return true if this node has children, i.e. the node is generally expandable. * If `andCollapsed` is set, we also check if this node is collapsed, i.e. * an expand operation is currently possible. */ isExpandable(andCollapsed?: boolean): boolean; /** Return true if _this_ node is currently in edit-title mode. * * See {@link WunderbaumNode.startEditTitle}. */ isEditingTitle(): boolean; /** Return true if this node is currently expanded. */ isExpanded(): boolean; /** Return true if this node is the first node of its parent's children. */ isFirstSibling(): boolean; /** Return true if this node is the last node of its parent's children. */ isLastSibling(): boolean; /** Return true if this node is lazy (even if data was already loaded) */ isLazy(): boolean; /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */ isLoaded(): boolean; /** Return true if node is currently loading, i.e. a GET request is pending. */ isLoading(): boolean; /** Return true if this node is a temporarily generated status node of type 'paging'. */ isPagingNode(): boolean; /** Return true if this node is a **direct** parent of `other`. * @see {@link WunderbaumNode.isAncestorOf} */ isParentOf(other: WunderbaumNode): boolean; /** Return true if this node is partially loaded. @experimental */ isPartload(): boolean; /** Return true if this node is partially selected (tri-state). */ isPartsel(): boolean; /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRadio(): boolean; /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRendered(): boolean; /** Return true if this node is the (invisible) system root node. * @see {@link WunderbaumNode.isTopLevel} */ isRootNode(): boolean; /** Return true if this node is selected, i.e. the checkbox is set. * `undefined` if partly selected (tri-state), false otherwise. */ isSelected(): TristateType; /** Return true if this node is a temporarily generated system node like * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). */ isStatusNode(): boolean; /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */ isTopLevel(): boolean; /** Return true if node is marked lazy but not yet loaded. * For non-lazy nodes always return false. */ isUnloaded(): boolean; /** Return true if all parent nodes are expanded. Note: this does not check * whether the node is scrolled into the visible part of the screen or viewport. */ isVisible(): boolean; protected _loadSourceObject(source: any, level?: number): void; _fetchWithOptions(source: any): Promise; /** Download data from the cloud, then call `.update()`. */ load(source: SourceType): Promise; /** * Load content of a lazy node. * If the node is already loaded, nothing happens. * @param [forceReload=false] If true, reload even if already loaded. */ loadLazy(forceReload?: boolean): Promise; /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4. * @see {@link WunderbaumNode.logDebug} */ log(...args: any[]): void; /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4 * and browser console level includes debug/verbose messages. * @see {@link WunderbaumNode.log} */ logDebug(...args: any[]): void; /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */ logError(...args: any[]): void; /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */ logInfo(...args: any[]): void; /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */ logWarn(...args: any[]): void; /** Expand all parents and optionally scroll into visible area as neccessary. * Promise is resolved, when lazy loading and animations are done. * @param {object} [options] passed to `setExpanded()`. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} */ makeVisible(options?: MakeVisibleOptions): Promise; /** Move this node to targetNode. */ moveTo(targetNode: WunderbaumNode, mode?: InsertNodeType, map?: NodeAnyCallback): void; /** Set focus relative to this node and optionally activate. * * 'left' collapses the node if it is expanded, or move to the parent * otherwise. * 'right' expands the node if it is collapsed, or move to the first * child otherwise. * * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the `event.key` that would normally trigger this move, * e.g. `ArrowLeft` = 'left'. * @param options */ navigate(where: NavigationType | string, options?: NavigateOptions): Promise; /** Delete this node and all descendants. */ remove(): void; /** Remove all descendants of this node. */ removeChildren(): void; /** Remove all HTML markup from the DOM. */ removeMarkup(): void; protected _getRenderInfo(): any; protected _createIcon(parentElem: HTMLElement, replaceChild: HTMLElement | null, showLoading: boolean): HTMLElement | null; /** * Create a whole new `
` element. * @see {@link WunderbaumNode._render} */ protected _render_markup(opts: RenderOptions): void; /** * Render `node.title`, `.icon` into an existing row. * * @see {@link WunderbaumNode._render} */ protected _render_data(opts: RenderOptions): void; /** * Update row classes to reflect active, focuses, etc. * @see {@link WunderbaumNode._render} */ protected _render_status(opts: RenderOptions): void; _render(options?: RenderOptions): void; /** * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad * event is triggered on next expand. */ resetLazy(): void; /** Convert node (or whole branch) into a plain object. * * The result is compatible with node.addChildren(). * * @param recursive include child nodes * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link Wunderbaum.toDictArray}. */ toDict(recursive?: boolean, callback?: NodeToDictCallback): WbNodeData; /** Return an option value that has a default, but may be overridden by a * callback or a node instance attribute. * * Evaluation sequence: * * - If `tree.options.` is a callback that returns something, use that. * - Else if `node.` is defined, use that. * - Else if `tree.types[]` is a value, use that. * - Else if `tree.options.` is a value, use that. * - Else use `defaultValue`. * * @param name name of the option property (on node and tree) * @param defaultValue return this if nothing else matched * {@link Wunderbaum.getOption|Wunderbaum.getOption} */ getOption(name: string, defaultValue?: any): any; /** Make sure that this node is visible in the viewport. * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo} */ scrollIntoView(options?: ScrollIntoViewOptions): Promise; /** * Activate this node, deactivate previous, send events, activate column and * scroll into viewport. */ setActive(flag?: boolean, options?: SetActiveOptions): Promise; /** * Expand or collapse this node. */ setExpanded(flag?: boolean, options?: SetExpandedOptions): Promise; /** * Set keyboard focus here. * @see {@link setActive} */ setFocus(flag?: boolean): void; /** Set a new icon path or class. */ setIcon(icon: string): void; /** Change node's {@link key} and/or {@link refKey}. */ setKey(key: string | null, refKey: string | null): void; /** * Trigger a repaint, typically after a status or data change. * * `change` defaults to 'data', which handles modifcations of title, icon, * and column content. It can be reduced to 'ChangeType.status' if only * active/focus/selected state has changed. * * This method will eventually call {@link WunderbaumNode._render} with * default options, but may be more consistent with the tree's * {@link Wunderbaum.update} API. */ update(change?: ChangeType): void; /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents?: boolean): WunderbaumNode[]; /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected?: boolean): string[]; /** Toggle the check/uncheck state. */ toggleSelected(options?: SetSelectedOptions): TristateType; /** Return true if at least on selectable descendant end-node is unselected. @internal */ _anySelectable(): boolean; protected _changeSelectStatusProps(state: TristateType): boolean; /** * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`. * This includes (de)selecting all descendants. */ fixSelection3AfterClick(opts?: SetSelectedOptions): void; /** * Fix selection status for multi-hier mode. * Only end-nodes are considered to update the descendants branch and parents. * Should be called after this node has loaded new children or after * children have been modified using the API. */ fixSelection3FromEndNodes(opts?: SetSelectedOptions): void; /** Modify the check/uncheck state. */ setSelected(flag?: boolean, options?: SetSelectedOptions): TristateType; /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */ setStatus(status: NodeStatusType, options?: SetStatusOptions): WunderbaumNode | null; /** Rename this node. */ setTitle(title: string): void; /** Set the node tooltip. */ setTooltip(tooltip: TooltipOption): void; /** * Sort child list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp?: SortCallback | null, deep?: boolean): void; /** * Renumber nodes `_nativeIndex`. This is useful to allow to restore the * order after sorting a column. * This method is automatically called after loading new child nodes. * @since 0.11.0 */ resetNativeChildOrder(options?: ResetOrderOptions): void; /** * Convenience method to implement column sorting. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options: SortByPropertyOptions): void; /** * Implement column sorting. * @since 0.14.0 */ sort(options: SortOptions): void; /** * Re-apply current sorting if any (use after lazy load). * Example: * ```js * load: function (e) { * // Whe loading a lazy branch, apply current sort order if any * e.node.resort(); * }, * ``` * @since 0.14.0 */ resort(options?: SortOptions): void; /** * Trigger `modifyChild` event on a parent to signal that a child was modified. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ triggerModifyChild(operation: string, child: WunderbaumNode | null, extra?: any): void; /** * Trigger `modifyChild` event on node.parent(!). * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... * @param {object} [extra] */ triggerModify(operation: string, extra?: any): void; /** * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order). * * Stop iteration, if fn() returns false. Skip current branch, if fn() * returns "skip".
* Return false if iteration was stopped. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * its children only. * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link Wunderbaum.visit}. */ visit(callback: NodeVisitCallback, includeSelf?: boolean): NodeVisitResponse; /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.
* Stop iteration, if callback() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. Return false to stop iteration */ visitParents(callback: (node: WunderbaumNode) => boolean | void, includeSelf?: boolean): boolean; /** * Call fn(node) for all sibling nodes.
* Stop iteration, if fn() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. * Return false to stop iteration. * @param includeSelf include this node in the iteration. */ visitSiblings(callback: (node: WunderbaumNode) => boolean | void, includeSelf?: boolean): boolean; /** * [ext-filter] Return true if this node is matched by current filter (or no filter is active). */ isMatched(): boolean; } } declare module "wb_options" { /*! * Wunderbaum - options * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { ColumnDefinitionList, DndOptionsType, DynamicBoolOption, DynamicBoolOrStringOption, DynamicCheckboxOption, DynamicIconOption, EditOptionsType, FilterOptionsType, IconMapType, NavModeEnum, NodeTypeDefinitionMap, SelectModeType, SourceType, TranslationsType, WbActivateEventType, WbButtonClickEventType, WbCancelableEventResultType, WbChangeEventType, WbClickEventType, WbDeactivateEventType, WbErrorEventType, WbExpandEventType, WbIconBadgeCallback, WbIconBadgeEventResultType, WbInitEventType, WbKeydownEventType, WbNodeEventType, WbReceiveEventType, WbRenderEventType, WbSelectEventType, WbTreeEventType } from "types"; /** * Properties of {@link wunderbaum.Wunderbaum.options}. * * This is similar, but not identical, to the options that can be passed to the * constructor(@see {@link InitWunderbaumOptions}). */ export interface WunderbaumOptions { /** * If true, add a `wb-skeleton` class to all nodes, that will result in a * 'glow' effect. Typically used with initial dummy nodes, while loading the * real data. * @default false. */ skeleton: boolean; /** * Translation map for some system messages. */ strings: TranslationsType; /** * 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose * @default 3 (4 in local debug environment) */ debugLevel: number; /** * Number of levels that are forced to be expanded, and have no expander icon. * E.g. 1 would keep all toplevel nodes expanded. * @default 0 */ minExpandLevel: number; /** * If true, allow to expand parent nodes, even if `node.children` conatains * an empty array (`[]`). This is the the behavior of macOS Finder, for example. * @default false */ emptyChildListExpandable: boolean; /** * Height of a node row div. * @default 22 */ rowHeightPx: number; /** * Icon font definition. May be a string (e.g. "fontawesome6" or "bootstrap") * or a map of `iconName: iconClass` pairs. * Note: the icon font must be loaded separately. * In order to only override some defauöt icons, use this pattern: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` * @default "bootstrap" */ iconMap: string | IconMapType; /** * Collapse siblings when a node is expanded. * @default false */ autoCollapse: boolean; /** * If true, the tree will automatically adjust its height to fit the parent * container. This is useful when the tree is embedded in a container with * a fixed or calculated sized. * If the parent container is unsized (e.g. grows with its content, `height: auto`), * then we can define a `height: ...` or `max_height: ...` style on the parent. * To avoid a recursive resize-loop, it may be helpful to set `overflow: hidden` * on the parent container. * * Set this option to `false` will disable auto-resizing. * * @default: true */ adjustHeight: boolean; /** * HTMLElement or selector that receives the top nodes breadcrumb. * @default undefined */ connectTopBreadcrumb: HTMLElement | string | null; /** * @default NavModeEnum.startRow */ navigationModeOption: NavModeEnum; /** * Show/hide header (default: null) * null: assume false for plain tree and true for grids. * string: use text as header (only for plain trees) * true: display a header (use tree's id as text for plain trees) * false: do not display a header */ header: boolean | string | null; /** * Show a `` element while loading data. * @default false. */ showSpinner: boolean; /** * Generate missing keys by hashing a combination of refKey (or title) and * the parent key. This is useful when the source data does not contain unique * keys but we want stable keys for persisting the active node, selection or * expansion state. Note that this still assumes that the same refKey must not * appear twice in the same parent node. * @default false. */ autoKeys: boolean; /** * If true, render a checkbox before the node tile to allow selection with the * mouse. Pass `"radio"` to render a radio button instead. * @default false. */ checkbox: DynamicCheckboxOption; /** Optional callback to render icons per node. */ icon?: DynamicIconOption; /** Optional callback to render a tooltip for the icon. */ iconTooltip?: DynamicBoolOrStringOption; /** Optional callback to render a tooltip for the node title. * Pass `true` to use the node's `title` property as tooltip. */ tooltip?: DynamicBoolOrStringOption; /** Optional callback to make a node unselectable. */ unselectable?: DynamicBoolOption; /** * @default true */ enabled: boolean; /** * * @default false */ fixedCol: boolean; /** * Default value for ColumnDefinition.filterable option. * @default false * @since 0.11.0 */ columnsFilterable: boolean; /** * Default value for ColumnDefinition.menu option. * @default false * @since 0.11.0 */ columnsMenu: boolean; /** * Default value for ColumnDefinition.resizable option. * @default false * @since 0.10.0 */ columnsResizable?: boolean; /** * Default value for ColumnDefinition.sortable option. * @default false * @since 0.11.0 */ columnsSortable?: boolean; /** * Group nodes with children or of `type: 'folder'` at the top when sorting. * If a function is passed, it is called with the node as argument to determine * whether the node is a folder or not. The function should return `true` for * folders. * and should return `true` for folders. * @default false * @since 0.14.0 */ sortFoldersFirst?: DynamicBoolOption; /** * @default "multi" */ selectMode: SelectModeType; /** * @default true */ quicksearch: boolean; /** * Scroll Node into view on Expand Click * @default true */ scrollIntoViewOnExpandClick: boolean; /** Configuration options for the drag-and-drop extension. */ dnd: DndOptionsType; /** Configuration options for the edit-title extension. */ edit: EditOptionsType; /** Configuration options for the node-filter extension. */ filter: FilterOptionsType; /** * `e.node` was activated. * @category Callback */ activate?: (e: WbActivateEventType) => void; /** * `e.node` is about to be activated. * Return `false` to prevent default handling, i.e. activating the node. * See also `deactivate` event. * @category Callback */ beforeActivate?: (e: WbActivateEventType) => WbCancelableEventResultType; /** * `e.node` is about to be expanded/collapsed. * Return `false` to prevent default handling, i.e. expanding/collapsing the node. * @category Callback */ beforeExpand?: (e: WbExpandEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default handling, i.e. (de)selecting the node. * @category Callback */ beforeSelect?: (e: WbSelectEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default handling, i.e. (de)selecting the node. * @category Callback */ buttonClick?: (e: WbButtonClickEventType) => void; /** * * @category Callback */ change?: (e: WbChangeEventType) => void; /** * * Return `false` to prevent default behavior, e.g. expand/collapse, (de)selection, or activation. * @category Callback */ click?: (e: WbClickEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default behavior, e.g. expand/collapse. * @category Callback */ dblclick?: (e: WbClickEventType) => WbCancelableEventResultType; /** * `e.node` was deactivated. * * Return `false` to prevent default handling, e.g. deactivating the node * and activating the next. * See also `activate` event. * @category Callback */ deactivate?: (e: WbDeactivateEventType) => WbCancelableEventResultType; /** * `e.node` was discarded from the viewport and its HTML markup removed. * @category Callback */ discard?: (e: WbNodeEventType) => void; /** * `e.node` is about to be rendered. We can add a badge to the icon cell here. * @category Callback */ iconBadge?: (e: WbIconBadgeCallback) => WbIconBadgeEventResultType; /** * An error occurred, e.g. during initialization or lazy loading. * @category Callback */ error?: (e: WbErrorEventType) => void; /** * `e.node` was expanded (`e.flag === true`) or collapsed (`e.flag === false`) * @category Callback */ expand?: (e: WbTreeEventType) => void; /** * The tree received or lost focus. * Check `e.flag` for status. * @category Callback */ focus?: (e: WbTreeEventType) => void; /** * Fires when the tree markup was created and the initial source data was loaded. * Typical use cases would be activating a node, setting focus, enabling other * controls on the page, etc.
* Also sent if an error occured during initialization (check `e.error` for status). * @category Callback */ init?: (e: WbInitEventType) => void; /** * Fires when a key was pressed while the tree has focus. * `e.node` is set if a node is currently active. * Return `false` to prevent default navigation. * @category Callback */ keydown?: (e: WbKeydownEventType) => WbCancelableEventResultType; /** * Fires when a node that was marked 'lazy', is expanded for the first time. * Typically we return an endpoint URL or the Promise of a fetch request that * provides a (potentially nested) list of child nodes. * @category Callback */ lazyLoad?: (e: WbNodeEventType) => void; /** * Fires when data was loaded (initial request, reload, or lazy loading), * after the data is applied and rendered. * @category Callback */ load?: (e: WbNodeEventType) => void; /** * @category Callback */ modifyChild?: (e: WbNodeEventType) => void; /** * Fires when data was fetched (initial request, reload, or lazy loading), * but before the data is applied and rendered. * Here we can modify and adjust the received data, for example to convert an * external response to native Wunderbaum syntax. * @category Callback */ receive?: (e: WbReceiveEventType) => void; /** * Fires when a node is about to be displayed. * The default HTML markup is already created, but not yet added to the DOM. * Now we can tweak the markup, create HTML elements in this node's column * cells, etc. * See also `Custom Rendering` for details. * @category Callback */ render?: (e: WbRenderEventType) => void; /** * Same as `render(e)`, but for the status nodes, i.e. `e.node.statusNodeType`. * @category Callback */ renderStatusNode?: (e: WbRenderEventType) => void; /** *`e.node` was selected (`e.flag === true`) or deselected (`e.flag === false`) * @category Callback */ select?: (e: WbNodeEventType) => void; /** * Fires when the viewport content was updated, after scroling, expanding etc. * @category Callback */ update?: (e: WbTreeEventType) => void; } /** * Available options for {@link wunderbaum.Wunderbaum}. * * Options are passed to the constructor as plain object: * * ```js * const tree = new mar10.Wunderbaum({ * id: "demo", * element: document.getElementById("demo-tree"), * source: "url/of/data/request", * ... * }); * ``` * * Event handlers are also passed as callbacks * * ```js * const tree = new mar10.Wunderbaum({ * ... * init: (e) => { * console.log(`Tree ${e.tree} was initialized and loaded.`) * }, * activate: (e) => { * console.log(`Node ${e.node} was activated.`) * }, * ... * }); * ``` * * Most of the properties are optional and have resonable default. * They are then available as {@link Wunderbaum.options} property and can be * changed at runtime.
* Only the `element` option is mandatory. * * Note that some options passed here, are *not* available as {@link Wunderbaum.options}. * They are moved to the `tree` instance instead: * - `tree.element` * - `tree.id` * - `tree.columns` * - `tree.types` * - ... * * Some options are only used during initialization and are not stored in the * tree instance: * - `source` * */ export interface InitWunderbaumOptions extends Partial { /** * The target `div` element (or selector) that shall become a Wunderbaum. */ element: string | HTMLDivElement; /** * The identifier of this tree. Used to reference the instance, especially * when multiple trees are present (e.g. `tree = mar10.Wunderbaum.getTree("demo")`). * * @default `"wb_" + COUNTER`. */ id?: string; /** * A list of maps that define column headers. If this option is set, * Wunderbaum becomes a treegrid control instead of a plain tree. * Column definitions can be passed as tree option, or be part of a `source` * response. * @default `[]` meaning this is a plain tree. */ columns?: ColumnDefinitionList; /** * Define shared attributes for multiple nodes of the same type. * This allows for more compact data models. Type definitions can be passed * as tree option, or be part of a `source` response. * * @default `{}`. */ types?: NodeTypeDefinitionMap; /** * Define the initial tree data. Typically a URL of an endpoint that serves * a JSON formatted structure, but also a callback, Promise, or static data * is allowed. * * @default `[]`. */ source?: SourceType; } } declare module "types" { /*! * Wunderbaum - types * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { WunderbaumNode } from "wb_node"; import { Wunderbaum } from "wunderbaum"; /** A value that can either be true, false, or undefined. */ export type TristateType = boolean | undefined; /** Show/hide checkbox or display a radiobutton icon instead. */ export type CheckboxOption = boolean | "radio"; /** A value that can either be true, false, or undefined. */ export type SortOrderType = "asc" | "desc" | undefined; /** An icon may either be * a string-tag that references an entry in the `iconMap` (e.g. `"folderOpen"`)), * an HTML string that contains a `<` and is used as-is, * an image URL string that contains a `.` or `/` and is rendered as ``, * a class string such as `"bi bi-folder"`, * or a boolean value that indicates if the default icon should be used or hidden. */ export type IconOption = boolean | string; /** Show/hide default tooltip or display a string. */ export type TooltipOption = boolean | string; export interface SourceAjaxType { url: string; params?: any; body?: any; options?: RequestInit; } export type SourceListType = Array; export interface SourceObjectType { _format?: "nested" | "flat"; _version?: number; types?: NodeTypeDefinitionMap; columns?: ColumnDefinitionList; children: SourceListType; _keyMap?: { [key: string]: string; }; _positional?: Array; _valueMap?: { [key: string]: Array; }; } /** Possible initilization for tree nodes. */ export type SourceType = string | SourceListType | SourceAjaxType | SourceObjectType; /** Passed to `find...()` methods. Should return true if node matches. */ export type MatcherCallback = (node: WunderbaumNode) => boolean; /** Used for `tree.iconBadge` event. */ export type WbIconBadgeCallback = (e: WbIconBadgeEventType) => WbIconBadgeEventResultType; /** * Passed to `sort()` methods. Should return -1, 0, or 1. * @deprecated Use SortKeyCallback instead */ export type SortCallback = (a: WunderbaumNode, b: WunderbaumNode) => number; /** Passed to `sort()` methods. Should return a representation that can be compared using `<`. */ export type SortKeyCallback = (node: WunderbaumNode) => string | number | any[]; /** When set as option, called when the value is needed (e.g. `colspan` type definition). */ export type BoolOptionResolver = (node: WunderbaumNode) => boolean; /** When set as option, called when the value is needed (e.g. `icon` type definition). */ export type BoolOrStringOptionResolver = (node: WunderbaumNode) => boolean | string; /** A callback that receives a node instance and returns an arbitrary value type. */ export type NodeAnyCallback = (node: WunderbaumNode) => any; /** A callback that receives a node instance and returns a string value. */ export type NodeStringCallback = (node: WunderbaumNode) => string; /** A callback that receives a node instance and property name returns a value. */ export type NodePropertyGetterCallback = (node: WunderbaumNode, propName: string) => any; /** A callback that receives a node instance and returns an iteration modifier. */ export type NodeVisitCallback = (node: WunderbaumNode) => NodeVisitResponse; /** * Returned by `NodeVisitCallback` to control iteration. * `false` stops iteration, `skip` skips descendants but continues. * All other values continue iteration. */ export type NodeVisitResponse = "skip" | boolean | void; /** * A callback that receives a node-data dictionary and a node instance and * returns an iteration modifier. */ export type NodeToDictCallback = (dict: WbNodeData, node: WunderbaumNode) => NodeVisitResponse; /** * A callback that receives a node instance and may returnsa `false` to prevent * (de)selection. */ export type NodeSelectCallback = (node: WunderbaumNode) => boolean | void; /** @internal */ export type DeprecationOptions = { since?: string; hint?: string; }; /** * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. */ export type DynamicBoolOption = boolean | BoolOptionResolver; export type DynamicStringOption = string | BoolOptionResolver; export type DynamicBoolOrStringOption = boolean | string | BoolOrStringOptionResolver; export type DynamicCheckboxOption = CheckboxOption | BoolOrStringOptionResolver; export type DynamicIconOption = IconOption | BoolOrStringOptionResolver; export type DynamicTooltipOption = TooltipOption | BoolOrStringOptionResolver; /** A plain object (dictionary) that represents a node instance. */ export interface WbNodeData { /** Defines if the `selected` state is displayed as checkbox, radio button, * or hidden. * Defaults to {@link WunderbaumOptions.checkbox}. */ checkbox?: CheckboxOption; /** Optional list of child nodes. * If `children` is an empty array, the node is considered a leaf. * If `lazy` is true and `children is undefined or null, the node, is * considered unloaded. Otherwise, the node is considered a leaf. */ children?: Array; /** Additional classes that are added to `
`. */ classes?: string; /** Only show title in a single, merged column. */ colspan?: boolean; /** Expand this node. */ expanded?: boolean; /** Defaults to standard icons (doc, folder, folderOpen, ...) * from {@link WunderbaumOptions.iconMap}. * Can be overridden by {@link WunderbaumOptions.icon}. */ icon?: IconOption; /** Tooltip for the node icon only. Defaults to {@link WunderbaumOptions.iconTooltip}. */ iconTooltip?: TooltipOption; /** The node's key. Must be unique for the whole tree. Defaults to a sequence number. */ key?: string; /** If true (and children are undefined or null), the node is considered lazy * and {@link WunderbaumOptions.lazyLoad} is called when expanded. */ lazy?: boolean; /** Make child nodes single-select radio buttons. */ radiogroup?: boolean; /** Node's reference key. Unlike {@link WunderbaumNode.key}, this value * may be non-unique. Nodes within the tree that share the same refKey are considered * clones. */ refKey?: string; /** The node's selection status, typically displayed as a checkbox. */ selected?: boolean; /** The node's status, typically displayed as merged single row. * @see {@link Wunderbaum.setStatus} */ statusNodeType?: NodeStatusType; /** The node's title. Will be html escaped to prevent XSS. */ title: string; /** Pass true to set node tooltip to the node's title. Defaults to {@link WunderbaumOptions.tooltip}. */ tooltip?: TooltipOption; /** Inherit shared settings from the matching entry in `InitWunderbaumOptions.types`. */ type?: string; /** Set to `true` to prevent selection. Defaults to {@link WunderbaumOptions.unselectable}. */ unselectable?: boolean; /** @internal */ _treeId?: string; /** Other data is passed to `node.data` and can be accessed via `node.data.NAME` */ [key: string]: unknown; } /** A plain object (dictionary) that defines node icons. */ export interface IconMapType { error: string; loading: string; noData: string; expanderExpanded: string; expanderCollapsed: string; expanderLazy: string; checkChecked: string; checkUnchecked: string; checkUnknown: string; radioChecked: string; radioUnchecked: string; radioUnknown: string; folder: string; folderOpen: string; folderLazy: string; doc: string; colSortable: string; colSortAsc: string; colSortDesc: string; colFilter: string; colFilterActive: string; colMenu: string; [key: string]: string; } /** Retuen value of an event handler that can return `false` to prevent the default action. */ export type WbCancelableEventResultType = false | void; export interface WbTreeEventType { /** Name of the event. */ type: string; /** The affected tree instance. */ tree: Wunderbaum; /** Exposed utility module methods * (see [API docs](https://mar10.github.io/wunderbaum/api/modules/util.html)). */ util: any; /** Originating HTML event if any (e.g. `click`). */ event?: Event; } export interface WbNodeEventType extends WbTreeEventType { /** The affected target node. */ node: WunderbaumNode; /** * Contains the node's type information, i.e. `tree.types[node.type]` if * defined. Set to `{}` otherwise. @see {@link Wunderbaum.types} */ typeInfo: NodeTypeDefinition; } export interface WbActivateEventType extends WbNodeEventType { prevNode: WunderbaumNode; /** The original event. */ event: Event; } export interface WbChangeEventType extends WbNodeEventType { /** Additional information derived from the original change event. */ info: WbEventInfo; /** The embedded element that fired the change event. */ inputElem: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; /** The new value of the embedded element, depending on the input element type. */ inputValue: any; /** Result of `inputElem.checkValidity()`. */ inputValid: boolean; } export interface WbClickEventType extends WbTreeEventType { /** The original event. */ event: MouseEvent; /** The clicked node if any. */ node: WunderbaumNode; /** Additional information derived from the original mouse event. */ info: WbEventInfo; } export interface WbDeactivateEventType extends WbNodeEventType { nextNode: WunderbaumNode; /** The original event. */ event: Event; } export interface WbEditApplyEventType extends WbNodeEventType { /** Additional information derived from the original change event. */ info: WbEventInfo; /** The input element of the node title that fired the change event. */ inputElem: HTMLInputElement; /** The previous node title. */ oldValue: string; /** The new node title. */ newValue: string; /** Result of `inputElem.checkValidity()`. */ inputValid: boolean; } export interface WbEditEditEventType extends WbNodeEventType { /** The input element of the node title that was just created. */ inputElem: HTMLInputElement; } export interface WbErrorEventType extends WbNodeEventType { error: any; } export interface WbExpandEventType extends WbNodeEventType { flag: boolean; } export interface WbFocusEventType extends WbTreeEventType { /** The original event. */ event: FocusEvent; /** True if `focusin`, false if `focusout`. */ flag: boolean; } export interface WbIconBadgeEventType extends WbNodeEventType { iconSpan: HTMLElement; } export interface WbIconBadgeEventResultType { /** Content of the badge `` if any. */ badge: string | number | HTMLSpanElement | null | false; /** Additional class name(s), separate with space. */ badgeClass?: string; /** Tooltip for the badge. */ badgeTooltip?: string; } export interface WbInitEventType extends WbTreeEventType { error?: any; } export interface WbKeydownEventType extends WbTreeEventType { /** The original event. */ event: KeyboardEvent; node: WunderbaumNode; /** Additional information derived from the original keyboard event. */ info: WbEventInfo; } export interface WbModifyChildEventType extends WbNodeEventType { /** Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ operation: string; child: WunderbaumNode; } export interface WbReceiveEventType extends WbNodeEventType { response: any; } export interface WbSelectEventType extends WbNodeEventType { flag: boolean; } export interface WbButtonClickEventType extends WbTreeEventType { info: WbEventInfo; /** The associated command, e.g. 'menu', 'sort', 'filter', ... */ command: string; } export interface WbRenderEventType extends WbNodeEventType { /** * True if the node's markup was not yet created. In this case the render * event should create embedded input controls (in addition to update the * values according to to current node data).
* False if the node's markup was already created. In this case the render * event should only update the values according to to current node data. */ isNew: boolean; /** The node's `` element. */ nodeElem: HTMLSpanElement; /** True if the node only displays the title and is stretched over all remaining columns. */ isColspan: boolean; /** * Array of node's `` elements. * The first element is ``, which contains the * node title and icon (`idx: 0`, id: '*'`). */ allColInfosById: ColumnEventInfoMap; /** * Array of node's `` elements, * *that should be rendered by the event handler*. * In contrast to `allColInfosById`, the node title is not part of this array. * If node.isColspan() is true, this array is empty (`[]`). * This allows to iterate over all relevant in a simple loop: * ``` * for (const col of Object.values(e.renderColInfosById)) { * switch (col.id) { * default: * // Assumption: we named column.id === node.data.NAME * col.elem.textContent = node.data[col.id]; * break; * } * } */ renderColInfosById: ColumnEventInfoMap; } /** * Contains the node's type information, i.e. `tree.types[node.type]` if * defined. * @see {@link Wunderbaum.types} and {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. */ export interface NodeTypeDefinition { /** En/disable checkbox for matching nodes. */ checkbox?: CheckboxOption; /** Optional class names that are added to all `div.wb-row` elements of matching nodes. */ classes?: string; /** Only show title and hide other columns if any. */ colspan?: boolean; /** Default icon for matching nodes. */ icon?: IconOption; /** Default icon tooltip for matching nodes. */ iconTooltip?: TooltipOption; [key: string]: unknown; } export type NodeTypeDefinitionMap = { [type: string]: NodeTypeDefinition; }; /** * Column type definitions. * @see {@link Wunderbaum.columns} */ export interface ColumnDefinition { /** Column ID as defined in `tree.columns` definition ("*" for title column). */ id: string; /** Column header (defaults to id) */ title: string; /** Column header tooltip (optional) */ tooltip?: string; /** Column width or weight. * Either an absolute pixel value (e.g. `"50px"`) or a relative weight (e.g. `1`) * that is used to calculate the width inside the remaining available space. * Default: `"*"`, which is interpreted as `1`. */ width?: string | number; /** Only used for columns with a relative weight. * Default: `4px`. */ minWidth?: string | number; /** Allow user to resize the column. * @default false (or global tree option `columnsSortable`) * @see {@link WunderbaumOptions.columnsResizable}. * @since 0.10.0 */ resizable?: boolean; /** Optional custom column width when user resized by mouse drag. * Default: unset. */ customWidthPx?: number; /** Display a 'filter' button in the column header. Default: false.
* Note: The actual filtering must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsFilterable`) * @since 0.11.0 */ filterable?: boolean; /** . * Default: inactive.
* Note: The actual filtering must be implemented in the `buttonClick()` event. */ filterActive?: boolean; /** Display a 'sort' button in the column header. Default: false.
* Note: The actual sorting must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsSortable`) * @see {@link WunderbaumOptions.columnsSortable}. * @since 0.11.0 */ sortable?: boolean; /** Optional custom column sort orde when user clicked the sort icon. * Default: unset, e.g. not sorted.
* Note: The actual sorting must be implemented in the `buttonClick()` event. * @since 0.11.0 */ sortOrder?: SortOrderType; /** Display a menu icon that may open a context menu for this column. * Note: The actual functionality must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsMenu`) * @see {@link WunderbaumOptions.columnsMenu}. * @since 0.11.0 */ menu?: boolean; /** Optional class names that are added to all `span.wb-col` header AND data * elements of that column. Separate multiple classes with space. */ classes?: string; /** If `headerClasses` is a set, it will be used for the header element only * (unlike `classes`, which is used for body and header cells). * Separate multiple classes with space. */ headerClasses?: string; /** Optional HTML content that is rendered into all `span.wb-col` elements of that column.*/ html?: string; /** @internal */ _weight?: number; /** @internal */ _widthPx?: number; /** @internal */ _ofsPx?: number; [key: string]: unknown; } export type ColumnDefinitionList = Array; /** * Column information (passed to the `render` event). */ export interface ColumnEventInfo { /** Column ID as defined in `tree.columns` definition ("*" for title column). */ id: string; /** Column index (0: leftmost title column). */ idx: number; /** The cell's `` element (null for plain trees). */ elem: HTMLSpanElement | null; /** The value of `tree.columns[]` for the current index. */ info: ColumnDefinition; } export type ColumnEventInfoMap = { [colId: string]: ColumnEventInfo; }; /** * Additional information derived from mouse or keyboard events. * @see {@link Wunderbaum.getEventInfo} */ export interface WbEventInfo { /** The original HTTP Event.*/ event: MouseEvent | KeyboardEvent; /** Canonical descriptive string of the event type including modifiers, * e.g. `Ctrl+Down`. @see {@link util.eventToString} */ canonicalName: string; /** The tree instance. */ tree: Wunderbaum; /** The affected node instance instance if any. */ node: WunderbaumNode | null; /** The affected part of the node span (e.g. title, expander, ...). */ region: NodeRegion; /** The definition of the affected column if any. */ colDef?: ColumnDefinition; /** The index of affected column or -1. */ colIdx: number; /** The column definition ID of affected column if any. */ colId?: string; /** The affected column's span tag if any. */ colElem?: HTMLSpanElement; } export type FilterModeType = null | "mark" | "dim" | "hide"; export type SelectModeType = "single" | "multi" | "hier"; export type NavigationType = "down" | "first" | "firstCol" | "last" | "lastCol" | "left" | "nextMatch" | "pageDown" | "pageUp" | "parent" | "prevMatch" | "right" | "up"; export type ApplyCommandType = NavigationType | "addChild" | "addSibling" | "collapse" | "collapseAll" | "copy" | "cut" | "edit" | "expand" | "expandAll" | "indent" | "moveDown" | "moveUp" | "outdent" | "paste" | "remove" | "rename" | "toggleSelect"; export type NodeFilterResponse = "skip" | "branch" | boolean | void; export type NodeFilterCallback = (node: WunderbaumNode) => NodeFilterResponse; /** * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}. */ export enum ChangeType { /** Re-render the whole viewport, headers, and all rows. */ any = "any", /** A node's title, icon, columns, or status have changed. Update the existing row markup. */ data = "data", /** The `tree.columns` definition has changed beyond simple width adjustments. */ colStructure = "colStructure", /** The viewport/window was resized. Adjust layout attributes for all elements. */ resize = "resize", /** A node's definition has changed beyond status and data. Re-render the whole row's markup. */ row = "row", /** Nodes have been added, removed, etc. Update markup. */ structure = "structure", /** A node's status has changed. Update current row's classes, to reflect active, selected, ... */ status = "status", /** Vertical scroll event. Update the 'top' property of all rows. */ scroll = "scroll" } /** @internal */ export enum RenderFlag { clearMarkup = "clearMarkup", header = "header", redraw = "redraw", scroll = "scroll" } /** Possible values for {@link WunderbaumNode.setStatus}. */ export enum NodeStatusType { ok = "ok", loading = "loading", error = "error", noData = "noData", paging = "paging" } /** Define the subregion of a node, where an event occurred. */ export enum NodeRegion { unknown = "", checkbox = "checkbox", column = "column", expander = "expander", icon = "icon", prefix = "prefix", title = "title" } /** Initial navigation mode and possible transition. */ export enum NavModeEnum { /** Start with row mode, but allow cell-nav mode */ startRow = "startRow", /** Cell-nav mode only */ cell = "cell", /** Start in cell-nav mode, but allow row mode */ startCell = "startCell", /** Row mode only */ row = "row" } /** Translatable strings. */ export type TranslationsType = { /** @default "Loading..." */ loading: string; /** @default "Error" */ loadError: string; /** @default "No data" */ noData: string; /** @default " » " */ breadcrumbDelimiter: string; /** @default "Found ${matches} of ${count}" */ queryResult: string; /** @default "No result" */ noMatch: string; /** @default "${match} of ${matches}" */ matchIndex: string; }; /** Possible values for {@link WunderbaumNode.addChildren}. */ export interface AddChildrenOptions { /** Insert children before this node (or index) * @default undefined or null: append as last child */ before?: WunderbaumNode | number | null; /** * Set `node.expanded = true` according to tree.options.minExpandLevel. * This does *not* load lazy nodes. * @default true */ applyMinExpanLevel?: boolean; /** (@internal Internal use, do not set! ) */ _level?: number; } /** Possible values for {@link Wunderbaum.applyCommand} and {@link WunderbaumNode.applyCommand}. */ export interface ApplyCommandOptions { [key: string]: unknown; } /** Possible values for {@link Wunderbaum.expandAll} and {@link WunderbaumNode.expandAll}. */ export interface ExpandAllOptions { /** Expand and load lazy nodes @default false */ loadLazy?: boolean; /** Unload lazily loaded children if any (if collapsing). @default false */ resetLazy?: boolean; /** Ignore tree's `minExpandLevel` option @default false */ force?: boolean; /** Restrict expand level. * Pass 0 to make only toplevel nodes visible, 1 to expand one level deeper, etc. * @default unset (unlimited) */ depth?: number; /** * Also collapse child nodes beyond the `depth` level. * Otherwise only the `depth` level is collapsed and the expand state of the * descendants is retained. * Only in combination with collapse and `depth`. * Expanding with `deep` option is not supported as recursion depth implied by * the `depth` option. However a `deep` option will be considered if * `collapseOthers` is set. * @default false */ deep?: boolean; /** * Expand up to level=depth and collapse all other branches. * Only in combination with `flag == true`, `depth > 0`. * @default false */ collapseOthers?: boolean; /** Keep active node visible @default true */ keepActiveNodeVisible?: boolean; } /** * Possible option values for {@link Wunderbaum.filterNodes}. * The defaults are inherited from the tree instances ´tree.options.filter` * settings (see also {@link FilterOptionsType}). */ export interface FilterNodesOptions { /** Expand all branches that contain matches while filtered @default false */ autoExpand?: boolean; /** Whether to implicitly match all children of matched nodes @default false */ matchBranch?: boolean; /** Match single characters in order, e.g. 'fb' will match 'FooBar' @default false */ fuzzy?: boolean; /**Hide expanders if all child nodes are hidden by filter @default false */ hideExpanders?: boolean; /** Highlight matches by wrapping inside `` tags. * Does not work for filter callbacks. @default true */ highlight?: boolean; /** Match end nodes only @default false */ leavesOnly?: boolean; /** Grayout unmatched nodes (pass 'hide' to remove instead) @default 'dim' */ mode?: FilterModeType; /** Display a 'no data' status node if result is empty @default true */ noData?: boolean | string; } /** Possible values for {@link Wunderbaum.getState}. */ export interface GetStateOptions { /** Include the active node's key (and expand its parents). @default true */ activeKey?: boolean; /** Include the expanded keys. @default false */ expandedKeys?: boolean; /** Include the selected keys. @default false */ selectedKeys?: boolean; } /** Possible values for {@link Wunderbaum.setState}. */ export interface SetStateOptions { /** Recursively load lazy nodes as needed. @default false */ expandLazy?: boolean; } /** Used by {@link Wunderbaum.getState} and {@link Wunderbaum.setState}. */ export interface TreeStateDefinition { /** List of expanded node's keys. */ expandedKeys: Array | undefined; /** The active node's key if any. */ activeKey: string | null; /** The active column index if any. */ activeColIdx: number | null; /** List of selected node's keys. */ selectedKeys: Array | undefined; } /** Possible values for {@link Wunderbaum.loadLazyNodes} `options` argument. */ export interface LoadLazyNodesOptions { /** Expand node (otherwise load, but keep collapsed). @default true */ expand?: boolean; /** Force reloading even if already loaded. @default false */ force?: boolean; /** Do not send events. @default false */ noEvents?: boolean; } /** Possible values for {@link WunderbaumNode.makeVisible}. */ export interface MakeVisibleOptions { /** Do not animate expand (currently not implemented). @default false */ noAnimation?: boolean; /** Scroll node into visible viewport area if required. @default true */ scrollIntoView?: boolean; /** Do not send events. @default false */ noEvents?: boolean; } /** Possible values for {@link WunderbaumNode.navigate}. */ export interface NavigateOptions { /** Activate the new node (otherwise focus only). @default true */ activate?: boolean; /** Originating event (e.g. KeyboardEvent) if any. */ event?: Event; } /** Possible values for {@link WunderbaumNode._render}. */ export interface RenderOptions { /** Which parts need update? @default ChangeType.data */ change?: ChangeType; /** Where to append a new node. @default 'last' */ after?: any; /** @internal. @default false */ isNew?: boolean; /** @internal. @default false */ preventScroll?: boolean; /** @internal. @default false */ isDataChange?: boolean; /** @internal. @default false */ top?: number; /** @internal. @default true */ resizeCols?: boolean; } /** Possible values for {@link WunderbaumNode.scrollIntoView} `options` argument. */ export interface ScrollIntoViewOptions { /** Do not animate (currently not implemented). @default false */ noAnimation?: boolean; /** Do not send events. @default false */ noEvents?: boolean; /** Keep this node visible at the top in any case. */ topNode?: WunderbaumNode; /** Add N pixel offset at top. */ ofsY?: number; } /** Possible values for {@link Wunderbaum.scrollTo} `options` argument. */ export interface ScrollToOptions extends ScrollIntoViewOptions { /** Which node to scroll into the viewport.*/ node: WunderbaumNode; } /** Possible values for {@link WunderbaumNode.setActive} `options` argument. */ export interface SetActiveOptions { /** Generate (de)activate event, even if node already has this status (@default: false). */ retrigger?: boolean; /** Do not generate (de)activate event (@default: false). */ noEvents?: boolean; /** Call `tree.setFocus()` to acquire keyboard focus (@default: false). */ focusTree?: boolean; /** Optional original event that will be passed to the (de)activate handler. */ event?: Event; /** Also call {@link Wunderbaum.setColumn}. */ colIdx?: number | string; /** * Focus embedded input control of the grid cell if any (requires colIdx >= 0). * If colIdx is 0 or '*', the node title is put into edit mode. * Implies `focusTree: true`, requires `colIdx`. */ edit?: boolean; } /** Possible values for {@link Wunderbaum.setColumn} `options` argument. */ export interface SetColumnOptions { /** * Focus embedded input control of the grid cell if any . * If colIdx is 0 or '*', the node title is put into edit mode. * @default false */ edit?: boolean; /** Horizontically scroll into view. @default: true */ scrollIntoView?: boolean; } /** Possible values for {@link WunderbaumNode.setExpanded} `options` argument. */ export interface SetExpandedOptions { /** Ignore {@link WunderbaumOptions}.minExpandLevel. @default false */ force?: boolean; /** Immediately update viewport (async otherwise). @default false */ immediate?: boolean; /** Do not animate expand (currently not implemented). @default false */ noAnimation?: boolean; /** Do not send events. @default false */ noEvents?: boolean; /** Unload lazily loaded children if any (if collapsing). @default false */ resetLazy?: boolean; /** Scroll up to bring expanded nodes into viewport. @default false */ scrollIntoView?: boolean; } /** Possible values for {@link WunderbaumNode.update} `options` argument. */ export interface UpdateOptions { /** Force immediate redraw instead of throttled/async mode. @default false */ immediate?: boolean; } /** Possible values for {@link WunderbaumNode.setSelected} `options` argument. */ export interface SetSelectedOptions { /** Ignore restrictions, e.g. (`unselectable`). @default false */ force?: boolean; /** Do not send `beforeSelect` or `select` events. @default false */ noEvents?: boolean; /** Apply to all descendant nodes (only for `selectMode: 'multi'`). @default false */ propagateDown?: boolean; /** Called for every node. May return false to prevent action. @default null */ callback?: NodeSelectCallback; } /** Possible values for {@link WunderbaumNode.setStatus} `options` argument. */ export interface SetStatusOptions { /** Displayed as status node title. */ message?: string; /** Used as tooltip. */ details?: string; } /** * Possible values for {@link Wunderbaum.reload} `options` argument. */ export interface ReloadOptions { /** Load this source instead. @default initial source (if loaded via ajax) */ source?: SourceType; /** Reactivate currently active node if any. @default true */ reactivate?: boolean; } /** * Possible values for {@link WunderbaumNode.resetNativeChildOrder} `options` argument. */ export interface ResetOrderOptions { /** Sort descendants recursively. @default true */ recursive?: boolean; /** The name of the node property that will be renumbered. * @default `_nativeIndex`. */ propName?: string; } /** * Possible values for {@link Wunderbaum.sort} and {@link WunderbaumNode.sort} * `options` argument. */ export interface SortOptions { /** The name of the node property that will be used for sorting. * Mandatory, unless {@link key} or {@link colId} are given. */ propName?: string; /** Callback that determines a node representation for comparison. * @default {@link common.nodeTitleKeyGetter} */ key?: SortKeyCallback; /** Callback that determines the order. @default {@link common.nodeTitleSorter} * @deprecated use {@link key} instead */ cmp?: SortCallback; /** Sort order 'asc' or 'desc'. * @default 'asc' (or if `updateColInfo` is true, the rotated status of the * column definition. * See also {@link WunderbaumOptions.sortFoldersFirst}. */ order?: SortOrderType; /** Sort descendants recursively. @default true */ deep?: boolean; /** Sort string values case insensitive. @default false */ caseInsensitive?: boolean; /** * Sort by this property if order is `undefined`. * See also {@link WunderbaumNode.resetNativeChildOrder}. * @default `_nativeIndex`. */ nativeOrderPropName?: string; /** * Rotate sort order (asc -> desc -> none) before sorting. * Update the sort icons in the column header * Note: * Sorting is done in-place. There is no 'unsorted' state, but we can * call `setCurrentSortOrder()` to renumber the `node._sortIdx` property, * which will be used as sort key, when `order` is `undefined`. * @default false */ updateColInfo?: boolean; /** Column ID as defined in `tree.columns` definition. Required if updateColInfo is true.*/ colId?: string; } /** * Possible values for {@link WunderbaumNode.sortByProperty} `options` argument. * @deprecated */ export type SortByPropertyOptions = SortOptions; /** Options passed to {@link Wunderbaum.visitRows}. */ export interface VisitRowsOptions { /** Skip filtered nodes and children of collapsed nodes. @default false */ includeHidden?: boolean; /** Return the start node as first result. @default true */ includeSelf?: boolean; /** Traverse in opposite direction, i.e. bottom up. @default false */ reverse?: boolean; /** Start traversal at this node @default first (topmost) tree node */ start?: WunderbaumNode | null; /** Wrap around at last node and continue at the top, * until the start node is reached again @default false */ wrap?: boolean; } /** * Passed as tree option.filer.connect to configure automatic integration of * filter UI controls. @experimental */ export interface FilterConnectType { inputElem: string | HTMLInputElement | null; modeButton?: string | HTMLButtonElement | null; nextButton?: string | HTMLButtonElement | HTMLAnchorElement | null; prevButton?: string | HTMLButtonElement | HTMLAnchorElement | null; matchInfoElem?: string | HTMLElement | null; } /** * Passed as tree options to configure default filtering behavior. * * @see {@link Wunderbaum.filterNodes} * @see {@link FilterNodesOptions} */ export type FilterOptionsType = { /** * Element or selector of input controls and buttons for filter query strings. * @experimental * @since 0.13 * @default null */ connect?: null | FilterConnectType; /** * Re-apply last filter if lazy data is loaded * @default true */ autoApply?: boolean; } & FilterNodesOptions; /** * Note:
* This options are used for renaming node titles.
* There is also the `tree.change` event to handle modifying node data from * input controls that are embedded in grid cells. */ export type EditOptionsType = { /** * Used to debounce the `change` event handler for grid cells [ms]. * @default 100 */ debounce?: number; /** * Minimum number of characters required for node title input field. * @default 1 */ minlength?: number; /** * Maximum number of characters allowed for node title input field. * @default null; */ maxlength?: null | number; /** * Array of strings to determine which user input should trigger edit mode. * E.g. `["clickActive", "F2", "macEnter"]`:
* 'clickActive': single click on active node title
* 'F2': press F2 key
* 'macEnter': press Enter (on macOS only)
* Pass an empty array to disable edit mode. * @default [] */ trigger?: string[]; /** * Trim whitespace before saving a node title. * @default true */ trim?: boolean; /** * Select all text of a node title, so it can be overwritten by typing. * @default true */ select?: boolean; /** * Handle 'clickActive' only if last click is less than this ms old (0: always) * @default 1000 */ slowClickDelay?: number; /** * Permanently apply node title input validations (CSS and tooltip) on keydown. * @default true */ validity?: boolean; /** * `beforeEdit(e)` may return an input HTML string. Otherwise use a default. * @category Callback */ beforeEdit?: null | ((e: WbNodeEventType) => boolean) | string; /** * * @category Callback */ edit?: null | ((e: WbNodeEventType & { inputElem: HTMLInputElement; }) => void); /** * * @category Callback */ apply?: null | ((e: WbNodeEventType & { inputElem: HTMLInputElement; }) => any) | Promise; }; export type InsertNodeType = "before" | "after" | "prependChild" | "appendChild"; export type DropEffectType = "none" | "copy" | "link" | "move"; export type DropEffectAllowedType = "none" | "copy" | "copyLink" | "copyMove" | "link" | "linkMove" | "move" | "all"; export type DropRegionType = "over" | "before" | "after"; export type DropRegionTypeSet = Set; export type DropRegionTypeList = Array; export interface DragEventType extends WbNodeEventType { /** The original event. */ event: DragEvent; /** The source node. */ node: WunderbaumNode; } export interface DropEventType extends WbNodeEventType { /** The original event. */ event: DragEvent; /** The target node. */ node: WunderbaumNode; /** The source node if any. */ sourceNode: WunderbaumNode; /** The DataTransfer object. */ dataTransfer: DataTransfer; } export type DndOptionsType = { /** * Expand nodes after n milliseconds of hovering * @default 1500 */ autoExpandMS?: 1500; /** * true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed * @default false */ multiSource?: false; /** * Restrict the possible cursor shapes and modifier operations * (can also be set in the dragStart event) * @default "all" */ effectAllowed?: DropEffectAllowedType; /** * Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed. * Overidable in the dragEnter or dragOver event. * @default "move" */ dropEffectDefault?: DropEffectType; /** * Use opinionated heuristics to determine the dropEffect ('copy', 'link', or 'move') * based on `effectAllowed`, `dropEffectDefault`, and modifier keys. * This is recalculated before each dragEnter and dragOver event and can be * overridden there. * * @default true */ guessDropEffect: boolean; /** * Prevent dropping nodes from different Wunderbaum trees * @default false */ preventForeignNodes?: boolean; /** * Prevent dropping items on unloaded lazy Wunderbaum tree nodes * @default true */ preventLazyParents?: boolean; /** * Prevent dropping items other than Wunderbaum tree nodes * @default false */ preventNonNodes?: boolean; /** * Prevent dropping nodes on own descendants * @default true */ preventRecursion?: boolean; /** * Prevent dropping nodes under same direct parent * @default false */ preventSameParent?: boolean; /** * Prevent dropping nodes 'before self', etc. (move only) * @default true */ preventVoidMoves?: boolean; /** * Serialize Node Data to datatransfer object * @default true */ serializeClipboardData?: boolean | ((nodeData: WbNodeData, node: WunderbaumNode) => string); /** * Enable auto-scrolling while dragging * @default true */ scroll?: boolean; /** * Active top/bottom margin in pixel * @default 20 */ scrollSensitivity?: 20; /** * Pixel per event * @default 5 */ scrollSpeed?: 5; /** * Optional callback passed to `toDict` on dragStart * @default null * @category Callback */ sourceCopyHook?: null; /** * Callback(sourceNode, data), return true, to enable dnd drag * @default null * @category Callback */ dragStart?: null | ((e: DragEventType) => boolean); /** * Callback(sourceNode, data) * @default null * @category Callback */ drag?: null | ((e: DragEventType) => void); /** * Callback(sourceNode, data) * @default null * @category Callback */ dragEnd?: null | ((e: DragEventType) => void); /** * Callback(targetNode, data), return true, to enable dnd drop * @default null * @category Callback */ dragEnter?: null | ((e: DropEventType) => DropRegionType | DropRegionTypeSet | DropRegionTypeList | boolean); /** * Callback(targetNode, data) * @default null * @category Callback */ dragOver?: null | ((e: DropEventType) => void); /** * Callback(targetNode, data), return false to prevent autoExpand * @default null * @category Callback */ dragExpand?: null | ((e: DropEventType) => boolean); /** * Callback(targetNode, data) * @default null * @category Callback */ drop?: null | ((e: WbNodeEventType & { event: DragEvent; region: DropRegionType; suggestedDropMode: InsertNodeType; suggestedDropEffect: DropEffectType; sourceNode: WunderbaumNode; sourceNodeData: WbNodeData | null; }) => void); /** * Callback(targetNode, data) * @default null * @category Callback */ dragLeave?: null | ((e: DropEventType) => void); }; export type GridOptionsType = object; export type KeynavOptionsType = object; export type LoggerOptionsType = object; } declare module "wb_ext_dnd" { import { Wunderbaum } from "wunderbaum"; import { WunderbaumExtension } from "wb_extension_base"; import { WunderbaumNode } from "wb_node"; import { DndOptionsType, DropEffectType, DropRegionType, DropRegionTypeSet } from "types"; import { DebouncedFunction } from "debounce"; export class DndExtension extends WunderbaumExtension { protected srcNode: WunderbaumNode | null; protected lastTargetNode: WunderbaumNode | null; protected lastEnterStamp: number; protected lastAllowedDropRegions: DropRegionTypeSet | null; protected lastDropEffect: DropEffectType | null; protected lastDropRegion: DropRegionType | false; protected currentScrollDir: number; protected applyScrollDirThrottled: DebouncedFunction<() => void>; constructor(tree: Wunderbaum); init(): void; /** Cleanup classes after target node is no longer hovered. */ protected _leaveNode(): void; /** */ protected unifyDragover(res: any): DropRegionTypeSet | false; /** * Calculates the drop region based on the drag event and the allowed drop regions. */ protected _calcDropRegion(e: DragEvent, allowed: DropRegionTypeSet | null): DropRegionType | false; /** * Guess drop effect (copy/link/move) using opinionated conventions. * * Default: dnd.dropEffectDefault */ protected _guessDropEffect(e: DragEvent): DropEffectType; /** Don't allow void operation ('drop on self').*/ protected _isVoidDrop(targetNode: WunderbaumNode, srcNode: WunderbaumNode | null, dropRegion: DropRegionType | false): boolean; protected _applyScrollDir(): void; protected _autoScroll(viewportY: number): number; /** Return true if a drag operation currently in progress. */ isDragging(): boolean; /** * Handle dragstart, drag and dragend events for the source node. */ protected onDragEvent(e: DragEvent): boolean; /** * Handle dragenter, dragover, dragleave, drop events. */ protected onDropEvent(e: DragEvent): boolean; } } declare module "wb_ext_edit" { /*! * Wunderbaum - ext-edit * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { Wunderbaum } from "wunderbaum"; import { WunderbaumExtension } from "wb_extension_base"; import { WunderbaumNode } from "wb_node"; import { EditOptionsType, InsertNodeType, WbNodeData } from "types"; export class EditExtension extends WunderbaumExtension { protected debouncedOnChange: (e: Event) => void; protected curEditNode: WunderbaumNode | null; protected relatedNode: WunderbaumNode | null; constructor(tree: Wunderbaum); protected _applyChange(eventName: string, node: WunderbaumNode, colElem: HTMLElement, inputElem: HTMLInputElement, extra: any): Promise; protected _onChange(e: Event): void; init(): void; _preprocessKeyEvent(data: any): boolean | undefined; /** Return true if a title is currently being edited. */ isEditingTitle(node?: WunderbaumNode): boolean; /** Start renaming, i.e. replace the title with an embedded ``. */ startEditTitle(node?: WunderbaumNode | null): void; /** * * @param apply * @returns */ stopEditTitle(apply: boolean): void; _stopEditTitle(apply: boolean, options: any): void; /** * Create a new child or sibling node and start edit mode. */ createNode(mode?: InsertNodeType, node?: WunderbaumNode | null, init?: string | WbNodeData): void; } } declare module "drag_observer" { /*! * Wunderbaum - drag_observer * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ export type DragCallbackArgType = { /** "dragstart", "drag", or "dragstop". */ type: string; /** Original mousedown or touch event that triggered the dragstart event. */ startEvent: MouseEvent | TouchEvent; /** Original mouse or touch event that triggered the current drag event. * Note that this is not the same as `startEvent`, but a mousemove in case of * a dragstart threshold. */ event: MouseEvent | TouchEvent; /** Custom data that was passed to the DragObserver, typically on dragstart. */ customData: any; /** Element which is currently dragged. */ dragElem: HTMLElement | null; /** Relative horizontal drag distance since start. */ dx: number; /** Relative vertical drag distance since start. */ dy: number; /** False if drag was canceled. */ apply?: boolean; }; export type DragCallbackType = (e: DragCallbackArgType) => boolean | void; type DragObserverOptionsType = { /**Event target (typically `window.document`). */ root: EventTarget; /**Event delegation selector.*/ selector?: string; /**Minimum drag distance in px. */ thresh?: number; /**Return `false` to cancel drag. */ dragstart: DragCallbackType; drag?: DragCallbackType; dragstop?: DragCallbackType; }; /** * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'. */ export class DragObserver { protected _handler: any; protected root: EventTarget; protected start: { event: MouseEvent | TouchEvent | null; x: number; y: number; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; }; protected dragElem: HTMLElement | null; protected dragging: boolean; protected customData: object; protected events: string[]; protected opts: DragObserverOptionsType; constructor(opts: DragObserverOptionsType); /** Unregister all event listeners. */ disconnect(): void; getDragElem(): HTMLElement | null; isDragging(): boolean; stopDrag(cb_event?: DragCallbackArgType): void; protected handleEvent(e: MouseEvent): boolean | void; } } declare module "wb_ext_grid" { /*! * Wunderbaum - ext-grid * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { Wunderbaum } from "wunderbaum"; import { WunderbaumExtension } from "wb_extension_base"; import { DragCallbackArgType, DragObserver } from "drag_observer"; import { GridOptionsType } from "types"; export class GridExtension extends WunderbaumExtension { protected observer: DragObserver; constructor(tree: Wunderbaum); init(): void; /** * Handles drag and sragstop events for column resizing. */ protected handleDrag(e: DragCallbackArgType): void; } } declare module "wb_ext_keynav" { /*! * Wunderbaum - ext-keynav * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { KeynavOptionsType } from "types"; import { Wunderbaum } from "wunderbaum"; import { WunderbaumExtension } from "wb_extension_base"; export class KeynavExtension extends WunderbaumExtension { constructor(tree: Wunderbaum); protected _getEmbeddedInputElem(elem: any): HTMLInputElement | null; protected _isCurInputFocused(): boolean; onKeyEvent(data: any): boolean | undefined; } } declare module "wb_ext_logger" { /*! * Wunderbaum - ext-logger * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { LoggerOptionsType } from "types"; import { WunderbaumExtension } from "wb_extension_base"; import { Wunderbaum } from "wunderbaum"; export class LoggerExtension extends WunderbaumExtension { readonly prefix: string; protected ignoreEvents: Set; constructor(tree: Wunderbaum); init(): void; onKeyEvent(data: any): boolean | undefined; } } declare module "wb_extension_base" { import { DndExtension } from "wb_ext_dnd"; import { EditExtension } from "wb_ext_edit"; import { FilterExtension } from "wb_ext_filter"; import { GridExtension } from "wb_ext_grid"; import { KeynavExtension } from "wb_ext_keynav"; import { LoggerExtension } from "wb_ext_logger"; import { WunderbaumOptions } from "wb_options"; import { Wunderbaum } from "wunderbaum"; export type ExtensionsDict = { dnd: DndExtension; edit: EditExtension; filter: FilterExtension; grid: GridExtension; keynav: KeynavExtension; logger: LoggerExtension; [key: string]: WunderbaumExtension; }; export abstract class WunderbaumExtension { enabled: boolean; readonly id: string; readonly tree: Wunderbaum; readonly treeOpts: WunderbaumOptions; readonly extensionOpts: any; constructor(tree: Wunderbaum, id: string, defaults: TOptions); /** Called on tree (re)init after all extensions are added, but before loading.*/ init(): void; getPluginOption(name: string, defaultValue?: any): any; setPluginOption(name: string, value: any): void; setEnabled(flag?: boolean): void; onKeyEvent(data: any): boolean | undefined; onRender(data: any): boolean | undefined; } } declare module "wb_ext_filter" { import { FilterNodesOptions, FilterOptionsType, NodeFilterCallback } from "types"; import { Wunderbaum } from "wunderbaum"; import { WunderbaumExtension } from "wb_extension_base"; export class FilterExtension extends WunderbaumExtension { queryInput: HTMLInputElement | null; prevButton: HTMLElement | HTMLAnchorElement | null; nextButton: HTMLElement | HTMLAnchorElement | null; modeButton: HTMLButtonElement | null; matchInfoElem: HTMLElement | null; lastFilterArgs: IArguments | null; constructor(tree: Wunderbaum); init(): void; setPluginOption(name: string, value: any): void; _updatedConnectedControls(): void; _connectControls(): void; _applyFilterNoUpdate(filter: string | RegExp | NodeFilterCallback, _opts: FilterNodesOptions): number; _applyFilterImpl(filter: string | RegExp | NodeFilterCallback, _opts: FilterNodesOptions): number; /** * [ext-filter] Dim or hide nodes. */ filterNodes(filter: string | RegExp | NodeFilterCallback, options: FilterNodesOptions): number; /** * [ext-filter] Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches(filter: string | NodeFilterCallback, options: FilterNodesOptions): number; /** * [ext-filter] Return the number of matched nodes. */ countMatches(): number; /** * [ext-filter] Re-apply current filter. */ updateFilter(): void; /** * [ext-filter] Reset the filter. */ clearFilter(): void; } } declare module "wunderbaum" { /*! * wunderbaum.ts * * A treegrid control. * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). * https://github.com/mar10/wunderbaum * * Released under the MIT license. * @version @VERSION * @date @DATE */ import * as util from "util"; import { ExtensionsDict, WunderbaumExtension } from "wb_extension_base"; import { AddChildrenOptions, ApplyCommandOptions, ApplyCommandType, ChangeType, ColumnDefinitionList, ExpandAllOptions, FilterModeType, FilterNodesOptions, IconMapType, GetStateOptions, MatcherCallback, NavigationType, NavModeEnum, NodeFilterCallback, NodeStatusType, NodeStringCallback, NodeToDictCallback, NodeTypeDefinitionMap, NodeVisitCallback, RenderFlag, ScrollToOptions, SetActiveOptions, SetColumnOptions, SetStateOptions, SetStatusOptions, SortCallback, SourceType, TreeStateDefinition, UpdateOptions, VisitRowsOptions, WbEventInfo, WbNodeData, SortOptions, DeprecationOptions, SortByPropertyOptions, ReloadOptions, LoadLazyNodesOptions } from "types"; import { WunderbaumNode } from "wb_node"; import { InitWunderbaumOptions, WunderbaumOptions } from "wb_options"; import { DebouncedFunction } from "debounce"; /** * A persistent plain object or array. * * See also {@link WunderbaumOptions}. */ export class Wunderbaum { protected static sequence: number; protected enabled: boolean; /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */ static version: string; /** The invisible root node, that holds all visible top level nodes. */ readonly root: WunderbaumNode; /** Unique tree ID as passed to constructor. Defaults to `"wb_SEQUENCE"`. */ readonly id: string; /** The `div` container element that was passed to the constructor. */ readonly element: HTMLDivElement; /** The `div.wb-header` element if any. */ readonly headerElement: HTMLDivElement; /** The `div.wb-list-container` element that contains the `nodeListElement`. */ readonly listContainerElement: HTMLDivElement; /** The `div.wb-node-list` element that contains all visible div.wb-row child elements. */ readonly nodeListElement: HTMLDivElement; /** Contains additional data that was sent as response to an Ajax source load request. */ readonly data: { [key: string]: any; }; protected readonly _updateViewportThrottled: DebouncedFunction<() => void>; protected extensionList: WunderbaumExtension[]; protected extensions: ExtensionsDict; /** Merged options from constructor args and tree- and extension defaults. */ options: WunderbaumOptions; protected keyMap: Map; protected refKeyMap: Map>; protected treeRowCount: number; protected _disableUpdateCount: number; protected _disableUpdateIgnoreCount: number; protected _activeNode: WunderbaumNode | null; protected _focusNode: WunderbaumNode | null; protected _initialSource: SourceType | null; /** Currently active node if any. * Use {@link WunderbaumNode.setActive|setActive} to modify. */ get activeNode(): WunderbaumNode; /** Current node hat has keyboard focus if any. * Use {@link WunderbaumNode.setFocus|setFocus()} to modify. */ get focusNode(): WunderbaumNode; /** Shared properties, referenced by `node.type`. */ types: NodeTypeDefinitionMap; /** List of column definitions. */ columns: ColumnDefinitionList; protected _columnsById: { [key: string]: any; }; protected resizeObserver: ResizeObserver; protected pendingChangeTypes: Set; /** A Promise that is resolved when the tree was initialized (similar to `init(e)` event). */ readonly ready: Promise; /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */ static util: typeof util; /** A map of default iconMaps. * May be used as default, when passing partial icon definition maps: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` */ static iconMaps: { bootstrap: IconMapType; fontawesome6: IconMapType; }; /** Expose some useful methods of the util.ts module as `tree._util`. */ _util: typeof util; /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ breadcrumb: HTMLElement | null; /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ filterMode: FilterModeType; /** @internal Use `setColumn()`/`getActiveColElem()` to access. */ activeColIdx: number; /** @internal */ _cellNavMode: boolean; /** @internal */ lastQuicksearchTime: number; /** @internal */ lastQuicksearchTerm: string; protected lastClickTime: number; constructor(options: InitWunderbaumOptions); private _registerEventHandlers; /** * Return a Wunderbaum instance, from element, id, index, or event. * * ```js * getTree(); // Get first Wunderbaum instance on page * getTree(1); // Get second Wunderbaum instance on page * getTree(event); // Get tree for this mouse- or keyboard event * getTree("foo"); // Get tree for this `tree.options.id` * getTree("#tree"); // Get tree for first matching element selector * ``` */ static getTree(el?: Element | Event | number | string | WunderbaumNode): Wunderbaum | null; /** * Return the icon-function -> icon-definition mapping. * @deprecated Use {@link Wunderbaum.iconMaps} */ get iconMap(): IconMapType; /** * Return a WunderbaumNode instance from element or event. */ static getNode(el: Element | Event): WunderbaumNode | null; /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link Wunderbaum.visit}. * * Example: * ```js * for(const node of tree) { * ... * } * ``` */ [Symbol.iterator](): IterableIterator; /** @internal */ protected _registerExtension(extension: WunderbaumExtension): void; /** Called on tree (re)init after markup is created, before loading. */ protected _initExtensions(): void; /** * Calculate a *stable*, unique key for a node from its refKey (or title). * We also add information from the parent, because a refKey may occur multiple * times in a tree (but not as child of the same parent). * @internal */ _calculateKey(data: WbNodeData, parent?: WunderbaumNode): string; /** Add node to tree's bookkeeping data structures. @internal */ _registerNode(node: WunderbaumNode): void; /** Remove node from tree's bookkeeping data structures. @internal */ _unregisterNode(node: WunderbaumNode): void; /** Call all hook methods of all registered extensions.*/ protected _callHook(hook: keyof WunderbaumExtension, data?: any): any; /** * Call tree method or extension method if defined. * * Example: * ```js * tree._callMethod("edit.startEdit", "arg1", "arg2") * ``` */ _callMethod(name: string, ...args: any[]): any; /** * Call event handler if defined in tree or tree.EXTENSION options. * * Example: * ```js * tree._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type: string, extra?: any): any; /** Return the node for given row index. */ protected _getNodeByRowIdx(idx: number): WunderbaumNode | null; /** Return the topmost visible node in the viewport. * @param complete If `false`, the node is considered visible if at least one * pixel is visible. */ getTopmostVpNode(complete?: boolean): WunderbaumNode; /** Return the lowest visible node in the viewport. */ getLowestVpNode(complete?: boolean): WunderbaumNode; /** Return preceding visible node in the viewport. */ protected _getPrevNodeInView(node?: WunderbaumNode, ofs?: number): WunderbaumNode; /** Return following visible node in the viewport. */ protected _getNextNodeInView(node?: WunderbaumNode, options?: { ofs?: number; reverse?: boolean; cb?: (n: WunderbaumNode) => boolean; }): WunderbaumNode; /** * Append (or insert) a list of toplevel nodes. * * @see {@link WunderbaumNode.addChildren} */ addChildren(nodeData: any, options?: AddChildrenOptions): WunderbaumNode; /** * Apply a modification (or navigation) operation on the **tree or active node**. */ applyCommand(cmd: ApplyCommandType, options?: ApplyCommandOptions): any; /** * Apply a modification (or navigation) operation on a **node**. * @see {@link WunderbaumNode.applyCommand} */ applyCommand(cmd: ApplyCommandType, node: WunderbaumNode, options?: ApplyCommandOptions): any; /** Delete all nodes. */ clear(): void; /** * Clear nodes and markup and detach events and observers. * * This method may be useful to free up resources before re-creating a tree * on an existing div, for example in unittest suites. * Note that this Wunderbaum instance becomes unusable afterwards. */ destroy(): void; /** * Return `tree.option.NAME` (also resolving if this is a callback). * * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. * * @param name option name (use dot notation to access extension option, e.g. * `filter.mode`) */ getOption(name: string, defaultValue?: any): any; /** * Set tree option. * Use dot notation to set plugin option, e.g. "filter.mode". */ setOption(name: string, value: any): void; /** Return true if the tree (or one of its nodes) has the input focus. */ hasFocus(): boolean; /** * Return true if the tree displays a header. Grids have a header unless the * `header` option is set to `false`. Plain trees have a header if the `header` * option is a string or `true`. */ hasHeader(): boolean; /** Run code, but defer rendering of viewport until done. * * ```js * const res = tree.runWithDeferredUpdate(() => { * return someFunctionThatWouldUpdateManyNodes(); * }); * ``` */ runWithDeferredUpdate(func: () => util.NotPromise): T; /** Run code, but defer rendering of viewport until done. * * ```js * const res = await tree.runWithDeferredUpdate(async () => { * return someAsyncFunctionThatWouldUpdateManyNodes(); * }); * ``` */ runWithDeferredUpdateAsync(func: () => Promise): Promise; /** Recursively expand all expandable nodes (triggers lazy load if needed). */ expandAll(flag?: boolean, options?: ExpandAllOptions): Promise; /** Recursively select all nodes. */ selectAll(flag?: boolean): import("types").TristateType; /** Toggle select all nodes. */ toggleSelect(): void; /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents?: boolean): WunderbaumNode[]; /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected?: boolean): string[]; protected _selectRange(eventInfo: WbEventInfo): false | void; /** Return the number of nodes in the data model. * @param visible if true, nodes that are hidden due to collapsed parents are ignored. */ count(visible?: boolean): number; /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`. */ countUnique(): number; /** @internal sanity check. */ _check(): void; /** * Find all nodes that match condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findAll} */ findAll(match: string | RegExp | MatcherCallback): WunderbaumNode[]; /** * Find all nodes with a given _refKey_ (aka a list of clones). * * @param refKey a `node.refKey` value to search for. * @returns an array of matching nodes with at least two element or `[]` * if nothing found. * * @see {@link WunderbaumNode.getCloneList} */ findByRefKey(refKey: string): WunderbaumNode[]; /** * Find first node that matches condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findFirst} */ findFirst(match: string | RegExp | MatcherCallback): WunderbaumNode; /** * Find first node that matches condition. * * @see {@link WunderbaumNode.findFirst} * */ findKey(key: string): WunderbaumNode | null; /** * Find the next visible node that starts with `match`, starting at `startNode` * and wrap-around at the end. * Used by quicksearch and keyboard navigation. */ findNextNode(match: string | MatcherCallback, startNode?: WunderbaumNode | null, reverse?: boolean): WunderbaumNode | null; /** * Find a node relative to another node. * * @param node * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the keyCode that would normally trigger this move, * e.g. `$.ui.keyCode.LEFT` = 'left'. * @param includeHidden Not yet implemented */ findRelatedNode(node: WunderbaumNode, where: NavigationType, includeHidden?: boolean): any; /** * Iterator version of {@link Wunderbaum.format}. */ format_iter(name_cb?: NodeStringCallback, connectors?: string[]): IterableIterator; /** * Return multiline string representation of the node hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.format((n)=>n.title)); * ``` * logs * ``` * Playground * ├─ Books * | ├─ Art of War * | ╰─ Don Quixote * ├─ Music * ... * ``` * * @see {@link Wunderbaum.format_iter} and {@link WunderbaumNode.format}. */ format(name_cb?: NodeStringCallback, connectors?: string[]): string; /** * Always returns null (so a tree instance behaves as `tree.root`). */ get parent(): null; /** * Return a list of top-level nodes. */ get children(): WunderbaumNode[]; /** * Return the active cell (`span.wb-col`) of the currently active node or null. */ getActiveColElem(): HTMLSpanElement; /** * Return the currently active node or null (alias for `tree.activeNode`). * Alias for {@link Wunderbaum.activeNode}. * * @see {@link WunderbaumNode.setActive} * @see {@link WunderbaumNode.isActive} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getActiveNode(): WunderbaumNode; /** * Return the first top level node if any (not the invisible root node). */ getFirstChild(): WunderbaumNode; /** * Return the last top level node if any (not the invisible root node). */ getLastChild(): WunderbaumNode; /** * Return the node that currently has keyboard focus or null. * Alias for {@link Wunderbaum.focusNode}. * @see {@link WunderbaumNode.setFocus} * @see {@link WunderbaumNode.hasFocus} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getFocusNode(): WunderbaumNode; /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ static getEventInfo(event: Event): WbEventInfo; /** * Return readable string representation for this instance. * @internal */ toString(): string; /** Return true if any node title or grid cell is currently beeing edited. * * See also {@link isEditingTitle}. */ isEditing(): boolean; /** Return true if any node is currently in edit-title mode. * * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}. */ isEditingTitle(): boolean; /** * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. */ isLoading(): boolean; /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4. * @see {@link logDebug} */ log(...args: any[]): void; /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4. * and browser console level includes debug/verbose messages. * @see {@link log} */ logDebug(...args: any[]): void; /** Write to `console.error` with tree name as prefix. */ logError(...args: any[]): void; /** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */ logInfo(...args: any[]): void; /** @internal */ logTime(label: string): string; /** @internal */ logTimeEnd(label: string): void; /** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */ logWarn(...args: any[]): void; /** Emit a warning for deprecated methods. @internal */ logDeprecate(method: string, options?: DeprecationOptions): void; /** Reset column widths to default. @since 0.10.0 */ resetColumns(): void; /** * Make sure that this node is vertically scrolled into the viewport. * * Nodes that are above the visible area become the top row, nodes that are * below the viewport become the bottom row. */ scrollTo(nodeOrOpts: ScrollToOptions | WunderbaumNode): void; /** * Make sure that this node is horizontally scrolled into the viewport. * Called by {@link setColumn}. */ protected scrollToHorz(): void; /** * Set column #colIdx to 'active'. * * This higlights the column header and -cells by adding the `wb-active` * class to all grid cells of the active column.
* Available in cell-nav mode only. * * If _options.edit_ is true, the embedded input element is focused, or if * colIdx is 0, the node title is put into edit mode. */ setColumn(colIdx: number | string, options?: SetColumnOptions): void; _setActiveNode(node: WunderbaumNode | null): void; /** Set or remove keyboard focus to the tree container. */ setActiveNode(key: string, flag?: boolean, options?: SetActiveOptions): void; /** Set or remove keyboard focus to the tree container. */ setFocus(flag?: boolean): void; _setFocusNode(node: WunderbaumNode | null): void; /** Return the current selection/expansion/activation status. @experimental */ getState(options?: GetStateOptions): TreeStateDefinition; /** Apply selection/expansion/activation status. @experimental */ setState(state: TreeStateDefinition, options?: SetStateOptions): Promise; /** * Schedule an update request to reflect a tree change. * The render operation is async and debounced unless the `immediate` option * is set. * * Use {@link WunderbaumNode.update} if only a single node has changed, * or {@link WunderbaumNode._render}) to pass special options. */ update(change: ChangeType, options?: UpdateOptions): void; /** * Update a row to reflect a single node's modification. * * @see {@link WunderbaumNode.update}, {@link WunderbaumNode._render} */ update(change: ChangeType, node: WunderbaumNode, options?: UpdateOptions): void; /** Disable mouse and keyboard interaction (return prev. state). */ setEnabled(flag?: boolean): boolean; /** Return false if tree is disabled. */ isEnabled(): boolean; /** Return true if tree has more than one column, i.e. has additional data columns. */ isGrid(): boolean; /** Return true if cell-navigation mode is active. */ isCellNav(): boolean; /** Return true if row-navigation mode is active. */ isRowNav(): boolean; /** Set the tree's navigation mode. */ setCellNav(flag?: boolean): void; /** Set the tree's navigation mode option. */ setNavigationOption(mode: NavModeEnum, reset?: boolean): void; /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */ setStatus(status: NodeStatusType, options?: SetStatusOptions): WunderbaumNode | null; /** Add or redefine node type definitions. */ setTypes(types: any, replace?: boolean): void; /** * Sort nodes list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp?: SortCallback | null, deep?: boolean): void; /** * Convenience method to implement column sorting. * @see {@link WunderbaumNode.sortByProperty}. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options: SortByPropertyOptions): void; /** * Sort nodes list by title or custom criteria. * @since 0.14.0 */ sort(options: SortOptions): void; /** Convert tree to an array of plain objects. * * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link WunderbaumNode.toDict}. */ toDictArray(callback?: NodeToDictCallback): Array; /** * Update column headers and column width. * Return true if at least one column width changed. */ _updateColumnWidths(): boolean; /** Create/update header markup from `this.columns` definition. * @internal */ protected _renderHeaderMarkup(): void; /** * Render pending changes that were scheduled using {@link WunderbaumNode.update} if any. * * This is hardly ever neccessary, since we normally either * - call `update(ChangeType.TYPE)` (async, throttled), or * - call `update(ChangeType.TYPE, {immediate: true})` (synchronous) * * `updatePendingModifications()` will only force immediate execution of * pending async changes if any. */ updatePendingModifications(): void; /** @internal */ _createNodeIcon(node: WunderbaumNode, showLoading: boolean, showBadge: boolean): HTMLElement | null; private _updateTopBreadcrumb; /** * This is the actual update method, which is wrapped inside a throttle method. * It calls `updateColumns()` and `_updateRows()`. * * This protected method should not be called directly but via * {@link WunderbaumNode.update}`, {@link Wunderbaum.update}, * or {@link Wunderbaum.updatePendingModifications}. * @internal */ protected _updateViewportImmediately(): void; protected _updateRows(options?: any): boolean; /** * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order). * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link WunderbaumNode.visit}. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * children only. * @returns {boolean} false, if the iterator was stopped. */ visit(callback: (node: WunderbaumNode) => any): import("types").NodeVisitResponse; /** * Call callback(node) for all nodes in vertical order, top down (or bottom up). * * Note that this considers expansion state, i.e. filtered nodes and children * of collapsed nodes are skipped, unless `includeHidden` is set. * * Stop iteration if callback() returns false.
* Return false if iteration was stopped. * * @returns {boolean} false if iteration was canceled */ visitRows(callback: NodeVisitCallback, options?: VisitRowsOptions): boolean; /** * Call fn(node) for all nodes in vertical order, bottom up. * @internal */ protected _visitRowsUp(callback: NodeVisitCallback, options: VisitRowsOptions): boolean; /** * Reload the tree with a new source. * * Previous data is cleared. Note that also column- and type defintions may * be passed with the `source` object. * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request * and restore the previous state. */ load(source: SourceType): Promise; /** Reload the tree and optionally restore state. * Source defaults to last ajax url if any. * Restoring the active node requires stable keys * @see {@link WunderbaumOptions.autoKeys} * @see {@link Wunderbaum.load} * @experimental */ reload(options?: ReloadOptions): Promise; /** * Make sure that all nodes in the given keyList are accessible. * This may include loading lazy parent nodes. * Recursively load (and optionally expand) all requested node paths. */ protected _loadLazyNodes(keyList: string[], options?: LoadLazyNodesOptions): Promise; /** * Disable render requests during operations that would trigger many updates. * * ```js * try { * tree.enableUpdate(false); * // ... (long running operation that would trigger many updates) * foo(); * // ... NOTE: make sure that async operations have finished, e.g. * await foo(); * } finally { * tree.enableUpdate(true); * } * ``` */ enableUpdate(flag: boolean): void; /** * Dim or hide unmatched nodes. * @param filter a string to match against node titles, or a callback function. * @param options filter options. Defaults to the `tree.options.filter` settings. * @returns the number of nodes that match the filter. * @example * ```ts * tree.filterNodes("foo", {mode: 'dim', fuzzy: true}); * // or pass a callback * tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'}); * ``` */ filterNodes(filter: string | RegExp | NodeFilterCallback, options: FilterNodesOptions): number; /** * Return the number of nodes that match the current filter. * @see {@link Wunderbaum.filterNodes} * @since 0.9.0 */ countMatches(): number; /** * Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches(filter: string | NodeFilterCallback, options: FilterNodesOptions): number; /** * Reset the filter. */ clearFilter(): void; /** * Return true if a filter is currently applied. */ isFilterActive(): boolean; /** * Re-apply current filter. */ updateFilter(): void; } } ================================================ FILE: dist/wunderbaum.esm.js ================================================ /*! * Wunderbaum - debounce.ts * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /* * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21 * MIT License: https://raw.githubusercontent.com/lodash/lodash/4.17.21-npm/LICENSE * Modified for TypeScript type annotations. */ /* --- */ /** Detect free variable `global` from Node.js. */ const freeGlobal = typeof global === "object" && global !== null && global.Object === Object && global; /** Detect free variable `globalThis` */ const freeGlobalThis = typeof globalThis === "object" && globalThis !== null && globalThis.Object == Object && globalThis; /** Detect free variable `self`. */ const freeSelf = typeof self === "object" && self !== null && self.Object === Object && self; /** Used as a reference to the global object. */ const root = freeGlobalThis || freeGlobal || freeSelf || Function("return this")(); /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * isObject({}) * // => true * * isObject([1, 2, 3]) * // => true * * isObject(Function) * // => true * * isObject(null) * // => false */ function isObject(value) { const type = typeof value; return value != null && (type === "object" || type === "function"); } /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', debounce(calculateLayout, 150)) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })) * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * jQuery(source).on('message', debounced) * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ function debounce(func, wait = 0, options = {}) { let lastArgs, lastThis, maxWait, result, timerId, lastCallTime; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === "function"; if (typeof func !== "function") { throw new TypeError("Expected a function"); } wait = +wait || 0; if (isObject(options)) { leading = !!options.leading; maxing = "maxWait" in options; maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; trailing = "trailing" in options ? !!options.trailing : trailing; } function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function startTimer(pendingFunc, wait) { if (useRAF) { root.cancelAnimationFrame(timerId); return root.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } function cancelTimer(id) { if (useRAF) { return root.cancelAnimationFrame(id); } clearTimeout(id); } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(Date.now()); } function pending() { return timerId !== undefined; } function debounced(...args) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; // eslint-disable-next-line @typescript-eslint/no-this-alias lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; } /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds (or once per browser frame). The throttled function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `throttle` and `debounce`. * * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] * The number of milliseconds to throttle invocations to; if omitted, * `requestAnimationFrame` is used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', throttle(updatePosition, 100)) * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * const throttled = throttle(renewToken, 300000, { 'trailing': false }) * jQuery(element).on('click', throttled) * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel) */ function throttle(func, wait = 0, options = {}) { let leading = true; let trailing = true; if (typeof func !== "function") { throw new TypeError("Expected a function"); } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { leading, trailing, maxWait: wait, }); } /*! * Wunderbaum - util * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** @module util */ /** Readable names for `MouseEvent.button` */ const MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right", 4: "back", 5: "forward", }; const MAX_INT = 9007199254740991; const userInfo = _getUserInfo(); /**True if the client is using a macOS platform. */ const isMac = userInfo.isMac; const REX_HTML = /[&<>"'/]/g; // Escape those characters const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips const ENTITY_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; /** A generic error that can be thrown to indicate a validation error when * handling the `apply` event for a node title or the `change` event for a * grid cell. */ class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } /** * A ES6 Promise, that exposes the resolve()/reject() methods. * * TODO: See [Promise.withResolvers()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers#description) * , a proposed standard, but not yet implemented in any browser. */ let Deferred$1 = class Deferred { constructor() { this.thens = []; this.catches = []; this.status = ""; } resolve(value) { if (this.status) { throw new Error("already settled"); } this.status = "resolved"; this.resolvedValue = value; this.thens.forEach((t) => t(value)); this.thens = []; // Avoid memleaks. } reject(error) { if (this.status) { throw new Error("already settled"); } this.status = "rejected"; this.rejectedError = error; this.catches.forEach((c) => c(error)); this.catches = []; // Avoid memleaks. } then(cb) { if (status === "resolved") { cb(this.resolvedValue); } else { this.thens.unshift(cb); } } catch(cb) { if (this.status === "rejected") { cb(this.rejectedError); } else { this.catches.unshift(cb); } } promise() { return { then: this.then, catch: this.catch, }; } }; /**Throw an `Error` if `cond` is falsey. */ function assert(cond, msg) { if (!cond) { msg = msg || "Assertion failed."; throw new Error(msg); } } function _getUserInfo() { const nav = navigator; // const ua = nav.userAgentData; const res = { isMac: /Mac/.test(nav.platform), }; return res; } /** Run `callback` when document was loaded. */ function documentReady(callback) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { callback(); } } /** Resolve when document was loaded. */ function documentReadyPromise() { return new Promise((resolve) => { documentReady(resolve); }); } /** * Iterate over Object properties or array elements. * * @param obj `Object`, `Array` or null * @param callback called for every item. * `this` also contains the item. * Return `false` to stop the iteration. */ function each(obj, callback) { if (obj == null) { // accept `null` or `undefined` return obj; } const length = obj.length; let i = 0; if (typeof length === "number") { for (; i < length; i++) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } else { for (const k in obj) { if (callback.call(obj[i], k, obj[k]) === false) { break; } } } return obj; } /** Shortcut for `throw new Error(msg)`. */ function error(msg) { throw new Error(msg); } /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */ function escapeHtml(s) { return ("" + s).replace(REX_HTML, function (s) { return ENTITY_MAP[s]; }); } // export function escapeRegExp(s: string) { // return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string // } /**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */ function escapeRegex(s) { return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); } /** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */ function escapeTooltip(s) { return ("" + s).replace(REX_TOOLTIP, function (s) { return ENTITY_MAP[s]; }); } /** TODO */ function extractHtmlText(s) { if (s.indexOf(">") >= 0) { error("Not implemented"); // return $("
").html(s).text(); } return s; } /** * Read the value from an HTML input element. * * If a `` is passed, the first child input is used. * Depending on the target element type, `value` is interpreted accordingly. * For example for a checkbox, a value of true, false, or null is returned if * the element is checked, unchecked, or indeterminate. * For datetime input control a numerical value is assumed, etc. * * Common use case: store the new user input in a `change` event handler: * * ```ts * change: (e) => { * const tree = e.tree; * const node = e.node; * // Read the value from the input control that triggered the change event: * let value = tree.getValueFromElem(e.element); * // and store it to the node model (assuming the column id matches the property name) * node.data[e.info.colId] = value; * }, * ``` * @param elem `` or `` or `` handle it node.logDebug(`Ignored ${eventName} inside focused input`); return; } // const curInputType = curInput.type || curInput.tagName; // const breakoutKeys = INPUT_KEYS[curInputType]; // if (!breakoutKeys.includes(eventName)) { // node.logDebug(`Ignored ${eventName} inside ${curInputType} input`); // return; // } } else if (curInput) { // On a cell that has an embedded, unfocused if (eventName.length === 1 && inputCanFocus) { // Typing a single char curInput.focus(); curInput.value = ""; node.logDebug(`Focus input: ${eventName}`); return false; } } if (eventName === "Tab") { eventName = "ArrowRight"; handled = true; } else if (eventName === "Shift+Tab") { eventName = tree.activeColIdx > 0 ? "ArrowLeft" : ""; handled = true; } switch (eventName) { case "+": case "Add": // case "=": // 187: '+' @ Chrome, Safari node.setExpanded(true); break; case "-": case "Subtract": node.setExpanded(false); break; case " ": // Space if (tree.activeColIdx === 0 && node.getOption("checkbox")) { node.toggleSelected(); handled = true; } else if (curInput && curInputType === "checkbox") { curInput.click(); // toggleCheckbox(curInput) // new Event("change") // curInput.change handled = true; } break; case "F2": if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Enter": tree.setFocus(); // Blur prev. input if any if ((tree.activeColIdx === 0 || isColspan) && node.isExpandable()) { node.setExpanded(!node.isExpanded()); handled = true; } else if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Escape": tree.setFocus(); // Blur prev. input if any node.log(`keynav: focus tree...`); if (tree.isCellNav() && navModeOption !== NavModeEnum.cell) { node.log(`keynav: setCellNav(false)`); tree.setCellNav(false); // row-nav mode tree.setFocus(); // handled = true; } break; case "ArrowLeft": tree.setFocus(); // Blur prev. input if any if (isColspan && node.isExpanded()) { node.setExpanded(false); } else if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(tree.activeColIdx - 1); } else if (navModeOption !== NavModeEnum.cell) { tree.setCellNav(false); // row-nav mode } handled = true; break; case "ArrowRight": tree.setFocus(); // Blur prev. input if any if (isColspan && !node.isExpanded()) { node.setExpanded(); } else if (!isColspan && tree.activeColIdx < tree.columns.length - 1) { tree.setColumn(tree.activeColIdx + 1); } handled = true; break; case "Home": // Generated by [Fn] + ArrowLeft on Mac // case "Meta+ArrowLeft": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(0); } handled = true; break; case "End": // Generated by [Fn] + ArrowRight on Mac // case "Meta+ArrowRight": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx < tree.columns.length - 1) { tree.setColumn(tree.columns.length - 1); } handled = true; break; case "ArrowDown": case "ArrowUp": case "Backspace": case "Control+End": // Generated by Control + [Fn] + ArrowRight on Mac case "Control+Home": // Generated by Control + [Fn] + Arrowleft on Mac case "Meta+ArrowDown": // [⌘] + ArrowDown on Mac case "Meta+ArrowUp": // [⌘] + ArrowUp on Mac case "PageDown": // Generated by [Fn] + ArrowDown on Mac case "PageUp": // Generated by [Fn] + ArrowUp on Mac node.navigate(eventName, { activate: activate, event: event }); // if (isCellEditMode) { // this._getEmbeddedInputElem(null, true); // set focus to input // } handled = true; break; default: handled = false; } } if (handled) { event.preventDefault(); } return; } } /*! * Wunderbaum - ext-logger * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ class LoggerExtension extends WunderbaumExtension { constructor(tree) { super(tree, "logger", {}); this.ignoreEvents = new Set([ "iconBadge", // "enhanceTitle", "render", "discard", ]); this.prefix = tree + ".ext-logger"; } init() { const tree = this.tree; // this.ignoreEvents.add(); if (tree.getOption("debugLevel") >= 4) { // const self = this; const ignoreEvents = this.ignoreEvents; const prefix = this.prefix; overrideMethod(tree, "callEvent", function (name, extra) { /* eslint-disable prefer-rest-params */ if (ignoreEvents.has(name)) { return tree._superApply(arguments); } const start = Date.now(); const res = tree._superApply(arguments); tree.logDebug(`${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`, arguments[1]); return res; }); } } onKeyEvent(data) { // this.tree.logInfo("onKeyEvent", eventToString(data.event), data); this.tree.logDebug(`${this.prefix}: onKeyEvent()`, data); return; } } /*! * Wunderbaum - ext-dnd * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ const nodeMimeType = "application/x-wunderbaum-node"; class DndExtension extends WunderbaumExtension { constructor(tree) { super(tree, "dnd", { autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering // dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" // dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) // #1021 `document.body` is not available yet // dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (override in drag, dragOver). guessDropEffect: true, // Calculate from `effectAllowed` and modifier keys) preventForeignNodes: false, // Prevent dropping nodes from different Wunderbaum trees preventLazyParents: true, // Prevent dropping items on unloaded lazy Wunderbaum tree nodes preventNonNodes: false, // Prevent dropping items other than Wunderbaum tree nodes preventRecursion: true, // Prevent dropping nodes on own descendants preventSameParent: false, // Prevent dropping nodes under same direct parent preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. (move only) serializeClipboardData: true, // Serialize node data to dataTransfer object scroll: true, // Enable auto-scrolling while dragging scrollSensitivity: 20, // Active top/bottom margin in pixel // scrollnterval: 50, // Generate event every 50 ms scrollSpeed: 5, // Scroll pixel per 50 ms // setTextTypeJson: false, // Allow dragging of nodes to different IE windows sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 // Events (drag support) dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag drag: null, // Callback(sourceNode, data) dragEnd: null, // Callback(sourceNode, data) // Events (drop support) dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop dragOver: null, // Callback(targetNode, data) dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand drop: null, // Callback(targetNode, data) dragLeave: null, // Callback(targetNode, data) }); // public dropMarkerElem?: HTMLElement; this.srcNode = null; this.lastTargetNode = null; this.lastEnterStamp = 0; this.lastAllowedDropRegions = null; this.lastDropEffect = null; this.lastDropRegion = false; this.currentScrollDir = 0; this.applyScrollDirThrottled = throttle(this._applyScrollDir, 50); } init() { super.init(); // Store the current scroll parent, which may be the tree // container, any enclosing div, or the document. // #761: scrollParent() always needs a container child // $temp = $("").appendTo(this.$container); // this.$scrollParent = $temp.scrollParent(); // $temp.remove(); const tree = this.tree; const dndOpts = tree.options.dnd; // Enable drag support if dragStart() is specified: if (dndOpts.dragStart) { onEvent(tree.element, "dragstart drag dragend", this.onDragEvent.bind(this)); } // Enable drop support if dragEnter() is specified: if (dndOpts.dragEnter) { onEvent(tree.element, "dragenter dragover dragleave drop", this.onDropEvent.bind(this)); } } /** Cleanup classes after target node is no longer hovered. */ _leaveNode() { // We remove the marker on dragenter from the previous target: const ltn = this.lastTargetNode; this.lastEnterStamp = 0; if (ltn) { ltn.setClass("wb-drop-target wb-drop-over wb-drop-after wb-drop-before", false); this.lastTargetNode = null; } } /** */ unifyDragover(res) { if (res === false) { return false; } else if (res instanceof Set) { return res.size > 0 ? res : false; } else if (res === true) { return new Set(["over", "before", "after"]); } else if (typeof res === "string" || isArray(res)) { res = toSet(res); return res.size > 0 ? res : false; } throw new Error("Unsupported drop region definition: " + res); } /** * Calculates the drop region based on the drag event and the allowed drop regions. */ _calcDropRegion(e, allowed) { const rowHeight = this.tree.options.rowHeightPx; const dy = e.offsetY; if (!allowed) { return false; } else if (allowed.size === 3) { return dy < 0.25 * rowHeight ? "before" : dy > 0.75 * rowHeight ? "after" : "over"; } else if (allowed.size === 1 && allowed.has("over")) { return "over"; } else { // Only 'before' and 'after': return dy > rowHeight / 2 ? "after" : "before"; } // return "over"; } /** * Guess drop effect (copy/link/move) using opinionated conventions. * * Default: dnd.dropEffectDefault */ _guessDropEffect(e) { // const nativeDropEffect = e.dataTransfer?.dropEffect; var _a; // if (nativeDropEffect && nativeDropEffect !== "none") { // return nativeDropEffect; // } const dndOpts = this.treeOpts.dnd; const ea = (_a = dndOpts.effectAllowed) !== null && _a !== void 0 ? _a : "all"; const canCopy = ["all", "copy", "copyLink", "copyMove"].includes(ea); const canLink = ["all", "link", "copyLink", "linkMove"].includes(ea); const canMove = ["all", "move", "copyMove", "linkMove"].includes(ea); let res = dndOpts.dropEffectDefault; if (dndOpts.guessDropEffect) { if (isMac) { if (e.altKey && canCopy) { res = "copy"; } if (e.metaKey && canMove) { res = "move"; // command key } if (e.altKey && e.metaKey && canLink) { res = "link"; } } else { if (e.ctrlKey && canCopy) { res = "copy"; } if (e.shiftKey && canMove) { res = "move"; } if (e.altKey && canLink) { res = "link"; } } } return res; } /** Don't allow void operation ('drop on self').*/ _isVoidDrop(targetNode, srcNode, dropRegion) { // this.tree.logDebug( // `_isVoidDrop: ${srcNode} -> ${dropRegion} ${targetNode}` // ); // TODO: should be checked on move only if (!this.treeOpts.dnd.preventVoidMoves || !srcNode) { return false; } if ((dropRegion === "before" && targetNode === srcNode.getNextSibling()) || (dropRegion === "after" && targetNode === srcNode.getPrevSibling())) { // this.tree.logDebug("Prevented before/after self"); return true; } // Don't allow dropping nodes on own parent (or self) return srcNode === targetNode || srcNode.parent === targetNode; } /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ _applyScrollDir() { if (this.isDragging() && this.currentScrollDir) { const dndOpts = this.tree.options.dnd; const sp = this.tree.element; // scroll parent const scrollTop = sp.scrollTop; if (this.currentScrollDir < 0) { sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed); } else if (this.currentScrollDir > 0) { sp.scrollTop = scrollTop + dndOpts.scrollSpeed; } } } /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ _autoScroll(viewportY) { const tree = this.tree; const dndOpts = tree.options.dnd; const sensitivity = dndOpts.scrollSensitivity; const sp = tree.element; // scroll parent const headerHeight = tree.headerElement.clientHeight; // May be 0 // const height = sp.clientHeight - headerHeight; // const height = sp.offsetHeight + headerHeight; const height = sp.offsetHeight; const scrollTop = sp.scrollTop; // tree.logDebug( // `autoScroll: height=${height}, scrollTop=${scrollTop}, viewportY=${viewportY}` // ); this.currentScrollDir = 0; if (scrollTop > 0 && viewportY > 0 && viewportY <= sensitivity + headerHeight) { // Mouse in top 20px area: scroll up // sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed); this.currentScrollDir = -1; } else if (scrollTop < sp.scrollHeight - height && viewportY >= height - sensitivity) { // Mouse in bottom 20px area: scroll down // sp.scrollTop = scrollTop + dndOpts.scrollSpeed; this.currentScrollDir = 1; } if (this.currentScrollDir) { this.applyScrollDirThrottled(); } return sp.scrollTop - scrollTop; } /** Return true if a drag operation currently in progress. */ isDragging() { return !!this.srcNode; } /** * Handle dragstart, drag and dragend events for the source node. */ onDragEvent(e) { var _a; const dndOpts = this.treeOpts.dnd; const srcNode = Wunderbaum.getNode(e); if (!srcNode) { this.tree.logWarn(`onDragEvent.${e.type}: no node`); return; } if (["dragstart", "dragend"].includes(e.type)) { this.tree.logDebug(`onDragEvent.${e.type} srcNode: ${srcNode}`, e); } // --- dragstart --- if (e.type === "dragstart") { // Set a default definition of allowed effects e.dataTransfer.effectAllowed = dndOpts.effectAllowed; //"copyMove"; // "all"; if (srcNode.isEditingTitle()) { srcNode.logDebug("Prevented dragging node in edit mode."); e.preventDefault(); return false; } // Let user cancel the drag operation, override effectAllowed, etc.: const res = srcNode._callEvent("dnd.dragStart", { event: e }); if (!res) { e.preventDefault(); return false; } const nodeData = srcNode.toDict(true, (n) => { // We don't want to reuse the key on drop: n._orgKey = n.key; delete n.key; }); nodeData._treeId = srcNode.tree.id; if (dndOpts.serializeClipboardData) { if (typeof dndOpts.serializeClipboardData === "function") { e.dataTransfer.setData(nodeMimeType, dndOpts.serializeClipboardData(nodeData, srcNode)); } else { e.dataTransfer.setData(nodeMimeType, JSON.stringify(nodeData)); } } // e.dataTransfer!.setData("text/html", $(node.span).html()); if (!((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.types.includes("text/plain"))) { e.dataTransfer.setData("text/plain", srcNode.title); } this.srcNode = srcNode; setTimeout(() => { // Decouple this call, so the CSS is applied to the node, but not to // the system generated drag image srcNode.setClass("wb-drag-source"); }, 0); // --- drag --- } else if (e.type === "drag") { if (dndOpts.drag) { srcNode._callEvent("dnd.drag", { event: e }); } // --- dragend --- } else if (e.type === "dragend") { srcNode.setClass("wb-drag-source", false); this.srcNode = null; if (this.lastTargetNode) { this._leaveNode(); } srcNode._callEvent("dnd.dragEnd", { event: e }); } return true; } /** * Handle dragenter, dragover, dragleave, drop events. */ onDropEvent(e) { var _a; // const isLink = event.dataTransfer.types.includes("text/uri-list"); const srcNode = this.srcNode; const srcTree = srcNode ? srcNode.tree : null; const targetNode = Wunderbaum.getNode(e); const dndOpts = this.treeOpts.dnd; const dt = e.dataTransfer; const dropRegion = this._calcDropRegion(e, this.lastAllowedDropRegions); /** Helper to log a message if predicate is false. */ const _t = (pred, msg) => { if (pred) { this.tree.log(`Prevented drop operation (${msg}).`); } return pred; }; if (!targetNode) { this._leaveNode(); e.preventDefault(); // Don't open file in browser when dropped in empty area return; } if (["drop"].includes(e.type)) { this.tree.logDebug(`onDropEvent.${e.type} targetNode: ${targetNode}, ea: ${dt === null || dt === void 0 ? void 0 : dt.effectAllowed}, ` + `de: ${dt === null || dt === void 0 ? void 0 : dt.dropEffect}, cy: ${e.offsetY}, r: ${dropRegion}, srcNode: ${srcNode}`, e); } // --- dragenter --- if (e.type === "dragenter") { // this.tree.logWarn(` onDropEvent.${e.type} targetNode: ${targetNode}`, e); this.lastAllowedDropRegions = null; // `dragleave` is not reliable with event delegation, so we generate it // from dragenter: if (this.lastTargetNode && this.lastTargetNode !== targetNode) { this._leaveNode(); } this.lastTargetNode = targetNode; this.lastEnterStamp = Date.now(); if ( // Don't drop on status node: _t(targetNode.isStatusNode(), "is status node") || // Prevent dropping nodes from different Wunderbaum trees: _t(dndOpts.preventForeignNodes && targetNode.tree !== srcTree, "preventForeignNodes") || // Prevent dropping items on unloaded lazy Wunderbaum tree nodes: _t(dndOpts.preventLazyParents && !targetNode.isLoaded(), "preventLazyParents") || // Prevent dropping items other than Wunderbaum tree nodes: _t(dndOpts.preventNonNodes && !srcNode, "preventNonNodes") || // Prevent dropping nodes on own descendants: _t(dndOpts.preventRecursion && (srcNode === null || srcNode === void 0 ? void 0 : srcNode.isAncestorOf(targetNode)), "preventRecursion") || // Prevent dropping nodes under same direct parent: _t(dndOpts.preventSameParent && srcNode && targetNode.parent === srcNode.parent, "preventSameParent") || // Don't allow void operation ('drop on self'): TODO: should be checked on move only _t(dndOpts.preventVoidMoves && targetNode === srcNode, "preventVoidMoves")) { dt.dropEffect = "none"; // this.tree.log("Prevented drop operation"); return true; // Prevent drop operation } // User may return a set of regions (or `false` to prevent drop) // Figure out a drop effect (copy/link/move) using opinated conventions. dt.dropEffect = this._guessDropEffect(e) || "none"; let regionSet = targetNode._callEvent("dnd.dragEnter", { event: e, sourceNode: srcNode, }); // regionSet = this.unifyDragover(regionSet); if (!regionSet) { dt.dropEffect = "none"; return true; // Prevent drop operation } this.lastAllowedDropRegions = regionSet; this.lastDropEffect = dt.dropEffect; const region = this._calcDropRegion(e, this.lastAllowedDropRegions); targetNode.setClass("wb-drop-target"); targetNode.setClass("wb-drop-over", region === "over"); targetNode.setClass("wb-drop-before", region === "before"); targetNode.setClass("wb-drop-after", region === "after"); e.preventDefault(); // Allow drop (Drop operation is denied by default) return false; // --- dragover --- } else if (e.type === "dragover") { const viewportY = e.clientY - this.tree.element.offsetTop; this._autoScroll(viewportY); dt.dropEffect = this._guessDropEffect(e) || "none"; targetNode._callEvent("dnd.dragOver", { event: e, sourceNode: srcNode }); const region = this._calcDropRegion(e, this.lastAllowedDropRegions); this.lastDropRegion = region; this.lastDropEffect = dt.dropEffect; if (dndOpts.autoExpandMS > 0 && targetNode.isExpandable(true) && !targetNode._isLoading && Date.now() - this.lastEnterStamp > dndOpts.autoExpandMS && targetNode._callEvent("dnd.dragExpand", { event: e, sourceNode: srcNode, }) !== false) { targetNode.setExpanded(); } if (!region || this._isVoidDrop(targetNode, srcNode, region)) { return; // We already rejected in dragenter } targetNode.setClass("wb-drop-over", region === "over"); targetNode.setClass("wb-drop-before", region === "before"); targetNode.setClass("wb-drop-after", region === "after"); e.preventDefault(); // Allow drop (Drop operation is denied by default) return false; // --- dragleave --- } else if (e.type === "dragleave") { // NOTE: we cannot trust this event, since it is always fired, // Instead we remove the marker on dragenter targetNode._callEvent("dnd.dragLeave", { event: e, sourceNode: srcNode }); // --- drop --- } else if (e.type === "drop") { e.stopPropagation(); // prevent browser from opening links? e.preventDefault(); // #69 prevent iOS browser from opening links this._leaveNode(); const region = this.lastDropRegion; let nodeData = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData(nodeMimeType); nodeData = nodeData ? JSON.parse(nodeData) : null; const srcNode = this.srcNode; const lastDropEffect = this.lastDropEffect; /* Before v0.14.0, we decoupled `_callEvent` like so: Decouple this call, because drop actions may prevent the dragend event from being fired on some browsers. setTimeout(() => {...}, 10); however this made e.dataTransfer.items inaccessible */ targetNode._callEvent("dnd.drop", { event: e, region: region, suggestedDropMode: region === "over" ? "appendChild" : region, suggestedDropEffect: lastDropEffect, sourceNode: srcNode, sourceNodeData: nodeData, dataTransfer: e.dataTransfer, }); } return false; } } /*! * Wunderbaum - drag_observer * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'. */ class DragObserver { constructor(opts) { this.start = { event: null, x: 0, y: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, }; this.dragElem = null; this.dragging = false; this.customData = {}; // TODO: touch events this.events = ["mousedown", "mouseup", "mousemove", "keydown"]; if (!opts.root) { throw new Error("Missing `root` option."); } this.opts = Object.assign({ thresh: 5 }, opts); this.root = opts.root; this._handler = this.handleEvent.bind(this); this.events.forEach((type) => { this.root.addEventListener(type, this._handler); }); } /** Unregister all event listeners. */ disconnect() { this.events.forEach((type) => { this.root.removeEventListener(type, this._handler); }); } getDragElem() { return this.dragElem; } isDragging() { return this.dragging; } stopDrag(cb_event) { if (this.dragging && this.opts.dragstop && cb_event) { cb_event.type = "dragstop"; try { this.opts.dragstop(cb_event); } catch (err) { console.error("dragstop error", err); // eslint-disable-line no-console } } this.dragElem = null; this.dragging = false; this.start.event = null; this.customData = {}; } handleEvent(e) { const type = e.type; const opts = this.opts; const cb_event = { type: e.type, startEvent: type === "mousedown" ? e : this.start.event, event: e, customData: this.customData, dragElem: this.dragElem, dx: e.pageX - this.start.x, dy: e.pageY - this.start.y, apply: undefined, }; // console.log("handleEvent", type, cb_event); switch (type) { case "keydown": this.stopDrag(cb_event); break; case "mousedown": if (this.dragElem) { this.stopDrag(cb_event); break; } if (opts.selector) { let elem = e.target; if (elem.matches(opts.selector)) { this.dragElem = elem; } else { elem = elem.closest(opts.selector); if (elem) { this.dragElem = elem; } else { break; // no event delegation selector matched } } } this.start.event = e; this.start.x = e.pageX; this.start.y = e.pageY; this.start.altKey = e.altKey; this.start.ctrlKey = e.ctrlKey; this.start.metaKey = e.metaKey; this.start.shiftKey = e.shiftKey; break; case "mousemove": // TODO: debounce/throttle? // TODO: horizontal mode: ignore if dx unchanged if (!this.dragElem) { break; } if (!this.dragging) { if (opts.thresh) { const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy; if (dist2 < opts.thresh * opts.thresh) { break; } } cb_event.type = "dragstart"; if (opts.dragstart(cb_event) === false) { this.stopDrag(cb_event); break; } this.dragging = true; } if (this.dragging && this.opts.drag) { cb_event.type = "drag"; this.opts.drag(cb_event); } break; case "mouseup": if (!this.dragging) { this.stopDrag(cb_event); break; } if (e.button === 0) { cb_event.apply = true; } else { cb_event.apply = false; } this.stopDrag(cb_event); break; } } } /*! * Wunderbaum - common * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script /** * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`. */ const DEFAULT_ROW_HEIGHT = 22; /** * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`. */ const ICON_WIDTH = 20; /** * Adjust the width of the title span, so overflow ellipsis work. * (2 x `$col-padding-x` + 3px rounding errors). */ const TITLE_SPAN_PAD_Y = 7; /** Render row markup for N nodes above and below the visible viewport. */ const RENDER_MAX_PREFETCH = 5; /** Minimum column width if not set otherwise. */ const DEFAULT_MIN_COL_WIDTH = 4; /** * A value for `node.type` that by convention may be used to mark a node as directory. * It may be used to sort 'directories' to the top. */ const NODE_TYPE_FOLDER = "folder"; /** Regular expression to detect if a string describes an image URL (in contrast * to a class name). Strings are considered image urls if they contain '.' or '/'. * `<` is ignored, because it is probably an html tag. */ const TEST_FILE_PATH = /^(?!.*<).*[/.]/; /** Regular expression to detect if a string describes an HTML element. */ const TEST_HTML = / Loading...
', // noData: "bi bi-search", noData: "bi bi-question-circle", expanderExpanded: "bi bi-chevron-down", // expanderExpanded: "bi bi-dash-square", expanderCollapsed: "bi bi-chevron-right", // expanderCollapsed: "bi bi-plus-square", expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander", // expanderLazy: "bi bi-chevron-bar-right", checkChecked: "bi bi-check-square", checkUnchecked: "bi bi-square", checkUnknown: "bi bi-dash-square-dotted", radioChecked: "bi bi-circle-fill", radioUnchecked: "bi bi-circle", radioUnknown: "bi bi-record-circle", folder: "bi bi-folder2", folderOpen: "bi bi-folder2-open", folderLazy: "bi bi-folder-symlink", doc: "bi bi-file-earmark", colSortable: "bi bi-chevron-expand", // colSortable: "bi bi-arrow-down-up", // colSortAsc: "bi bi-chevron-down", // colSortDesc: "bi bi-chevron-up", colSortAsc: "bi bi-arrow-down", colSortDesc: "bi bi-arrow-up", colFilter: "bi bi-filter-circle", colFilterActive: "bi bi-filter-circle-fill wb-helper-invalid", colMenu: "bi bi-three-dots-vertical", }, fontawesome6: { error: "fa-solid fa-triangle-exclamation", loading: "fa-solid fa-chevron-right fa-beat", noData: "fa-solid fa-circle-question", expanderExpanded: "fa-solid fa-chevron-down", expanderCollapsed: "fa-solid fa-chevron-right", expanderLazy: "fa-solid fa-chevron-right wb-helper-lazy-expander", checkChecked: "fa-regular fa-square-check", checkUnchecked: "fa-regular fa-square", checkUnknown: "fa-regular fa-square-minus", radioChecked: "fa-solid fa-circle", radioUnchecked: "fa-regular fa-circle", radioUnknown: "fa-regular fa-circle-question", folder: "fa-regular fa-folder-closed", folderOpen: "fa-regular fa-folder-open", folderLazy: "fa-solid fa-folder-plus", doc: "fa-regular fa-file", colSortable: "fa-solid fa-fw fa-sort", colSortAsc: "fa-solid fa-fw fa-sort-up", colSortDesc: "fa-solid fa-fw fa-sort-down", colFilter: "fa-solid fa-fw fa-filter", colFilterActive: "fa-solid fa-fw fa-filter wb-helper-invalid", colMenu: "fa-solid fa-fw fa-ellipsis-v", }, }; /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */ const RESERVED_TREE_SOURCE_KEYS = new Set([ "_format", // reserved for future use "_keyMap", // Used for compressed data format "_positional", // Used for compressed data format "_typeList", // Used for compressed data format @deprecated "_valueMap", // Used for compressed data format "_version", // reserved for future use "children", "columns", "types", ]); // /** Key codes that trigger grid navigation, even when inside an input element. */ // export const INPUT_BREAKOUT_KEYS: Set = new Set([ // // "ArrowDown", // // "ArrowUp", // "Enter", // "Escape", // ]); /** Map `KeyEvent.key` to navigation action. */ const KEY_TO_NAVIGATION_MAP = { ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", Backspace: "parent", End: "lastCol", Home: "firstCol", "Control+End": "last", "Control+Home": "first", "Meta+ArrowDown": "last", // macOs "Meta+ArrowUp": "first", // macOs PageDown: "pageDown", PageUp: "pageUp", }; /** Return a callback that returns true if the node title matches the string * or regular expression. * @see {@link WunderbaumNode.findAll} */ function makeNodeTitleMatcher(match) { if (match instanceof RegExp) { return function (node) { return match.test(node.title); }; } assert(typeof match === "string", `Expected a string or RegExp: ${match}`); // s = escapeRegex(s.toLowerCase()); return function (node) { return node.title === match; // console.log("match " + node, node.title.toLowerCase().indexOf(match)) // return node.title.toLowerCase().indexOf(match) >= 0; }; } /** Return a callback that returns true if the node title starts with a string (case-insensitive). */ function makeNodeTitleStartMatcher(s) { s = escapeRegex(s); const reMatch = new RegExp("^" + s, "i"); return function (node) { return reMatch.test(node.title); }; } /** Compare two nodes by title (case-insensitive). * @deprecated Use `key` option instead of `cmp` in sort methods. */ function nodeTitleSorter(a, b) { const x = a.title.toLowerCase(); const y = b.title.toLowerCase(); return x === y ? 0 : x > y ? 1 : -1; } // /** Compare nodes by title (case-insensitive). */ // export function nodeTitleKeyGetter( // node: WunderbaumNode // ): string | number | Array { // return node.title.toLowerCase(); // } /** * Convert 'flat' to 'nested' format. * * Flat node entry format: * [PARENT_IDX, {KEY_VALUE_ARGS}] * or, if N _positional re defined: * [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N] * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended: * [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}] * * 1. Parent-referencing list is converted to a list of nested dicts with * optional `children` properties. * 2. `[POSITIONAL_ARGS]` are added as dict attributes. */ function unflattenSource(source) { var _a, _b, _c; const { _format, _keyMap = {}, _positional = [], children } = source; const _positionalCount = _positional.length; if (_format !== "flat") { throw new Error(`Expected source._format: "flat", but got ${_format}`); } if (_positionalCount && _positional.includes("children")) { throw new Error(`source._positional must not include "children": ${_positional}`); } let longToShort = _keyMap; if (_keyMap.t) { // Inverse keyMap was used (pre 0.7.0) // TODO: raise Error on final 1.x release const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console longToShort = {}; for (const [key, value] of Object.entries(_keyMap)) { longToShort[value] = key; } } const positionalShort = _positional.map((e) => { var _a; return (_a = longToShort[e]) !== null && _a !== void 0 ? _a : e; }); const newChildren = []; const keyToNodeMap = {}; const indexToNodeMap = {}; const keyAttrName = (_a = longToShort["key"]) !== null && _a !== void 0 ? _a : "key"; const childrenAttrName = (_b = longToShort["children"]) !== null && _b !== void 0 ? _b : "children"; for (const [index, nodeTuple] of children.entries()) { // Node entry format: // [PARENT_ID, [POSITIONAL_ARGS]] // or // [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}] let kwargs; const [parentId, ...args] = nodeTuple; if (args.length === _positionalCount) { kwargs = {}; } else if (args.length === _positionalCount + 1) { kwargs = args.pop(); if (typeof kwargs !== "object") { throw new Error(`unflattenSource: Expected dict as last tuple element: ${nodeTuple}`); } } else { throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`); } // Free up some memory as we go nodeTuple[1] = null; if (nodeTuple[2] != null) { nodeTuple[2] = null; } // We keep `kwargs` as our new node definition. Then we add all positional // values to this object: args.forEach((val, positionalIdx) => { kwargs[positionalShort[positionalIdx]] = val; }); args.length = 0; // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric // index of the source.children list. If PARENT_ID is a string, we search // a parent with node.key of this value. indexToNodeMap[index] = kwargs; const key = kwargs[keyAttrName]; if (key != null) { keyToNodeMap[key] = kwargs; } let parentNode = null; if (parentId === null) ; else if (typeof parentId === "number") { parentNode = indexToNodeMap[parentId]; if (parentNode === undefined) { throw new Error(`unflattenSource: Could not find parent node by index: ${parentId}.`); } } else { parentNode = keyToNodeMap[parentId]; if (parentNode === undefined) { throw new Error(`unflattenSource: Could not find parent node by key: ${parentId}`); } } if (parentNode) { (_c = parentNode[childrenAttrName]) !== null && _c !== void 0 ? _c : (parentNode[childrenAttrName] = []); parentNode[childrenAttrName].push(kwargs); } else { newChildren.push(kwargs); } } source.children = newChildren; } /** * Decompresses the source data by * - converting from 'flat' to 'nested' format * - expanding short alias names to long names (if defined in _keyMap) * - resolving value indexes to value strings (if defined in _valueMap) * * @param source - The source object to be decompressed. * @returns void */ function decompressSourceData(source) { let { _format, _version = 1, _keyMap, _valueMap } = source; assert(_version === 1, `Expected file version 1 instead of ${_version}`); let longToShort = _keyMap; let shortToLong = {}; if (longToShort) { for (const [key, value] of Object.entries(longToShort)) { shortToLong[value] = key; } } // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction) // TODO: raise Error on final 1.x release if (longToShort && longToShort.t) { const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console [longToShort, shortToLong] = [shortToLong, longToShort]; } // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap) // TODO: raise Error on final 1.x release if (source._typeList != null) { const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`; if (_valueMap != null) { throw new Error(msg); } else { console.warn(msg); // eslint-disable-line no-console _valueMap = { type: source._typeList }; delete source._typeList; } } if (_format === "flat") { unflattenSource(source); } delete source._format; delete source._version; delete source._keyMap; delete source._valueMap; delete source._positional; function _iter(childList) { for (const node of childList) { // Iterate over a list of names, because we modify inside the loop // (for ... of ... does not allow this) Object.getOwnPropertyNames(node).forEach((propName) => { const value = node[propName]; // Replace short names with long names if defined in _keyMap let longName = propName; if (_keyMap && shortToLong[propName] != null) { longName = shortToLong[propName]; if (longName !== propName) { node[longName] = value; delete node[propName]; } } // Replace type index with type name if defined in _valueMap if (_valueMap && typeof value === "number" && _valueMap[longName] != null) { const newValue = _valueMap[longName][value]; if (newValue == null) { throw new Error(`Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]`); } node[longName] = newValue; } }); // Recursion if (node.children) { _iter(node.children); } } } if (_keyMap || _valueMap) { _iter(source.children); } } /*! * Wunderbaum - ext-grid * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ class GridExtension extends WunderbaumExtension { constructor(tree) { super(tree, "grid", { // throttle: 200, }); this.observer = new DragObserver({ root: window.document, selector: "span.wb-col-resizer-active", thresh: 4, // throttle: 400, dragstart: (e) => { const info = Wunderbaum.getEventInfo(e.startEvent); const colDef = info.colDef; const allow = colDef && this.tree.element.contains(e.dragElem) && toBool(colDef.resizable, tree.options.columnsResizable, false); // this.tree.log("dragstart", colDef, e, info); this.tree.element.classList.toggle("wb-col-resizing", !!allow); info.colElem.classList.toggle("wb-col-resizing", !!allow); // We start dagging, so we remember the actual width in *pixels* // (which may be 'auto' or '100%'). // Since we we re-create the markup on each update, we also cannot store // the original event or DOM element, but only the colDef object. if (allow) { // Store initial target column infos in customData e.customData.colDef = colDef; e.customData.orgCustomWidthPx = colDef.customWidthPx; const curWidthPx = Number.parseInt(info.colElem.style.width, 10); e.customData.orgWidthPx = curWidthPx; // Set custom width to current width, so that we can modify it colDef.customWidthPx = curWidthPx; // this.tree.log( // `dragstart customWidthPx=${colDef.customWidthPx}`, // e, // info // ); this.tree.update(ChangeType.colStructure); // this.tree.log( // `dragstart 2 customWidthPx=${colDef.customWidthPx}`, // e, // info // ); } return allow; }, drag: (e) => { // TODO: throttle return this.handleDrag(e); }, dragstop: (e) => { return this.handleDrag(e); }, }); } init() { super.init(); } /** * Handles drag and sragstop events for column resizing. */ handleDrag(e) { const custom = e.customData; const colDef = custom.colDef; // this.tree.log(`${e.type} (dx=${e.dx})`, e, info); if (e.type === "dragstop" || e.type === "drag") { this.tree.element.classList.remove("wb-col-resizing"); // info.colElem!.classList.remove("wb-col-resizing"); if (e.apply || e.type === "drag") { const minWidth = toPixel(colDef.minWidth, DEFAULT_MIN_COL_WIDTH); const newWidth = Math.max(minWidth, custom.orgWidthPx + e.dx); colDef.customWidthPx = newWidth; // this.tree.log( // `${e.type} minWidth=${minWidth}, newWidth=${newWidth}`, // colDef // ); } else { // Drag was cancelled this.tree.log("Column resize cancelled", e); colDef.customWidthPx = custom.orgCustomWidthPx; // Restore original width or undefined } this.tree.update(ChangeType.colStructure); } } } /*! * Wunderbaum - deferred * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** * Implement a ES6 Promise, that exposes a resolve() and reject() method. * * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}. * Example: * ```js * function foo() { * let dfd = new Deferred(), * ... * dfd.resolve('foo') * ... * return dfd.promise(); * } * ``` */ class Deferred { constructor() { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } /** Resolve the Promise. */ resolve(value) { this._resolve(value); } /** Reject the Promise. */ reject(reason) { this._reject(reason); } /** Return the native Promise instance.*/ promise() { return this._promise; } /** Call Promise.then on the embedded promise instance.*/ then(cb) { return this._promise.then(cb); } /** Call Promise.catch on the embedded promise instance.*/ catch(cb) { return this._promise.catch(cb); } /** Call Promise.finally on the embedded promise instance.*/ finally(cb) { return this._promise.finally(cb); } } /*! * Wunderbaum - wunderbaum_node * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** WunderbaumNode properties that can be passed with source data. * (Any other source properties will be stored as `node.data.PROP`.) */ const NODE_PROPS = new Set([ "checkbox", "classes", "expanded", "icon", "iconTooltip", "key", "lazy", "_partsel", "radiogroup", "refKey", "selected", "statusNodeType", "title", "tooltip", "type", "unselectable", ]); /** WunderbaumNode properties that will be returned by `node.toDict()`.) */ const NODE_DICT_PROPS = new Set(NODE_PROPS); NODE_DICT_PROPS.delete("_partsel"); NODE_DICT_PROPS.delete("unselectable"); // /** Node properties that are of type bool (or boolean & string). // * When parsing, we accept 0 for false and 1 for true for better JSON compression. // */ // export const NODE_BOOL_PROPS: Set = new Set([ // "checkbox", // "colspan", // "expanded", // "icon", // "iconTooltip", // "radiogroup", // "selected", // "tooltip", // "unselectable", // ]); /** * A single tree node. * * **NOTE:**
* Generally you should not modify properties directly, since this may break * the internal bookkeeping. */ class WunderbaumNode { constructor(tree, parent, data) { var _a; /** Reference key. Unlike {@link key}, a `refKey` may occur multiple * times within a tree (in this case we have 'clone nodes'). * @see Use {@link setKey} to modify. */ this.refKey = undefined; /** * Array of child nodes (null for leaf nodes). * For lazy nodes, this is `null` or ùndefined` until the children are loaded * and leaf nodes may be `[]` (empty array). * @see {@link hasChildren}, {@link addChildren}, {@link lazy}. */ this.children = null; /** Additional classes added to `div.wb-row`. * @see {@link hasClass}, {@link setClass}. */ this.classes = null; //new Set(); /** Custom data that was passed to the constructor */ this.data = {}; this._isLoading = false; this._requestId = 0; this._errorInfo = null; this._partsel = false; this._partload = false; this.subMatchCount = 0; this._rowIdx = 0; this._rowElem = undefined; assert(!parent || parent.tree === tree, `Invalid parent: ${parent}`); assert(!data.children, "'children' not allowed here"); this.tree = tree; this.parent = parent; this.key = tree._calculateKey(data, parent); this.title = "" + ((_a = data.title) !== null && _a !== void 0 ? _a : "<" + this.key + ">"); this.expanded = !!data.expanded; this.lazy = !!data.lazy; // We set the following node properties only if a matching data value is // passed data.refKey != null ? (this.refKey = "" + data.refKey) : 0; data.type != null ? (this.type = "" + data.type) : 0; data.icon != null ? (this.icon = intToBool(data.icon)) : 0; data.tooltip != null ? (this.tooltip = intToBool(data.tooltip)) : 0; data.iconTooltip != null ? (this.iconTooltip = intToBool(data.iconTooltip)) : 0; data.statusNodeType != null ? (this.statusNodeType = ("" + data.statusNodeType)) : 0; data.colspan != null ? (this.colspan = !!data.colspan) : 0; // Selection data.checkbox != null ? (this.checkbox = intToBool(data.checkbox)) : 0; data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0; data.selected != null ? (this.selected = !!data.selected) : 0; data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0; if (data.classes) { this.setClass(data.classes); } // Store custom fields as `node.data` for (const [key, value] of Object.entries(data)) { if (!NODE_PROPS.has(key)) { this.data[key] = value; } } if (parent && !this.statusNodeType) { // Don't register root node or status nodes tree._registerNode(this); } } /** * Return readable string representation for this instance. * @internal */ toString() { return `WunderbaumNode@${this.key}<'${this.title}'>`; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link WunderbaumNode.visit}. * * Example: * ```js * for(const n of node) { * ... * } * ``` */ *[Symbol.iterator]() { // let node: WunderbaumNode | null = this; const cl = this.children; if (cl) { for (let i = 0, l = cl.length; i < l; i++) { const n = cl[i]; yield n; if (n.children) { yield* n; } } // Slower: // for (let node of this.children) { // yield node; // yield* node : 0; // } } } // /** Return an option value. */ // protected _getOpt( // name: string, // nodeObject: any = null, // treeOptions: any = null, // defaultValue: any = null // ): any { // return evalOption( // name, // this, // nodeObject || this, // treeOptions || this.tree.options, // defaultValue // ); // } /** Call event handler if defined in tree.options. * Example: * ```js * node._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type, extra) { var _a; return (_a = this.tree) === null || _a === void 0 ? void 0 : _a._callEvent(type, extend({ node: this, typeInfo: this.type ? this.tree.types[this.type] : {}, }, extra)); } /** * Append (or insert) a list of child nodes. * * Tip: pass `{ before: 0 }` to prepend new nodes as first children. * * @returns first child added */ addChildren(nodeData, options) { const tree = this.tree; let { before = null, applyMinExpanLevel = true, _level } = options !== null && options !== void 0 ? options : {}; // let { before, loadLazy=true, _level } = options ?? {}; // const isTopCall = _level == null; _level !== null && _level !== void 0 ? _level : (_level = this.getLevel()); const nodeList = []; try { tree.enableUpdate(false); if (isPlainObject(nodeData)) { nodeData = [nodeData]; } const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel; for (const child of nodeData) { const subChildren = child.children; // Remove children property from source data because it should not be // passed to the constructor of WunderbaumNode: delete child.children; const n = new WunderbaumNode(tree, this, child); // Set `children` property again, so it can be used in `reload()` if (subChildren != null) { child.children = subChildren; } if (forceExpand && !n.isUnloaded()) { n.expanded = true; } nodeList.push(n); if (subChildren) { n.addChildren(subChildren, { _level: _level + 1 }); } } if (!this.children) { this.children = nodeList; } else if (before == null || this.children.length === 0) { this.children = this.children.concat(nodeList); } else { // Returns null if before is not a direct child: before = this.findDirectChild(before); const pos = this.children.indexOf(before); assert(pos >= 0, `options.before must be a direct child of ${this}`); // insert nodeList after children[pos] this.children.splice(pos, 0, ...nodeList); } // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null); tree.update(ChangeType.structure); } finally { // if (tree.options.selectMode === "hier") { // if (this.parent && this.parent.children) { // this.fixSelection3FromEndNodes(); // } else { // // may happen when loading __root__; // } // } tree.enableUpdate(true); } // if(isTopCall && loadLazy){ // this.logWarn("addChildren(): loadLazy is not yet implemented.") // } return nodeList[0]; } /** * Append or prepend a node, or append a child node. * * This a convenience function that calls addChildren() * * @param nodeData node definition * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') * @returns new node */ addNode(nodeData, mode = "appendChild") { if (mode === "over") { mode = "appendChild"; // compatible with drop region } switch (mode) { case "after": return this.parent.addChildren(nodeData, { before: this.getNextSibling(), }); case "before": return this.parent.addChildren(nodeData, { before: this }); case "prependChild": // Insert before the first child if any // let insertBefore = this.children ? this.children[0] : undefined; return this.addChildren(nodeData, { before: 0 }); case "appendChild": return this.addChildren(nodeData); } assert(false, `Invalid mode: ${mode}`); return undefined; } /** * Apply a modification (or navigation) operation. * * @see {@link Wunderbaum.applyCommand} */ applyCommand(cmd, options) { return this.tree.applyCommand(cmd, this, options); } /** * Collapse all expanded sibling nodes if any. * (Automatically called when `autoCollapse` is true.) */ collapseSiblings(options) { for (const node of this.parent.children) { if (node !== this && node.expanded) { node.setExpanded(false, options); } } } /** * Add/remove one or more classes to `
`. * * This also maintains `node.classes`, so the class will survive a re-render. * * @param className one or more class names. Multiple classes can be passed * as space-separated string, array of strings, or set of strings. */ setClass(className, flag = true) { const cnSet = toSet(className); if (flag) { if (this.classes === null) { this.classes = new Set(); } cnSet.forEach((cn) => { var _a; this.classes.add(cn); (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag); }); } else { if (this.classes === null) { return; } cnSet.forEach((cn) => { var _a; this.classes.delete(cn); (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag); }); if (this.classes.size === 0) { this.classes = null; } } } /** Start editing this node's title. */ startEditTitle() { this.tree._callMethod("edit.startEditTitle", this); } /** * Call `setExpanded()` on all descendant nodes. * * @param flag true to expand, false to collapse. * @param options Additional options. * @see {@link Wunderbaum.expandAll} * @see {@link WunderbaumNode.setExpanded} */ async expandAll(flag = true, options) { const tree = this.tree; const { collapseOthers, deep, depth, force, keepActiveNodeVisible = true, loadLazy, resetLazy, } = options !== null && options !== void 0 ? options : {}; // limit expansion level to `depth` (or tree.minExpandLevel). Default: unlimited const treeLevel = this.tree.options.minExpandLevel || null; // 0 -> null const minLevel = depth !== null && depth !== void 0 ? depth : (force ? null : treeLevel); const expandOpts = { deep: deep, force: force, loadLazy: loadLazy, resetLazy: resetLazy, scrollIntoView: false, // don't scroll every node while iterating }; this.logInfo(`expandAll(${flag}, depth=${depth}, minLevel=${minLevel})`); assert(!(flag && deep != null && !collapseOthers), "Expanding with `deep` option is not supported (implied by the `depth` option)."); // Expand all direct children in parallel: async function _iter(n, level) { var _a; // n.logInfo(` _iter(level=${level})`); const promises = []; (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach((cn) => { if (flag) { if (!cn.expanded && (minLevel == null || level < minLevel) && (cn.children || (loadLazy && cn.lazy))) { // Node is collapsed and may be expanded (i.e. has children or is lazy) // Expanding may be async, so we store the promise. // Also the recursion is delayed until expansion finished. const p = cn.setExpanded(true, expandOpts); promises.push(p); if (depth == null) { p.then(async () => { await _iter(cn, level + 1); }); } } else { // We don't expand the node, but still visit descendants. // There we may find lazy nodes, so we promises.push(_iter(cn, level + 1)); } } else { // Collapsing is always synchronous, so no promises required // Do not collapse until minExpandLevel if (minLevel == null || level >= minLevel) { cn.setExpanded(false, expandOpts); } if ((minLevel != null && level < minLevel) || deep) { _iter(cn, level + 1); // recursion, even if cn was already collapsed } } }); return new Promise((resolve) => { Promise.all(promises).then(() => { resolve(true); }); }); } const tag = tree.logTime(`${this}.expandAll(${flag}, depth=${depth})`); try { tree.enableUpdate(false); await _iter(this, 0); if (collapseOthers) { assert(flag, "Option `collapseOthers` requires flag=true"); assert(minLevel != null, "Option `collapseOthers` requires `depth` or `minExpandLevel`"); this.expandAll(false, { depth: minLevel }); } } finally { tree.enableUpdate(true); tree.logTimeEnd(tag); } if (tree.activeNode && keepActiveNodeVisible) { tree.activeNode.scrollIntoView(); } } /** * Find all descendant nodes that match condition (excluding self). * * If `match` is a string, search for exact node title. * If `match` is a RegExp expression, apply it to node.title, using * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test). * If `match` is a callback, match all nodes for that the callback(node) returns true. * * Returns an empty array if no nodes were found. * * Examples: * ```js * // Match all node titles that match exactly 'Joe': * nodeList = node.findAll("Joe") * // Match all node titles that start with 'Joe' case sensitive: * nodeList = node.findAll(/^Joe/) * // Match all node titles that contain 'oe', case insensitive: * nodeList = node.findAll(/oe/i) * // Match all nodes with `data.price` >= 99: * nodeList = node.findAll((n) => { * return n.data.price >= 99; * }) * ``` */ findAll(match) { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); const res = []; this.visit((n) => { if (matcher(n)) { res.push(n); } }); return res; } /** Return the direct child with a given key, index or null. */ findDirectChild(ptr) { const cl = this.children; if (!cl) { return null; } if (typeof ptr === "string") { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].key === ptr) { return cl[i]; } } } else if (typeof ptr === "number") { return cl[ptr]; } else if (ptr.parent === this) { // Return null if `ptr` is not a direct child return ptr; } return null; } /** * Find first descendant node that matches condition (excluding self) or null. * * @see {@link WunderbaumNode.findAll} for examples. */ findFirst(match) { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); let res = null; this.visit((n) => { if (matcher(n)) { res = n; return false; } }); return res; } /** Find a node relative to self. * * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()} */ findRelatedNode(where, includeHidden = false) { return this.tree.findRelatedNode(this, where, includeHidden); } /** * Iterator version of {@link WunderbaumNode.format}. */ *format_iter(name_cb, connectors) { connectors !== null && connectors !== void 0 ? connectors : (connectors = [" ", " | ", " ╰─ ", " ├─ "]); name_cb !== null && name_cb !== void 0 ? name_cb : (name_cb = (node) => "" + node); function _is_last(node) { const ca = node.parent.children; return node === ca[ca.length - 1]; } const _format_line = (node) => { // https://www.measurethat.net/Benchmarks/Show/12196/0/arr-unshift-vs-push-reverse-small-array const parts = [name_cb(node)]; parts.unshift(connectors[_is_last(node) ? 2 : 3]); let p = node.parent; while (p && p !== this) { // `this` is the top node parts.unshift(connectors[_is_last(p) ? 0 : 1]); p = p.parent; } return parts.join(""); }; yield name_cb(this); for (const node of this) { yield _format_line(node); } } /** * Return a multiline string representation of a node/subnode hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.getActiveNode().format((n)=>n.title)); * ``` * logs * ``` * Books * ├─ Art of War * ╰─ Don Quixote * ``` * @see {@link WunderbaumNode.format_iter} */ format(name_cb, connectors) { const a = []; for (const line of this.format_iter(name_cb, connectors)) { a.push(line); } return a.join("\n"); } /** Return the `` element with a given index or id. * @returns {WunderbaumNode | null} */ getColElem(colIdx) { var _a; if (typeof colIdx === "string") { colIdx = this.tree.columns.findIndex((value) => value.id === colIdx); } const colElems = (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.querySelectorAll("span.wb-col"); return colElems ? colElems[colIdx] : null; } /** * Return all nodes with the same refKey. * * @param includeSelf Include this node itself. * @see {@link Wunderbaum.findByRefKey} */ getCloneList(includeSelf = false) { if (!this.refKey) { return []; } const clones = this.tree.findByRefKey(this.refKey); if (includeSelf) { return clones; } return [...clones].filter((n) => n !== this); } /** Return the first child node or null. * @returns {WunderbaumNode | null} */ getFirstChild() { return this.children ? this.children[0] : null; } /** Return the last child node or null. * @returns {WunderbaumNode | null} */ getLastChild() { return this.children ? this.children[this.children.length - 1] : null; } /** Return node depth (starting with 1 for top level nodes). */ getLevel() { let i = 0, p = this.parent; while (p) { i++; p = p.parent; } return i; } /** Return the successive node (under the same parent) or null. */ getNextSibling() { const ac = this.parent.children; const idx = ac.indexOf(this); return ac[idx + 1] || null; } /** Return the parent node (null for the system root node). */ getParent() { // TODO: return null for top-level nodes? return this.parent; } /** Return an array of all parent nodes (top-down). * @param includeRoot Include the invisible system root node. * @param includeSelf Include the node itself. */ getParentList(includeRoot = false, includeSelf = false) { const l = []; let dtn = includeSelf ? this : this.parent; while (dtn) { if (includeRoot || dtn.parent) { l.unshift(dtn); } dtn = dtn.parent; } return l; } /** Return a string representing the hierarchical node path, e.g. "a/b/c". * @param includeSelf * @param part property name or callback * @param separator */ getPath(includeSelf = true, part = "title", separator = "/") { let val; const path = []; const isFunc = typeof part === "function"; this.visitParents((n) => { if (n.parent) { val = isFunc ? part(n) : n[part]; path.unshift(val); } return undefined; // TODO remove this line }, includeSelf); return path.join(separator); } /** Return the preceding node (under the same parent) or null. */ getPrevSibling() { const ac = this.parent.children; const idx = ac.indexOf(this); return ac[idx - 1] || null; } /** Return true if node has children. * Return undefined if not sure, i.e. the node is lazy and not yet loaded. */ hasChildren() { if (this.lazy) { if (this.children == null) { return undefined; // null or undefined: Not yet loaded } else if (this.children.length === 0) { return false; // Loaded, but response was empty } else if (this.children.length === 1 && this.children[0].isStatusNode()) { return undefined; // Currently loading or load error } return true; // One or more child nodes } return !!(this.children && this.children.length); } /** Return true if node has className set. */ hasClass(className) { return this.classes ? this.classes.has(className) : false; } /** Return true if node is the currently focused node. @since 0.9.0 */ hasFocus() { return this.tree.focusNode === this; } /** Return true if this node is the currently active tree node. */ isActive() { return this.tree.activeNode === this; } /** Return true if this node is a direct or indirect parent of `other`. * @see {@link WunderbaumNode.isParentOf} */ isAncestorOf(other) { return other && other.isDescendantOf(this); } /** Return true if this node is a **direct** subnode of `other`. * @see {@link WunderbaumNode.isDescendantOf} */ isChildOf(other) { return other && this.parent === other; } /** Return true if this node's refKey is used by at least one other node. */ isClone() { return !!this.refKey && this.tree.findByRefKey(this.refKey).length > 1; } /** Return true if this node's title spans all columns, i.e. the node has no * grid cells. */ isColspan() { return !!this.getOption("colspan"); } /** Return true if this node is a direct or indirect subnode of `other`. * @see {@link WunderbaumNode.isChildOf} */ isDescendantOf(other) { if (!other || other.tree !== this.tree) { return false; } let p = this.parent; while (p) { if (p === other) { return true; } if (p === p.parent) { error(`Recursive parent link: ${p}`); } p = p.parent; } return false; } /** Return true if this node has children, i.e. the node is generally expandable. * If `andCollapsed` is set, we also check if this node is collapsed, i.e. * an expand operation is currently possible. */ isExpandable(andCollapsed = false) { // `false` is never expandable (unofficial) if ((andCollapsed && this.expanded) || this.children === false) { return false; } if (this.children == null) { return !!this.lazy; // null or undefined can trigger lazy load } if (this.children.length === 0) { return !!this.tree.options.emptyChildListExpandable; } return true; } /** Return true if _this_ node is currently in edit-title mode. * * See {@link WunderbaumNode.startEditTitle}. */ isEditingTitle() { return this.tree._callMethod("edit.isEditingTitle", this); } /** Return true if this node is currently expanded. */ isExpanded() { return !!this.expanded; } /** Return true if this node is the first node of its parent's children. */ isFirstSibling() { const p = this.parent; return !p || p.children[0] === this; } /** Return true if this node is the last node of its parent's children. */ isLastSibling() { const p = this.parent; return !p || p.children[p.children.length - 1] === this; } /** Return true if this node is lazy (even if data was already loaded) */ isLazy() { return !!this.lazy; } /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */ isLoaded() { return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node } /** Return true if node is currently loading, i.e. a GET request is pending. */ isLoading() { return this._isLoading; } /** Return true if this node is a temporarily generated status node of type 'paging'. */ isPagingNode() { return this.statusNodeType === "paging"; } /** Return true if this node is a **direct** parent of `other`. * @see {@link WunderbaumNode.isAncestorOf} */ isParentOf(other) { return other && other.parent === this; } /** Return true if this node is partially loaded. @experimental */ isPartload() { return !!this._partload; } /** Return true if this node is partially selected (tri-state). */ isPartsel() { return !this.selected && !!this._partsel; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRadio() { return !!this.parent.radiogroup || this.getOption("checkbox") === "radio"; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRendered() { return !!this._rowElem; } /** Return true if this node is the (invisible) system root node. * @see {@link WunderbaumNode.isTopLevel} */ isRootNode() { return this.tree.root === this; } /** Return true if this node is selected, i.e. the checkbox is set. * `undefined` if partly selected (tri-state), false otherwise. */ isSelected() { return this.selected ? true : this._partsel ? undefined : false; } /** Return true if this node is a temporarily generated system node like * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). */ isStatusNode() { return !!this.statusNodeType; } /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */ isTopLevel() { return this.tree.root === this.parent; } /** Return true if node is marked lazy but not yet loaded. * For non-lazy nodes always return false. */ isUnloaded() { // Also checks if the only child is a status node: return this.hasChildren() === undefined; } /** Return true if all parent nodes are expanded. Note: this does not check * whether the node is scrolled into the visible part of the screen or viewport. */ isVisible() { const hasFilter = this.tree.filterMode === "hide"; const parents = this.getParentList(false, false); // TODO: check $(n.span).is(":visible") // i.e. return false for nodes (but not parents) that are hidden // by a filter if (hasFilter && !this.match && !this.subMatchCount) { // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); return false; } for (let i = 0, l = parents.length; i < l; i++) { const n = parents[i]; if (!n.expanded) { // this.debug("isVisible: HIDDEN (parent collapsed)"); return false; } // if (hasFilter && !n.match && !n.subMatchCount) { // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); // return false; // } } // this.debug("isVisible: VISIBLE"); return true; } _loadSourceObject(source, level) { var _a; const tree = this.tree; level !== null && level !== void 0 ? level : (level = this.getLevel()); // Let caller modify the parsed JSON response: const res = this._callEvent("receive", { response: source }); if (res != null) { source = res; } if (isArray(source)) { source = { children: source }; } assert(isPlainObject(source), `Expected an array or plain object: ${source}`); const format = (_a = source.format) !== null && _a !== void 0 ? _a : "nested"; assert(format === "nested" || format === "flat", `Expected source.format = 'nested' or 'flat': ${format}`); // Pre-rocess for 'nested' or 'flat' format decompressSourceData(source); assert(source.children, "If `source` is an object, it must have a `children` property"); if (source.types) { tree.logInfo("Redefine types", source.columns); tree.setTypes(source.types, false); delete source.types; } if (source.columns) { tree.logInfo("Redefine columns", source.columns); tree.columns = source.columns; delete source.columns; tree.update(ChangeType.colStructure); } this.addChildren(source.children); // Add extra data to `tree.data` for (const [key, value] of Object.entries(source)) { if (!RESERVED_TREE_SOURCE_KEYS.has(key)) { tree.data[key] = value; // tree.logDebug(`Add source.${key} to tree.data.${key}`); } } if (tree.options.selectMode === "hier") { this.fixSelection3FromEndNodes(); } // Allow to un-sort nodes after sorting this.resetNativeChildOrder(); this._callEvent("load"); } async _fetchWithOptions(source) { var _a, _b; // Either a URL string or an object with a `.url` property. let url, params, body, options, rest; let fetchOpts = {}; if (typeof source === "string") { // source is a plain URL string: assume GET request url = source; fetchOpts.method = "GET"; } else if (isPlainObject(source)) { // source is a plain object with `.url` property. ({ url, params, body, options, ...rest } = source); assert(!rest || Object.keys(rest).length === 0, `Unexpected source properties: ${Object.keys(rest)}. Use 'options' instead.`); assert(typeof url === "string", `expected source.url as string`); if (isPlainObject(options)) { fetchOpts = options; } if (isPlainObject(body)) { // we also accept 'body' as object... assert(!fetchOpts.body, "options.body should be passed as source.body"); fetchOpts.body = JSON.stringify(fetchOpts.body); (_a = fetchOpts.method) !== null && _a !== void 0 ? _a : (fetchOpts.method = "POST"); // set default } if (isPlainObject(params)) { url += "?" + new URLSearchParams(params); (_b = fetchOpts.method) !== null && _b !== void 0 ? _b : (fetchOpts.method = "GET"); // set default } } else { url = ""; // keep linter happy error(`Unsupported source format: ${source}`); } this.setStatus(NodeStatusType.loading); const response = await fetch(url, fetchOpts); if (!response.ok) { error(`GET ${url} returned ${response.status}, ${response}`); } return await response.json(); } /** Download data from the cloud, then call `.update()`. */ async load(source) { const tree = this.tree; const requestId = Date.now(); const prevParent = this.parent; const start = Date.now(); let elap = 0, elapLoad = 0, elapProcess = 0; // Check for overlapping requests if (this._requestId) { this.logWarn(`Recursive load request #${requestId} while #${this._requestId} is pending. ` + "The previous request will be ignored."); } this._requestId = requestId; // const timerLabel = tree.logTime(this + ".load()"); try { const url = typeof source === "string" ? source : source.url; if (!url) { // An array or a plain object (that does NOT contain a `.url` property) // will be treated as native Wunderbaum data if (typeof source.then === "function") { const msg = tree.logTime(`Resolve thenable ${source}`); source = await Promise.resolve(source); tree.logTimeEnd(msg); } this._loadSourceObject(source); elapProcess = Date.now() - start; } else { // Either a URL string or an object with a `.url` property. const data = await this._fetchWithOptions(source); elapLoad = Date.now() - start; if (this._requestId && this._requestId > requestId) { this.logWarn(`Ignored load response #${requestId} because #${this._requestId} is pending.`); return; } else { this.logDebug(`Received response for load request #${requestId}`); } if (this.parent === null && prevParent !== null) { this.logWarn("Lazy parent node was removed while loading: discarding response."); return; } this.setStatus(NodeStatusType.ok); // if (data.columns) { // tree.logInfo("Re-define columns", data.columns); // util.assert(!this.parent); // tree.columns = data.columns; // delete data.columns; // tree.updateColumns({ calculateCols: false }); // } const startProcess = Date.now(); this._loadSourceObject(data); elapProcess = Date.now() - startProcess; } } catch (error) { this.logError("Error during load()", source, error); this._callEvent("error", { error: error }); this.setStatus(NodeStatusType.error, { message: "" + error }); throw error; } finally { this._requestId = 0; elap = Date.now() - start; if (tree.options.debugLevel >= 3) { tree.logInfo(`Load source took ${elap / 1000} seconds ` + `(transfer: ${elapLoad / 1000}s, ` + `processing: ${elapProcess / 1000}s)`); } } } /** * Load content of a lazy node. * If the node is already loaded, nothing happens. * @param [forceReload=false] If true, reload even if already loaded. */ async loadLazy(forceReload = false) { const wasExpanded = this.expanded; assert(this.lazy, "load() requires a lazy node"); if (!forceReload && !this.isUnloaded()) { return; // Already loaded: nothing to do } if (this.isLoading()) { this.logWarn("loadLazy() called while already loading: ignored."); return; // Already loading: prevent duplicate requests } if (this.isLoaded()) { this.resetLazy(); // Also collapses if currently expanded } // `lazyLoad` may be long-running, so mark node as loading now. `this.load()` // will reset the status later. this.setStatus(NodeStatusType.loading); try { const source = await this._callEvent("lazyLoad"); if (source === false) { this.setStatus(NodeStatusType.ok); return; } assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false."); await this.load(source); this.setStatus(NodeStatusType.ok); // Also resets `this._isLoading` if (wasExpanded) { this.expanded = true; this.tree.update(ChangeType.structure); } else { this.update(); // Fix expander icon to 'loaded' } } catch (e) { this.logError("Error during loadLazy()", e); this._callEvent("error", { error: e }); // Also resets `this._isLoading`: this.setStatus(NodeStatusType.error, { message: "" + e }); } return; } /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4. * @see {@link WunderbaumNode.logDebug} */ log(...args) { if (this.tree.options.debugLevel >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4 * and browser console level includes debug/verbose messages. * @see {@link WunderbaumNode.log} */ logDebug(...args) { if (this.tree.options.debugLevel >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */ logError(...args) { if (this.tree.options.debugLevel >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */ logInfo(...args) { if (this.tree.options.debugLevel >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */ logWarn(...args) { if (this.tree.options.debugLevel >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Expand all parents and optionally scroll into visible area as neccessary. * Promise is resolved, when lazy loading and animations are done. * @param {object} [options] passed to `setExpanded()`. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} */ async makeVisible(options) { let i; const dfd = new Deferred(); const deferreds = []; const parents = this.getParentList(false, false); const len = parents.length; const noAnimation = getOption(options, "noAnimation", false); const scroll = getOption(options, "scrollIntoView", true); // Expand bottom-up, so only the top node is animated for (i = len - 1; i >= 0; i--) { // self.debug("pushexpand" + parents[i]); const seOpts = { noAnimation: noAnimation }; deferreds.push(parents[i].setExpanded(true, seOpts)); } Promise.all(deferreds).then(() => { // All expands have finished // self.debug("expand DONE", scroll); // Note: this.tree may be none when switching demo trees if (scroll && this.tree) { // Make sure markup and _rowIdx is updated before we do the scroll calculations this.tree.updatePendingModifications(); this.scrollIntoView().then(() => { // self.debug("scroll DONE"); dfd.resolve(); }); } else { dfd.resolve(); } }); return dfd.promise(); } /** Move this node to targetNode. */ moveTo(targetNode, mode = "appendChild", map) { if (mode === "over") { mode = "appendChild"; // compatible with drop region } if (mode === "prependChild") { if (targetNode.children && targetNode.children.length) { mode = "before"; targetNode = targetNode.children[0]; } else { mode = "appendChild"; } } let pos; const tree = this.tree; const prevParent = this.parent; const targetParent = mode === "appendChild" ? targetNode : targetNode.parent; if (this === targetNode) { return; } else if (!this.parent) { error("Cannot move system root"); } else if (targetParent.isDescendantOf(this)) { error("Cannot move a node to its own descendant"); } if (targetParent !== prevParent) { prevParent.triggerModifyChild("remove", this); } // Unlink this node from current parent if (this.parent.children.length === 1) { if (this.parent === targetParent) { return; // #258 } this.parent.children = this.parent.lazy ? [] : null; this.parent.expanded = false; } else { pos = this.parent.children.indexOf(this); assert(pos >= 0, "invalid source parent"); this.parent.children.splice(pos, 1); } // Insert this node to target parent's child list this.parent = targetParent; if (targetParent.hasChildren()) { switch (mode) { case "appendChild": // Append to existing target children targetParent.children.push(this); break; case "before": // Insert this node before target node pos = targetParent.children.indexOf(targetNode); assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos, 0, this); break; case "after": // Insert this node after target node pos = targetParent.children.indexOf(targetNode); assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos + 1, 0, this); break; default: error(`Invalid mode '${mode}'.`); } } else { targetParent.children = [this]; } // Let caller modify the nodes if (map) { targetNode.visit(map, true); } if (targetParent === prevParent) { targetParent.triggerModifyChild("move", this); } else { // prevParent.triggerModifyChild("remove", this); targetParent.triggerModifyChild("add", this); } // Handle cross-tree moves if (tree !== targetNode.tree) { // Fix node.tree for all source nodes // util.assert(false, "Cross-tree move is not yet implemented."); this.logWarn("Cross-tree moveTo is experimental!"); this.visit((n) => { // TODO: fix selection state and activation, ... n.tree = targetNode.tree; }, true); } // Make sure we update async, because discarding the markup would prevent // DragAndDrop to generate a dragend event on the source node setTimeout(() => { // Even indentation may have changed: tree.update(ChangeType.any); }, 0); // TODO: fix selection state // TODO: fix active state } /** Set focus relative to this node and optionally activate. * * 'left' collapses the node if it is expanded, or move to the parent * otherwise. * 'right' expands the node if it is collapsed, or move to the first * child otherwise. * * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the `event.key` that would normally trigger this move, * e.g. `ArrowLeft` = 'left'. * @param options */ async navigate(where, options) { var _a; // Allow to pass 'ArrowLeft' instead of 'left' const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where); // Otherwise activate or focus the related node const node = this.findRelatedNode(navType); if (!node) { this.logWarn(`Could not find related node '${where}'.`); return Promise.resolve(this); } // setFocus/setActive will scroll later (if autoScroll is specified) try { node.makeVisible({ scrollIntoView: false }); } catch (e) { // ignore } node.setFocus(); if ((options === null || options === void 0 ? void 0 : options.activate) === false) { return Promise.resolve(this); } return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event }); } /** Delete this node and all descendants. */ remove() { const tree = this.tree; const pos = this.parent.children.indexOf(this); this.triggerModify("remove"); this.parent.children.splice(pos, 1); this.visit((n) => { n.removeMarkup(); tree._unregisterNode(n); }, true); tree.update(ChangeType.structure); } /** Remove all descendants of this node. */ removeChildren() { var _a, _b; const tree = this.tree; if (!this.children) { return; } if ((_a = tree.activeNode) === null || _a === void 0 ? void 0 : _a.isDescendantOf(this)) { tree.activeNode.setActive(false); // TODO: don't fire events } if ((_b = tree.focusNode) === null || _b === void 0 ? void 0 : _b.isDescendantOf(this)) { tree._setFocusNode(null); } // TODO: persist must take care to clear select and expand cookies // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) this.triggerModifyChild("remove", null); this.visit((n) => { tree._unregisterNode(n); }); if (this.lazy) { // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes this.children = []; } else { this.children = null; } // util.assert(this.parent); // don't call this for root node if (!this.isRootNode()) { this.expanded = false; } this.tree.update(ChangeType.structure); } /** Remove all HTML markup from the DOM. */ removeMarkup() { if (this._rowElem) { delete this._rowElem._wb_node; this._rowElem.remove(); this._rowElem = undefined; } } _getRenderInfo() { const allColInfosById = {}; const renderColInfosById = {}; const isColspan = this.isColspan(); const colElems = this._rowElem ? (this._rowElem.querySelectorAll("span.wb-col")) : null; let idx = 0; for (const col of this.tree.columns) { allColInfosById[col.id] = { id: col.id, idx: idx, elem: colElems ? colElems[idx] : null, info: col, }; // renderColInfosById only contains columns that need rendering: if (!isColspan && col.id !== "*") { renderColInfosById[col.id] = allColInfosById[col.id]; } idx++; } return { allColInfosById: allColInfosById, renderColInfosById: renderColInfosById, }; } _createIcon(parentElem, replaceChild, showLoading) { const iconElem = this.tree._createNodeIcon(this, showLoading, true); if (iconElem) { if (replaceChild) { parentElem.replaceChild(iconElem, replaceChild); } else { parentElem.appendChild(iconElem); } } return iconElem; } /** * Create a whole new `
` element. * @see {@link WunderbaumNode._render} */ _render_markup(opts) { const tree = this.tree; const treeOptions = tree.options; const rowHeight = treeOptions.rowHeightPx; const checkbox = this.getOption("checkbox"); const columns = tree.columns; const level = this.getLevel(); const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx; let elem; let rowDiv = this._rowElem; let checkboxSpan = null; let expanderSpan = null; const isNew = !rowDiv; assert(isNew, "Expected unrendered node"); assert(!isNew || (opts && opts.after), "opts.after expected, unless updating"); assert(!this.isRootNode(), "Root node not allowed"); rowDiv = document.createElement("div"); rowDiv.classList.add("wb-row"); rowDiv.style.top = this._rowIdx * rowHeight + "px"; this._rowElem = rowDiv; // Attach a node reference to the DOM Element: rowDiv._wb_node = this; const nodeElem = document.createElement("span"); nodeElem.classList.add("wb-node", "wb-col"); rowDiv.appendChild(nodeElem); let ofsTitlePx = 0; if (checkbox) { checkboxSpan = document.createElement("i"); checkboxSpan.classList.add("wb-checkbox"); if (checkbox === "radio" || this.parent.radiogroup) { checkboxSpan.classList.add("wb-radio"); } nodeElem.appendChild(checkboxSpan); ofsTitlePx += ICON_WIDTH; } for (let i = level - 1; i > 0; i--) { elem = document.createElement("i"); elem.classList.add("wb-indent"); nodeElem.appendChild(elem); ofsTitlePx += ICON_WIDTH; } if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) { expanderSpan = document.createElement("i"); expanderSpan.classList.add("wb-expander"); nodeElem.appendChild(expanderSpan); ofsTitlePx += ICON_WIDTH; } // Render the icon (show a 'loading' icon if we do not have an expander that // we would prefer). const iconSpan = this._createIcon(nodeElem, null, !expanderSpan); if (iconSpan) { ofsTitlePx += ICON_WIDTH; } const titleSpan = document.createElement("span"); titleSpan.classList.add("wb-title"); nodeElem.appendChild(titleSpan); // this._callEvent("enhanceTitle", { titleSpan: titleSpan }); // Store the width of leading icons with the node, so we can calculate // the width of the embedded title span later nodeElem._ofsTitlePx = ofsTitlePx; // Support HTML5 drag-n-drop if (tree.options.dnd.dragStart) { nodeElem.draggable = true; } // Render columns const isColspan = this.isColspan(); if (!isColspan && columns.length > 1) { let colIdx = 0; for (const col of columns) { colIdx++; let colElem; if (col.id === "*") { colElem = nodeElem; } else { colElem = document.createElement("span"); colElem.classList.add("wb-col"); rowDiv.appendChild(colElem); } if (colIdx === activeColIdx) { colElem.classList.add("wb-active"); } // Add classes from `columns` definition to `` cells col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; if (isNew && col.html) { if (typeof col.html === "string") { colElem.innerHTML = col.html; } } } } // Attach to DOM as late as possible const after = opts ? opts.after : "last"; switch (after) { case "first": tree.nodeListElement.prepend(rowDiv); break; case "last": tree.nodeListElement.appendChild(rowDiv); break; default: opts.after.after(rowDiv); } // Now go on and fill in data and update classes opts.isNew = true; this._render_data(opts); } /** * Render `node.title`, `.icon` into an existing row. * * @see {@link WunderbaumNode._render} */ _render_data(opts) { assert(this._rowElem, "No _rowElem"); const tree = this.tree; const treeOptions = tree.options; const rowDiv = this._rowElem; const isNew = !!opts.isNew; // Called by _render_markup()? const preventScroll = !!opts.preventScroll; const columns = tree.columns; const isColspan = this.isColspan(); // Row markup already exists const nodeElem = rowDiv.querySelector("span.wb-node"); const titleSpan = nodeElem.querySelector("span.wb-title"); const scrollTop = tree.element.scrollTop; if (this.titleWithHighlight) { titleSpan.innerHTML = this.titleWithHighlight; } else { titleSpan.textContent = this.title; // TODO: this triggers scroll events } const tooltip = this.getOption("tooltip", false); if (tooltip) { titleSpan.title = tooltip === true ? this.title : tooltip; } // NOTE: At least on Safari, this render call triggers a scroll event // probably when a focused input is replaced. if (preventScroll) { tree.element.scrollTop = scrollTop; } // Set the width of the title span, so overflow ellipsis work if (!treeOptions.skeleton) { if (isColspan) { const vpWidth = tree.element.clientWidth; titleSpan.style.width = vpWidth - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } else { titleSpan.style.width = columns[0]._widthPx - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } } // Update row classes opts.isDataChange = true; this._render_status(opts); // Let user modify the result if (this.statusNodeType) { this._callEvent("renderStatusNode", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, }); } else if (this.parent) { // Skip root node const renderInfo = this._getRenderInfo(); this._callEvent("render", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, allColInfosById: renderInfo.allColInfosById, renderColInfosById: renderInfo.renderColInfosById, }); } } /** * Update row classes to reflect active, focuses, etc. * @see {@link WunderbaumNode._render} */ _render_status(opts) { // this.log("_render_status", opts); const tree = this.tree; const iconMap = tree.iconMap; const treeOptions = tree.options; const typeInfo = this.type ? tree.types[this.type] : null; const rowDiv = this._rowElem; // Row markup already exists const nodeSpan = rowDiv.querySelector("span.wb-node"); const expanderElem = nodeSpan.querySelector("i.wb-expander"); const checkboxElem = nodeSpan.querySelector("i.wb-checkbox"); const rowClasses = ["wb-row"]; this.expanded ? rowClasses.push("wb-expanded") : 0; this.lazy ? rowClasses.push("wb-lazy") : 0; this.selected ? rowClasses.push("wb-selected") : 0; this._partsel ? rowClasses.push("wb-partsel") : 0; this === tree.activeNode ? rowClasses.push("wb-active") : 0; this === tree.focusNode ? rowClasses.push("wb-focus") : 0; this._errorInfo ? rowClasses.push("wb-error") : 0; this._isLoading ? rowClasses.push("wb-loading") : 0; this.isColspan() ? rowClasses.push("wb-colspan") : 0; this.statusNodeType ? rowClasses.push("wb-status-" + this.statusNodeType) : 0; this.match ? rowClasses.push("wb-match") : 0; this.subMatchCount ? rowClasses.push("wb-submatch") : 0; treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0; // Replace previous classes: rowDiv.className = rowClasses.join(" "); // Add classes from `node.classes` this.classes ? rowDiv.classList.add(...this.classes) : 0; // Add classes from `tree.types[node.type]` if (typeInfo && typeInfo.classes) { rowDiv.classList.add(...typeInfo.classes); } if (expanderElem) { let image = null; if (this._isLoading) { image = iconMap.loading; } else if (this.isExpandable(false)) { if (this.expanded) { image = iconMap.expanderExpanded; } else { image = iconMap.expanderCollapsed; } } else if (this.lazy && this.children == null) { image = iconMap.expanderLazy; } if (image == null) { expanderElem.className = "wb-expander"; expanderElem.classList.add("wb-indent"); } else if (TEST_HTML.test(image)) { expanderElem.replaceWith(elemFromHtml(image)); } else if (TEST_FILE_PATH.test(image)) { expanderElem.style.backgroundImage = `url('${image}')`; } else { expanderElem.className = "wb-expander " + image; } } if (checkboxElem) { let cbclass = "wb-checkbox "; if (this.isRadio()) { cbclass += "wb-radio "; if (this.selected) { cbclass += iconMap.radioChecked; // } else if (this._partsel) { // cbclass += iconMap.radioUnknown; } else { cbclass += iconMap.radioUnchecked; } } else { if (this.selected) { cbclass += iconMap.checkChecked; } else if (this._partsel) { cbclass += iconMap.checkUnknown; } else { cbclass += iconMap.checkUnchecked; } } checkboxElem.className = cbclass; } // Fix active cell in cell-nav mode if (!opts.isNew) { let i = 0; for (const colSpan of rowDiv.children) { colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx); colSpan.classList.remove("wb-error", "wb-invalid"); } // Update icon (if not opts.isNew, which would rebuild markup anyway) const iconSpan = nodeSpan.querySelector("i.wb-icon"); if (iconSpan) { this._createIcon(nodeSpan, iconSpan, !expanderElem); } } // Adjust column width if (opts.resizeCols !== false && !this.isColspan()) { const colElems = rowDiv.querySelectorAll("span.wb-col"); let idx = 0; let ofs = 0; for (const colDef of this.tree.columns) { const colElem = colElems[idx]; colElem.style.left = `${ofs}px`; colElem.style.width = `${colDef._widthPx}px`; idx++; ofs += colDef._widthPx; } } } /* * Create or update node's markup. * * `options.change` defaults to ChangeType.data, which updates the title, * icon, and status. It also triggers the `render` event, that lets the user * create or update the content of embeded cell elements. * * If only the status or other class-only modifications have changed, * `options.change` should be set to ChangeType.status instead for best * efficiency. * * Calling `update()` is almost always a better alternative. * @see {@link WunderbaumNode.update} */ _render(options) { // this.log("render", options); const opts = Object.assign({ change: ChangeType.data }, options); if (!this._rowElem) { opts.change = ChangeType.row; } switch (opts.change) { case "status": this._render_status(opts); break; case "data": this._render_data(opts); break; case "row": // _rowElem is not yet created (asserted in _render_markup) this._render_markup(opts); break; default: error(`Invalid change type '${opts.change}'.`); } } /** * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad * event is triggered on next expand. */ resetLazy() { this.removeChildren(); this.expanded = false; this.lazy = true; this.children = null; this.tree.update(ChangeType.structure); } /** Convert node (or whole branch) into a plain object. * * The result is compatible with node.addChildren(). * * @param recursive include child nodes * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link Wunderbaum.toDictArray}. */ toDict(recursive = false, callback) { const dict = {}; NODE_DICT_PROPS.forEach((propName) => { const val = this[propName]; if (val instanceof Set) { // Convert Set to string (or skip if set is empty) val.size ? (dict[propName] = Array.prototype.join.call(val.keys(), " ")) : 0; } else if (val || val === false || val === 0) { dict[propName] = val; } }); if (!isEmptyObject(this.data)) { dict.data = extend({}, this.data); if (isEmptyObject(dict.data)) { delete dict.data; } } if (callback) { const res = callback(dict, this); if (res === false) { // Note: a return value of `false` is only used internally return false; // Don't include this node nor its children } if (res === "skip") { recursive = false; // Include this node, but not the children } } if (recursive) { if (isArray(this.children)) { dict.children = []; for (let i = 0, l = this.children.length; i < l; i++) { const node = this.children[i]; if (!node.isStatusNode()) { // Note: a return value of `false` is only used internally const res = node.toDict(true, callback); if (res !== false) { dict.children.push(res); } } } } } return dict; } /** Return an option value that has a default, but may be overridden by a * callback or a node instance attribute. * * Evaluation sequence: * * - If `tree.options.` is a callback that returns something, use that. * - Else if `node.` is defined, use that. * - Else if `tree.types[]` is a value, use that. * - Else if `tree.options.` is a value, use that. * - Else use `defaultValue`. * * @param name name of the option property (on node and tree) * @param defaultValue return this if nothing else matched * {@link Wunderbaum.getOption|Wunderbaum.getOption} */ getOption(name, defaultValue) { const tree = this.tree; let opts = tree.options; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [opts, name] = name.split("."); } const value = opts[name]; // ?? defaultValue; // A callback resolver always takes precedence if (typeof value === "function") { const res = value.call(tree, { type: "resolve", tree: tree, node: this, // typeInfo: this.type ? tree.types[this.type] : {}, }); if (res !== undefined) { return res; } } // If this node has an explicit local setting, use it: if (this[name] !== undefined) { return this[name]; } // Use value from type definition if defined const typeInfo = this.type ? tree.types[this.type] : undefined; const res = typeInfo ? typeInfo[name] : undefined; if (res !== undefined) { return res; } // Use value from value options dict, fallback do default return value !== null && value !== void 0 ? value : defaultValue; } /** Make sure that this node is visible in the viewport. * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo} */ async scrollIntoView(options) { const opts = Object.assign({ node: this }, options); return this.tree.scrollTo(opts); } /** * Activate this node, deactivate previous, send events, activate column and * scroll into viewport. */ async setActive(flag = true, options) { const tree = this.tree; const prev = tree.getActiveNode(); const retrigger = options === null || options === void 0 ? void 0 : options.retrigger; // Default: false const focusTree = options === null || options === void 0 ? void 0 : options.focusTree; // Default: false // const focusNode = options?.focusNode !== false; // Default: true const noEvents = options === null || options === void 0 ? void 0 : options.noEvents; // Default: false const orgEvent = options === null || options === void 0 ? void 0 : options.event; // Default: null const colIdx = options === null || options === void 0 ? void 0 : options.colIdx; // Default: null const edit = options === null || options === void 0 ? void 0 : options.edit; // Default: false // util.assert(!colIdx || tree.isCellNav(), "colIdx requires cellNav"); assert(!edit || colIdx != null, "edit requires colIdx"); if (!noEvents) { if (flag) { if (prev !== this || retrigger) { if ((prev === null || prev === void 0 ? void 0 : prev._callEvent("deactivate", { nextNode: this, event: orgEvent, })) === false || this._callEvent("beforeActivate", { prevNode: prev, event: orgEvent, }) === false) { return; } tree._setActiveNode(null); prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status); } } else if (prev === this || retrigger) { this._callEvent("deactivate", { nextNode: null, event: orgEvent }); } } if (prev !== this) { if (flag) { tree._setActiveNode(this); } prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status); this.update(ChangeType.status); } return this.makeVisible().then(() => { if (flag) { if (focusTree || edit) { tree.setFocus(); tree._setFocusNode(this); tree.focusNode.setFocus(); } // if (focusNode || edit) { // tree.focusNode = this; // tree.focusNode.setFocus(); // } if (colIdx != null && tree.isCellNav()) { tree.setColumn(colIdx, { edit: edit }); } if (!noEvents) { this._callEvent("activate", { prevNode: prev, event: orgEvent }); } } }); } /** * Expand or collapse this node. */ async setExpanded(flag = true, options) { const { force, scrollIntoView, immediate, resetLazy } = options !== null && options !== void 0 ? options : {}; const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events if (!flag && this.isExpanded() && this.getLevel() <= this.tree.getOption("minExpandLevel") && !force) { this.logDebug("Ignored collapse request below minExpandLevel."); return; } if (!flag === !this.expanded) { return; // Nothing to do } if (sendEvents && this._callEvent("beforeExpand", { flag: flag }) === false) { return; } // this.log("setExpanded()"); if (flag && this.getOption("autoCollapse")) { this.collapseSiblings(options); } if (flag && this.lazy && this.children == null) { await this.loadLazy(); } else if (!flag && resetLazy && this.lazy && this.children) { this.resetLazy(); } this.expanded = flag; const updateOpts = { immediate: immediate }; // const updateOpts = { immediate: !!util.getOption(options, "immediate") }; this.tree.update(ChangeType.structure, updateOpts); if (flag && scrollIntoView) { const lastChild = this.getLastChild(); if (lastChild) { this.tree.updatePendingModifications(); lastChild.scrollIntoView({ topNode: this }); } } if (sendEvents) { this._callEvent("expand", { flag: flag }); } } /** * Set keyboard focus here. * @see {@link setActive} */ setFocus(flag = true) { assert(!!flag, "Blur is not yet implemented"); const prev = this.tree.focusNode; this.tree._setFocusNode(this); prev === null || prev === void 0 ? void 0 : prev.update(); this.update(); } /** Set a new icon path or class. */ setIcon(icon) { this.icon = icon; this.update(); } /** Change node's {@link key} and/or {@link refKey}. */ setKey(key, refKey) { throw new Error("Not yet implemented"); } // /** // * Calculate a *stable*, unique key for this node from its refKey (or title). // * We also add information from the parent, because a refKey may occur multiple // * times in a tree. // */ // calcUniqueKey() { // // Assuming that the parent's key was calculated the same way, we implicitly // // involve the whole refKey-path: // const s = this.key + (this.refKey || this.title); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // const h1 = util.murmurHash3(s, true); // return "id_" + h1 + util.murmurHash3(h1 + s, true); // // const l = []; // // // eslint-disable-next-line @typescript-eslint/no-this-alias // // let node: WunderbaumNode = this; // // while (node.parent) { // // l.unshift(node.refKey || node.key); // // node = node.parent; // // } // // const path = l.join("/"); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // // const h1 = util.murmurHash3(path, true); // // return "id_" + h1 + util.murmurHash3(h1 + path, true); // } /** * Trigger a repaint, typically after a status or data change. * * `change` defaults to 'data', which handles modifcations of title, icon, * and column content. It can be reduced to 'ChangeType.status' if only * active/focus/selected state has changed. * * This method will eventually call {@link WunderbaumNode._render} with * default options, but may be more consistent with the tree's * {@link Wunderbaum.update} API. */ update(change = ChangeType.data) { assert(change === ChangeType.status || change === ChangeType.data, `Invalid change type ${change}`); this.tree.update(change, this); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents = false) { const nodeList = []; this.visit((node) => { if (node.selected) { nodeList.push(node); if (stopOnParents === true) { return "skip"; // stop processing this branch } } }); return nodeList; } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false) { const refKeys = new Set(); this.visit((node) => { if (node.refKey != null && (!selected || node.selected)) { refKeys.add(node.refKey); } }); return Array.from(refKeys); } /** Toggle the check/uncheck state. */ toggleSelected(options) { let flag = this.isSelected(); if (flag === undefined && !this.isRadio()) { flag = this._anySelectable(); } else { flag = !flag; } return this.setSelected(flag, options); } /** Return true if at least on selectable descendant end-node is unselected. @internal */ _anySelectable() { let found = false; this.visit((node) => { if (node.selected === false && !node.unselectable && !node.hasChildren() && !node.parent.radiogroup) { found = true; return false; // Stop iteration } }); return found; } /* Apply selection state to a single node. */ _changeSelectStatusProps(state) { let changed = false; switch (state) { case false: changed = this.selected || this._partsel; this.selected = false; this._partsel = false; break; case true: changed = !this.selected || !this._partsel; this.selected = true; this._partsel = true; break; case undefined: changed = this.selected || !this._partsel; this.selected = false; // #110: end nodess cannot have a `_partsel` flag this._partsel = this.hasChildren() ? true : false; break; default: error(`Invalid state: ${state}`); } if (changed) { this.update(); } return changed; } /** * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`. * This includes (de)selecting all descendants. */ fixSelection3AfterClick(opts) { const force = !!(opts === null || opts === void 0 ? void 0 : opts.force); const flag = this.isSelected(); this.visit((node) => { if (node.radiogroup) { return "skip"; // Don't (de)select this branch } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(flag); } }); this.fixSelection3FromEndNodes(); } /** * Fix selection status for multi-hier mode. * Only end-nodes are considered to update the descendants branch and parents. * Should be called after this node has loaded new children or after * children have been modified using the API. */ fixSelection3FromEndNodes(opts) { const force = !!(opts === null || opts === void 0 ? void 0 : opts.force); assert(this.tree.options.selectMode === "hier", "expected selectMode 'hier'"); // Visit all end nodes and adjust their parent's `selected` and `_partsel` // attributes. Return selection state true, false, or undefined. const _walk = (node) => { let state; const children = node.children; if (children && children.length) { // check all children recursively let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; // the selection state of a node is not relevant; we need the end-nodes const s = _walk(child); if (s !== false) { someSelected = true; } if (s !== true) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; } else { // This is an end-node: simply report the status state = !!node.selected; } // #939: Keep a `_partsel` flag that was explicitly set on a lazy node if (node._partsel && !node.selected && node.lazy && node.children == null) { state = undefined; } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(state); } return state; }; _walk(this); // Update parent's state this.visitParents((node) => { let state; const children = node.children; let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; state = !!child.selected; // When fixing the parents, we trust the sibling status (i.e. we don't recurse) if (state || child._partsel) { someSelected = true; } if (!state) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; node._changeSelectStatusProps(state); }); } /** Modify the check/uncheck state. */ setSelected(flag = true, options) { const tree = this.tree; const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events const prev = this.isSelected(); const isRadio = this.parent && this.parent.radiogroup; const selectMode = tree.options.selectMode; const canSelect = (options === null || options === void 0 ? void 0 : options.force) || !this.getOption("unselectable"); flag = !!flag; // this.logDebug(`setSelected(${flag})`, this); if (!canSelect) { return prev; } if ((options === null || options === void 0 ? void 0 : options.propagateDown) && selectMode === "multi") { tree.runWithDeferredUpdate(() => { this.visit((node) => { node.setSelected(flag); }); }); return prev; } if (flag === prev || (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false)) { return prev; } tree.runWithDeferredUpdate(() => { if (isRadio) { // Radiobutton Group if (!flag && !(options === null || options === void 0 ? void 0 : options.force)) { return prev; // don't uncheck radio buttons } for (const sibling of this.parent.children) { sibling.selected = sibling === this; } } else { this.selected = flag; if (selectMode === "hier") { this.fixSelection3AfterClick(); } else if (selectMode === "single" && flag) { tree.visit((n) => { if (n !== this) { n.selected = false; } }); } } }); if (sendEvents) { this._callEvent("select", { flag: flag }); } return prev; } /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */ setStatus(status, options) { const tree = this.tree; const message = options === null || options === void 0 ? void 0 : options.message; const details = options === null || options === void 0 ? void 0 : options.details; let statusNode = null; const _clearStatusNode = () => { // Remove dedicated dummy node, if any const children = this.children; if (children && children.length && children[0].isStatusNode()) { children[0].remove(); } }; const _setStatusNode = (data) => { // Create/modify the dedicated dummy node for 'loading...' or // 'error!' status. (only called for direct child of the invisible // system root) const children = this.children; const firstChild = children ? children[0] : null; assert(data.statusNodeType, "Not a status node"); assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node"); statusNode = this.addNode(data, "prependChild"); statusNode.match = -1; // Mark as 'match' to avoid hiding tree.update(ChangeType.structure); return statusNode; }; _clearStatusNode(); switch (status) { case "ok": this._isLoading = false; this._errorInfo = null; break; case "loading": this._isLoading = true; this._errorInfo = null; if (this.parent) { this.update(ChangeType.status); } else { // If this is the invisible root, add a visible top-level node _setStatusNode({ statusNodeType: status, title: tree.options.strings.loading + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, tooltip: details, }); } // this.update(); break; case "error": _setStatusNode({ statusNodeType: status, title: tree.options.strings.loadError + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, // classes: "wb-center", tooltip: details, }); this._isLoading = false; this._errorInfo = { message: message, details: details }; break; case "noData": _setStatusNode({ statusNodeType: status, title: message || tree.options.strings.noData, checkbox: false, colspan: true, tooltip: details, }); this._isLoading = false; this._errorInfo = null; break; default: error("invalid node status " + status); } tree.update(ChangeType.structure); return statusNode; } /** Rename this node. */ setTitle(title) { this.title = title; this.update(); // this.triggerModify("rename"); // TODO } /** Set the node tooltip. */ setTooltip(tooltip) { this.tooltip = tooltip; this.update(); } /** * Sort child list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp = nodeTitleSorter, deep = false) { this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep }); } /** * Renumber nodes `_nativeIndex`. This is useful to allow to restore the * order after sorting a column. * This method is automatically called after loading new child nodes. * @since 0.11.0 */ resetNativeChildOrder(options) { const { recursive = true, propName = "_nativeIndex" } = options !== null && options !== void 0 ? options : {}; if (this.children) { this.children.forEach((child, i) => { child.data[propName] = i; if (recursive && child.children) { child.resetNativeChildOrder(options); } }); } } /** * Convenience method to implement column sorting. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options) { this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" }); return this.sort(options); } /** * Implement column sorting. * @since 0.14.0 */ sort(options) { const tree = this.tree; let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined, // Support click on column sort header: updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options; propName !== null && propName !== void 0 ? propName : (propName = colId); if (propName === "*") { propName = "title"; } const isFolder = tree.options.sortFoldersFirst === true ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER : tree.options.sortFoldersFirst; if (updateColInfo) { const colDef = this.tree["_columnsById"][options.colId]; assert(colDef, `Invalid colId specified: ${options.colId}`); order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined])); for (const col of this.tree.columns) { col.sortOrder = col === colDef ? order : undefined; } if (order === undefined) { propName = nativeOrderPropName; order = "asc"; } this.tree.update(ChangeType.colStructure); } else { propName !== null && propName !== void 0 ? propName : (propName = "title"); order !== null && order !== void 0 ? order : (order = "asc"); } this.logDebug(`sort(), propName=${propName}, ${order}`, options); assert(propName || cmp || key, "No `propName` or `key` specified"); // Define a key callback from the parameters we have if (key == null && cmp == null) { key = (node) => { let val; if (NODE_DICT_PROPS.has(propName)) { val = node[propName]; } else { val = node.data[propName]; } if (caseInsensitive && typeof val === "string") { val = val.toLowerCase(); } return val; }; } // Define a compare callback that uses the key callback if (cmp) { assert(!key, "`key` and `cmp` are mutually exclusive"); tree.logDeprecate("SortOptions.cmp", { since: "0.14.0", hint: "use the `key` callback instead", }); } else { if (options.propName || options.caseInsensitive) { tree.logWarn("sort(): ignoring propName, caseInsensitive"); } cmp = (a, b) => { if (isFolder) { const isFolderA = isFolder(a); if (isFolderA !== isFolder(b)) { return isFolderA ? -1 : 1; } } let x = key(a); let y = key(b); // Assure we have reasonable comparisons with null values: if (x == null) { x = typeof y === "string" ? "" : 0; } else if (typeof x === "boolean") { x = x ? 1 : 0; } if (y == null) { y = typeof x === "string" ? "" : 0; } else if (typeof y === "boolean") { y = y ? 1 : 0; } if (order === "desc") { return x === y ? 0 : x > y ? -1 : 1; } return x === y ? 0 : x > y ? 1 : -1; }; } function _sortChildren(cl) { if (!cl) { return; } cl.sort(cmp); if (deep) { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].children) { _sortChildren(cl[i].children); } } } } if (this.children) { _sortChildren(this.children); } this.tree.update(ChangeType.structure); // this.triggerModify("sort"); // TODO } /** * Re-apply current sorting if any (use after lazy load). * Example: * ```js * load: function (e) { * // Whe loading a lazy branch, apply current sort order if any * e.node.resort(); * }, * ``` * @since 0.14.0 */ resort(options = {}) { for (const colDef of this.tree.columns) { if (colDef.sortOrder) { options.colId = colDef.id; options.order = colDef.sortOrder; this.sort(options); break; } } } /** * Trigger `modifyChild` event on a parent to signal that a child was modified. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ triggerModifyChild(operation, child, extra) { this.logDebug(`modifyChild(${operation})`, extra, child); if (!this.tree.options.modifyChild) { return; } if (child && child.parent !== this) { error("child " + child + " is not a child of " + this); } this._callEvent("modifyChild", extend({ operation: operation, child: child }, extra)); } /** * Trigger `modifyChild` event on node.parent(!). * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... * @param {object} [extra] */ triggerModify(operation, extra) { // if (!this.parent) { // return; // } this.parent.triggerModifyChild(operation, this, extra); } /** * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order). * * Stop iteration, if fn() returns false. Skip current branch, if fn() * returns "skip".
* Return false if iteration was stopped. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * its children only. * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link Wunderbaum.visit}. */ visit(callback, includeSelf = false) { let res = true; const children = this.children; if (includeSelf === true) { res = callback(this); if (res === false || res === "skip") { return res; } } if (children) { for (let i = 0, l = children.length; i < l; i++) { res = children[i].visit(callback, true); if (res === false) { break; } } } return res; } /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.
* Stop iteration, if callback() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. Return false to stop iteration */ visitParents(callback, includeSelf = false) { if (includeSelf && callback(this) === false) { return false; } let p = this.parent; while (p) { if (callback(p) === false) { return false; } p = p.parent; } return true; } /** * Call fn(node) for all sibling nodes.
* Stop iteration, if fn() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. * Return false to stop iteration. * @param includeSelf include this node in the iteration. */ visitSiblings(callback, includeSelf = false) { const ac = this.parent.children; for (let i = 0, l = ac.length; i < l; i++) { const n = ac[i]; if (includeSelf || n !== this) { if (callback(n) === false) { return false; } } } return true; } /** * [ext-filter] Return true if this node is matched by current filter (or no filter is active). */ isMatched() { return !(this.tree.filterMode && !this.match); } } WunderbaumNode.sequence = 0; /*! * Wunderbaum - ext-edit * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ // const START_MARKER = "\uFFF7"; class EditExtension extends WunderbaumExtension { constructor(tree) { super(tree, "edit", { debounce: 100, minlength: 1, maxlength: null, trigger: [], //["clickActive", "F2", "macEnter"], trim: true, select: true, slowClickDelay: 1000, // Handle 'clickActive' only if last click is less than this old (0: always) validity: true, //"Please enter a title", // --- Events --- // (note: there is also the `tree.change` event.) beforeEdit: null, edit: null, apply: null, }); this.curEditNode = null; this.relatedNode = null; this.debouncedOnChange = debounce(this._onChange.bind(this), this.getPluginOption("debounce")); } /* * Call an event handler, while marking the current node cell 'busy'. * Deal with returned promises and ValidationError. * Convert a ValidationError into a input.setCustomValidity() call and vice versa. */ async _applyChange(eventName, node, colElem, inputElem, extra) { node.log(`_applyChange(${eventName})`, extra); colElem.classList.add("wb-busy"); colElem.classList.remove("wb-error", "wb-invalid"); inputElem.setCustomValidity(""); // Call event handler either ('change' or 'edit.appy'), which may return a // promise or a scalar value or throw a ValidationError. return new Promise((resolve, reject) => { const res = node._callEvent(eventName, extra); // normalize to promise, even if a scalar value was returned and await it Promise.resolve(res) .then((res) => { resolve(res); }) .catch((err) => { reject(err); }); }) .then((res) => { if (!inputElem.checkValidity()) { // Native validation failed or handler called 'inputElem.setCustomValidity()' node.logWarn("inputElem.checkValidity() failed: throwing...."); throw new ValidationError(inputElem.validationMessage); } return res; }) .catch((err) => { if (err instanceof ValidationError) { node.logWarn("catched ", err); colElem.classList.add("wb-invalid"); if (inputElem.setCustomValidity && !inputElem.validationMessage) { inputElem.setCustomValidity(err.message); } if (inputElem.validationMessage) { inputElem.reportValidity(); } // throw err; } else { node.logError(`Error in ${eventName} event handler (throw e.util.ValidationError instead if this was intended)`, err); colElem.classList.add("wb-error"); throw err; } }) .finally(() => { colElem.classList.remove("wb-busy"); }); } /* * Called for when a control that is embedded in a cell fires a `change` event. */ _onChange(e) { const info = Wunderbaum.getEventInfo(e); const node = info.node; const colElem = info.colElem; if (!node || info.colIdx === 0) { this.tree.log("Ignored change event for removed element or node title"); return; } // See also WbChangeEventType this._applyChange("change", node, colElem, e.target, { info: info, event: e, inputElem: e.target, inputValue: Wunderbaum.util.getValueFromElem(e.target), inputValid: e.target.checkValidity(), }); } init() { super.init(); onEvent(this.tree.element, "change", //"change input", ".contenteditable,input,textarea,select", // #61: we must not debounce the `change`, event.target may be reset to null // when the debounced handler is called. // (e) => { // this.debouncedOnChange(e); // } (e) => this._onChange(e)); } /* Called by ext_keynav to pre-process input. */ _preprocessKeyEvent(data) { const event = data.event; const eventName = eventToString(event); const tree = this.tree; const trigger = this.getPluginOption("trigger"); // const inputElem = // event.target && event.target.closest("input,[contenteditable]"); // tree.logDebug(`_preprocessKeyEvent: ${eventName}, editing:${this.isEditingTitle()}`); // --- Title editing: apply/discard --- // if (inputElem) { if (this.isEditingTitle()) { switch (eventName) { case "Enter": this._stopEditTitle(true, { event: event }); return false; case "Escape": this._stopEditTitle(false, { event: event }); return false; } // If the event target is an input element or `contenteditable="true"`, // we ignore it as navigation command return false; } // --- Trigger title editing if (tree.isRowNav() || tree.activeColIdx === 0) { switch (eventName) { case "Enter": if (trigger.indexOf("macEnter") >= 0 && isMac) { this.startEditTitle(); return false; } break; case "F2": if (trigger.indexOf("F2") >= 0) { this.startEditTitle(); return false; } break; } return true; } return true; } /** Return true if a title is currently being edited. */ isEditingTitle(node) { return node ? this.curEditNode === node : !!this.curEditNode; } /** Start renaming, i.e. replace the title with an embedded ``. */ startEditTitle(node) { node = node !== null && node !== void 0 ? node : this.tree.getActiveNode(); const validity = this.getPluginOption("validity"); const select = this.getPluginOption("select"); if (!node) { return; } if (node.isStatusNode()) { node.logWarn("Cannot edit status node."); return; } this.tree.logDebug(`startEditTitle(node=${node})`); let inputHtml = node._callEvent("edit.beforeEdit"); if (inputHtml === false) { node.logDebug("beforeEdit canceled operation."); return; } // `beforeEdit(e)` may return an input HTML string. Otherwise use a default // (we also treat a `true` return value as 'use default'): if (inputHtml === true || !inputHtml) { const title = escapeHtml(node.title); let opt = this.getPluginOption("maxlength"); const maxlength = opt ? ` maxlength="${opt}"` : ""; opt = this.getPluginOption("minlength"); const minlength = opt ? ` minlength="${opt}"` : ""; const required = opt > 0 ? " required" : ""; inputHtml = ``; } const titleSpan = node .getColElem(0) .querySelector(".wb-title"); titleSpan.innerHTML = inputHtml; const inputElem = titleSpan.firstElementChild; if (validity) { // Permanently apply input validations (CSS and tooltip) inputElem.addEventListener("keydown", (e) => { inputElem.setCustomValidity(""); if (!inputElem.reportValidity()) { node.logWarn(`Invalid input: '${inputElem.value}'`); } }); } inputElem.focus(); if (select) { inputElem.select(); } this.curEditNode = node; node._callEvent("edit.edit", { inputElem: inputElem, }); } /** * * @param apply * @returns */ stopEditTitle(apply) { return this._stopEditTitle(apply, {}); } /* * * @param apply * @param opts.canKeepOpen */ _stopEditTitle(apply, options) { var _a; options !== null && options !== void 0 ? options : (options = {}); const focusElem = document.activeElement; let newValue = focusElem ? getValueFromElem(focusElem) : null; const node = this.curEditNode; const forceClose = !!options.forceClose; const validity = this.getPluginOption("validity"); if (newValue && this.getPluginOption("trim")) { newValue = newValue.trim(); } if (!node) { // this.tree.logDebug("stopEditTitle: not in edit mode."); return; } node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue); if (apply && newValue !== null && newValue !== node.title) { const errMsg = focusElem.validationMessage; if (errMsg) { // input element's native validation failed throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`); } const colElem = node.getColElem(0); this._applyChange("edit.apply", node, colElem, focusElem, { oldValue: node.title, newValue: newValue, inputElem: focusElem, inputValid: focusElem.checkValidity(), }).then((value) => { var _a; const errMsg = focusElem.validationMessage; if (validity && errMsg && value !== false) { // Handler called 'inputElem.setCustomValidity()' to signal error throw new Error(`Edit apply validation failed for "${newValue}": ${errMsg}.`); } // Discard the embedded `` // node.logDebug("applyChange:", value, forceClose) if (!forceClose && value === false) { // Keep open return; } node === null || node === void 0 ? void 0 : node.setTitle(newValue); // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; this.tree.setFocus(); // restore focus that was in the input element }); // .catch((err) => { // node.logError(err); // }); // Trigger 'change' event for embedded `` // focusElem.blur(); } else { // Discard the embedded `` // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; // We discarded the , so we have to acquire keyboard focus again this.tree.setFocus(); } } /** * Create a new child or sibling node and start edit mode. */ createNode(mode = "after", node, init) { const tree = this.tree; node = node !== null && node !== void 0 ? node : tree.getActiveNode(); assert(node, "No node was passed, or no node is currently active."); // const validity = this.getPluginOption("validity"); mode = mode || "prependChild"; if (init == null) { init = { title: "" }; } else if (typeof init === "string") { init = { title: init }; } else { assert(isPlainObject(init), `Expected a plain object: ${init}`); } // Make sure node is expanded (and loaded) in 'child' mode if ((mode === "prependChild" || mode === "appendChild") && (node === null || node === void 0 ? void 0 : node.isExpandable(true))) { node.setExpanded().then(() => { this.createNode(mode, node, init); }); return; } const newNode = node.addNode(init, mode); newNode.setClass("wb-edit-new"); this.relatedNode = node; // Don't filter new nodes: newNode.match = -1; newNode.makeVisible({ noAnimation: true }).then(() => { this.startEditTitle(newNode); }); } } /*! * wunderbaum.ts * * A treegrid control. * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). * https://github.com/mar10/wunderbaum * * Released under the MIT license. * @version v0.14.1 * @date Sun, 22 Mar 2026 05:52:05 GMT */ // import "./wunderbaum.scss"; class WbSystemRoot extends WunderbaumNode { constructor(tree) { super(tree, null, { key: "__root__", title: tree.id, }); } toString() { return `WbSystemRoot@${this.key}<'${this.tree.id}'>`; } } /** * A persistent plain object or array. * * See also {@link WunderbaumOptions}. */ class Wunderbaum { /** Currently active node if any. * Use {@link WunderbaumNode.setActive|setActive} to modify. */ get activeNode() { var _a; // Check for deleted node, i.e. node.tree === null return ((_a = this._activeNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._activeNode : null; } /** Current node hat has keyboard focus if any. * Use {@link WunderbaumNode.setFocus|setFocus()} to modify. */ get focusNode() { var _a; // Check for deleted node, i.e. node.tree === null return ((_a = this._focusNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._focusNode : null; } constructor(options) { this.enabled = true; /** Contains additional data that was sent as response to an Ajax source load request. */ this.data = {}; this.extensionList = []; this.extensions = {}; this.keyMap = new Map(); this.refKeyMap = new Map(); this.treeRowCount = 0; this._disableUpdateCount = 0; this._disableUpdateIgnoreCount = 0; this._activeNode = null; this._focusNode = null; this._initialSource = null; /** Shared properties, referenced by `node.type`. */ this.types = {}; /** List of column definitions. */ this.columns = []; this._columnsById = {}; // Modification Status this.pendingChangeTypes = new Set(); /** Expose some useful methods of the util.ts module as `tree._util`. */ this._util = util; // --- SELECT --- // public selectRangeAnchor: WunderbaumNode | null = null; // --- BREADCRUMB --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ this.breadcrumb = null; // --- FILTER --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ this.filterMode = null; // --- KEYNAV --- /** @internal Use `setColumn()`/`getActiveColElem()` to access. */ this.activeColIdx = 0; /** @internal */ this._cellNavMode = false; /** @internal */ this.lastQuicksearchTime = 0; /** @internal */ this.lastQuicksearchTerm = ""; // --- EDIT --- this.lastClickTime = 0; // Set default options and merge with user options const initOptions = Object.assign({ id: undefined, source: [], // URL for GET/PUT, Ajax options, or callback element: unsafeCast(null), debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose header: null, // Show/hide header (pass bool or string) rowHeightPx: DEFAULT_ROW_HEIGHT, iconMap: "bootstrap", columns: [], //util.unsafeCast(null), types: {}, enabled: true, fixedCol: false, showSpinner: false, checkbox: false, minExpandLevel: 0, emptyChildListExpandable: false, skeleton: false, autoCollapse: false, adjustHeight: true, connectTopBreadcrumb: null, columnsFilterable: false, columnsMenu: false, columnsResizable: false, columnsSortable: false, selectMode: "multi", // SelectModeType scrollIntoViewOnExpandClick: true, // --- Extensions (actually set by exensions on init) dnd: unsafeCast(null), edit: unsafeCast(null), filter: unsafeCast(null), // --- KeyNav --- navigationModeOption: unsafeCast(null), quicksearch: true, // --- Events --- // iconBadge: null, // change: null, // ... // --- Strings --- strings: { loadError: "Error", loading: "Loading...", noData: "No data", breadcrumbDelimiter: " » ", queryResult: "Found ${matches} of ${count}", noMatch: "No results", matchIndex: "${match} of ${matches}", }, }, options); const opts = initOptions; this.options = opts; const readyDeferred = new Deferred(); this.ready = readyDeferred.promise(); let readyOk = false; this.ready .then(() => { readyOk = true; try { this._callEvent("init"); } catch (error) { // We re-raise in the reject handler, but Chrome resets the stack // frame then, so we log it here: this.logError("Exception inside `init(e)` event:", error); } }) .catch((err) => { if (readyOk) { // Error occurred in `init` handler. We can re-raise, but Chrome // resets the stack frame. throw err; } else { // Error in load process this._callEvent("init", { error: err }); } }); this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence; delete initOptions.id; this.root = new WbSystemRoot(this); this._registerExtension(new KeynavExtension(this)); this._registerExtension(new EditExtension(this)); this._registerExtension(new FilterExtension(this)); this._registerExtension(new DndExtension(this)); this._registerExtension(new GridExtension(this)); this._registerExtension(new LoggerExtension(this)); this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {}); // --- Evaluate options this.columns = initOptions.columns || []; delete initOptions.columns; if (!this.columns || !this.columns.length) { const title = typeof opts.header === "string" ? opts.header : this.id; this.columns = [{ id: "*", title: title, width: "*" }]; } if (initOptions.types) { this.setTypes(initOptions.types, true); } delete initOptions.types; // --- Create Markup this.element = elemFromSelector(initOptions.element); assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`); delete initOptions.element; this.element.classList.add("wunderbaum"); if (!this.element.getAttribute("tabindex")) { this.element.tabIndex = 0; } if (opts.rowHeightPx !== DEFAULT_ROW_HEIGHT) { this.element.style.setProperty("--wb-row-outer-height", opts.rowHeightPx + "px"); this.element.style.setProperty("--wb-row-inner-height", opts.rowHeightPx - 2 + "px"); } // Attach tree instance to
this.element._wb_tree = this; // Create header markup, or take it from the existing html this.headerElement = this.element.querySelector("div.wb-header"); const wantHeader = opts.header == null ? this.columns.length > 1 : !!opts.header; if (this.headerElement) { // User existing header markup to define `this.columns` assert(!this.columns, "`opts.columns` must not be set if table markup already contains a header"); this.columns = []; const rowElement = this.headerElement.querySelector("div.wb-row"); for (const colDiv of rowElement.querySelectorAll("div")) { this.columns.push({ id: colDiv.dataset.id || `col_${this.columns.length}`, // id: colDiv.dataset.id || null, title: "" + colDiv.textContent, // text: "" + colDiv.textContent, width: "*", // TODO: read from header span }); } } else { // We need a row div, the rest will be computed from `this.columns` const coldivs = "".repeat(this.columns.length); this.element.innerHTML = `
${coldivs}
`; if (!wantHeader) { const he = this.element.querySelector("div.wb-header"); he.style.display = "none"; } } // this.element.innerHTML += `
`; this.listContainerElement = this.element.querySelector("div.wb-list-container"); this.nodeListElement = this.listContainerElement.querySelector("div.wb-node-list"); this.headerElement = this.element.querySelector("div.wb-header"); this.element.classList.toggle("wb-grid", this.columns.length > 1); if (this.options.connectTopBreadcrumb) { this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb); assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`); this.breadcrumb.addEventListener("click", (e) => { // const node = Wunderbaum.getNode(e)!; const elem = e.target; if (elem && elem.matches("a.wb-breadcrumb")) { const node = this.keyMap.get(elem.dataset.key); node === null || node === void 0 ? void 0 : node.setActive(); e.preventDefault(); } }); } this._initExtensions(); // --- apply initial options ["enabled", "fixedCol"].forEach((optName) => { if (opts[optName] != null) { this.setOption(optName, opts[optName]); } }); // --- Load initial data if (initOptions.source) { if (opts.showSpinner) { this.nodeListElement.innerHTML = `${opts.strings.loading}`; } this.load(initOptions.source) .then(() => { // The source may have defined columns, so we may adjust the nav mode if (opts.navigationModeOption == null) { if (this.isGrid()) { this.setNavigationOption(NavModeEnum.cell); } else { this.setNavigationOption(NavModeEnum.row); } } else { this.setNavigationOption(opts.navigationModeOption); } this.update(ChangeType.structure, { immediate: true }); readyDeferred.resolve(); }) .catch((error) => { readyDeferred.reject(error); }) .finally(() => { var _a; (_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove(); this.element.classList.remove("wb-initializing"); }); } else { readyDeferred.resolve(); } // Async mode is sometimes required, because this.element.clientWidth // has a wrong value at start??? this.update(ChangeType.any); // --- Bind listeners this._registerEventHandlers(); this.resizeObserver = new ResizeObserver((entries) => { // this.log("ResizeObserver: Size changed", entries); this.update(ChangeType.resize); }); this.resizeObserver.observe(this.element); } _registerEventHandlers() { this.element.addEventListener("scroll", (e) => { // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e); this.update(ChangeType.scroll); }); onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => { var _a, _b; const info = Wunderbaum.getEventInfo(e); const command = (_b = (_a = e.target) === null || _a === void 0 ? void 0 : _a.dataset) === null || _b === void 0 ? void 0 : _b.command; this._callEvent("buttonClick", { event: e, info: info, command: command, }); }); onEvent(this.nodeListElement, "click", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; const mouseEvent = e; // this.log("click", info); if (this._callEvent("click", { event: e, node: node, info: info }) === false) { this.lastClickTime = Date.now(); return false; } if (node) { if (mouseEvent.ctrlKey) { node.toggleSelected(); return; } // Edit title if 'clickActive' is triggered: const trigger = this.getOption("edit.trigger"); const slowClickDelay = this.getOption("edit.slowClickDelay"); if (trigger.indexOf("clickActive") >= 0 && info.region === "title" && node.isActive() && (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) { node.startEditTitle(); } if (info.region === NodeRegion.expander) { node.setExpanded(!node.isExpanded(), { scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false, }); } else if (info.region === NodeRegion.checkbox) { node.toggleSelected(); } else { if (info.colIdx >= 0) { node.setActive(true, { colIdx: info.colIdx, event: e }); } else { node.setActive(true, { event: e }); } } } this.lastClickTime = Date.now(); }); onEvent(this.nodeListElement, "dblclick", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; // this.log("dblclick", info, e); if (this._callEvent("dblclick", { event: e, node: node, info: info }) === false) { return false; } if (node && info.colIdx === 0 && node.isExpandable() && info.region !== NodeRegion.expander) { this._callMethod("edit._stopEditTitle"); node.setExpanded(!node.isExpanded()); } }); onEvent(this.element, "keydown", (e) => { const info = Wunderbaum.getEventInfo(e); const eventName = eventToString(e); const node = info.node || this.getFocusNode(); this._callHook("onKeyEvent", { event: e, node: node, info: info, eventName: eventName, }); }); onEvent(this.element, "focusin focusout", (e) => { const flag = e.type === "focusin"; const targetNode = Wunderbaum.getNode(e); this._callEvent("focus", { flag: flag, event: e }); if (flag && this.isRowNav() && !this.isEditingTitle()) { if (this.options.navigationModeOption === NavModeEnum.row) { targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive(); } else { this.setCellNav(); } } if (!flag) { this._callMethod("edit._stopEditTitle", true, { event: e, forceClose: true, }); } }); } /** * Return a Wunderbaum instance, from element, id, index, or event. * * ```js * getTree(); // Get first Wunderbaum instance on page * getTree(1); // Get second Wunderbaum instance on page * getTree(event); // Get tree for this mouse- or keyboard event * getTree("foo"); // Get tree for this `tree.options.id` * getTree("#tree"); // Get tree for first matching element selector * ``` */ static getTree(el) { if (el instanceof Wunderbaum) { return el; } else if (el instanceof WunderbaumNode) { return el.tree; } if (el === undefined) { el = 0; // get first tree } if (typeof el === "number") { el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element } else if (typeof el === "string") { // Search all trees for matching ID for (const treeElem of document.querySelectorAll(".wunderbaum")) { const tree = treeElem._wb_tree; if (tree && tree.id === el) { return tree; } } // Search by selector el = document.querySelector(el); if (!el) { return null; } } else if (el.target) { el = el.target; } assert(el instanceof Element, `Invalid el type: ${el}`); if (!el.matches(".wunderbaum")) { el = el.closest(".wunderbaum"); } if (el && el._wb_tree) { return el._wb_tree; } return null; } /** * Return the icon-function -> icon-definition mapping. * @deprecated Use {@link Wunderbaum.iconMaps} */ get iconMap() { const map = this.options.iconMap; if (typeof map === "string") { return defaultIconMaps[map]; } return map; } /** * Return a WunderbaumNode instance from element or event. */ static getNode(el) { if (!el) { return null; } else if (el instanceof WunderbaumNode) { return el; } else if (el.target !== undefined) { el = el.target; // el was an Event } // `el` is a DOM element // let nodeElem = obj.closest("div.wb-row"); while (el) { if (el._wb_node) { return el._wb_node; } el = el.parentElement; //.parentNode; } return null; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link Wunderbaum.visit}. * * Example: * ```js * for(const node of tree) { * ... * } * ``` */ *[Symbol.iterator]() { yield* this.root; } /** @internal */ _registerExtension(extension) { this.extensionList.push(extension); this.extensions[extension.id] = extension; // this.extensionMap.set(extension.id, extension); } /** Called on tree (re)init after markup is created, before loading. */ _initExtensions() { for (const ext of this.extensionList) { ext.init(); } } /** * Calculate a *stable*, unique key for a node from its refKey (or title). * We also add information from the parent, because a refKey may occur multiple * times in a tree (but not as child of the same parent). * @internal */ _calculateKey(data, parent) { if (data.key) { // Always use an explicitly passed key return data.key; } // Auto-keys are optional, use a monotonic counter by default: if (!this.options.autoKeys) { return "" + ++WunderbaumNode.sequence; } // Add the parent's key to the hash. Assuming this was generated by the // same algorithm, this should incorporate the whole path: const s = (parent ? parent.key : "") + (data.refKey || data.title); // 32-bit has a high probability of collisions, so we pump up to 64-bit // https://security.stackexchange.com/q/209882/207588 const h1 = murmurHash3(s, true); let key = "id_" + h1 + murmurHash3(h1 + s, true); // Check for collisions // (Most likely if the same title occurs multiple in the same parent). const existingNode = this.keyMap.get(key); if (existingNode) { key += "." + ++Wunderbaum.sequence; this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data); } return key; } /** Add node to tree's bookkeeping data structures. @internal */ _registerNode(node) { const key = node.key; assert(key != null, `Missing key: '${node}'.`); assert(!this.keyMap.has(key), `Duplicate key: '${key}': ${node}.`); this.keyMap.set(key, node); const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); // Set of nodes with this refKey if (rks) { rks.add(node); } else { this.refKeyMap.set(rk, new Set([node])); } } } /** Remove node from tree's bookkeeping data structures. @internal */ _unregisterNode(node) { // Remove refKey reference from map (if any) const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); if (rks && rks.delete(node) && !rks.size) { // We just removed the last element this.refKeyMap.delete(rk); } } // Remove key reference from map this.keyMap.delete(node.key); // Mark as disposed node.tree = null; node.parent = null; // Remove HTML markup node.removeMarkup(); } /** Call all hook methods of all registered extensions.*/ _callHook(hook, data = {}) { let res; const d = extend({}, { tree: this, options: this.options, result: undefined }, data); for (const ext of this.extensionList) { res = ext[hook].call(ext, d); if (res === false) { break; } if (d.result !== undefined) { res = d.result; } } return res; } /** * Call tree method or extension method if defined. * * Example: * ```js * tree._callMethod("edit.startEdit", "arg1", "arg2") * ``` */ _callMethod(name, ...args) { const [p, n] = name.split("."); const obj = n ? this.extensions[p] : this; const func = obj[n]; if (func) { return func.apply(obj, args); } else { this.logError(`Calling undefined method '${name}()'.`); } } /** * Call event handler if defined in tree or tree.EXTENSION options. * * Example: * ```js * tree._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type, extra) { const [p, n] = type.split("."); const opts = this.options; const func = n ? opts[p][n] : opts[p]; if (func) { return func.call(this, extend({ type: type, tree: this, util: this._util }, extra)); // } else { // this.logError(`Triggering undefined event '${type}'.`) } } /** Return the node for given row index. */ _getNodeByRowIdx(idx) { // TODO: start searching from active node (reverse) let node = null; this.visitRows((n) => { if (n._rowIdx === idx) { node = n; return false; } }); return node; } /** Return the topmost visible node in the viewport. * @param complete If `false`, the node is considered visible if at least one * pixel is visible. */ getTopmostVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const gracePx = 1; // ignore subpixel scrolling const scrollParent = this.element; // const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; // + headerHeight; let topIdx; if (complete) { topIdx = Math.ceil((scrollTop - gracePx) / rowHeight); } else { topIdx = Math.floor(scrollTop / rowHeight); } return this._getNodeByRowIdx(topIdx); } /** Return the lowest visible node in the viewport. */ getLowestVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const clientHeight = scrollParent.clientHeight - headerHeight; let bottomIdx; if (complete) { bottomIdx = Math.floor((scrollTop + clientHeight) / rowHeight) - 1; } else { bottomIdx = Math.ceil((scrollTop + clientHeight) / rowHeight) - 1; } bottomIdx = Math.min(bottomIdx, this.count(true) - 1); return this._getNodeByRowIdx(bottomIdx); } /** Return preceding visible node in the viewport. */ _getPrevNodeInView(node, ofs = 1) { this.visitRows((n) => { node = n; if (ofs-- <= 0) { return false; } }, { reverse: true, start: node || this.getActiveNode() }); return node; } /** Return following visible node in the viewport. */ _getNextNodeInView(node, options) { let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1; const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse); this.visitRows((n) => { node = n; if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) { return false; } if (ofs-- <= 0) { return false; } }, { reverse: reverse, start: node || this.getActiveNode() }); return node; } /** * Append (or insert) a list of toplevel nodes. * * @see {@link WunderbaumNode.addChildren} */ addChildren(nodeData, options) { return this.root.addChildren(nodeData, options); } /** * Apply a modification or navigation operation. * * Most of these commands simply map to a node or tree method. * This method is especially useful when implementing keyboard mapping, * context menus, or external buttons. * * Valid commands: * - 'moveUp', 'moveDown' * - 'indent', 'outdent' * - 'remove' * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate * */ applyCommand(cmd, nodeOrOpts, options) { let // clipboard, node, refNode; // options = $.extend( // { setActive: true, clipboard: CLIPBOARD }, // options_ // ); if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { node = this.getActiveNode(); assert(options === undefined, `Unexpected options: ${options}`); options = nodeOrOpts; } // clipboard = options.clipboard; switch (cmd) { // Sorting and indentation: case "moveUp": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "before"); node.setActive(); } break; case "moveDown": refNode = node.getNextSibling(); if (refNode) { node.moveTo(refNode, "after"); node.setActive(); } break; case "indent": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "appendChild"); refNode.setExpanded(); node.setActive(); } break; case "outdent": if (!node.isTopLevel()) { node.moveTo(node.getParent(), "after"); node.setActive(); } break; // Remove: case "remove": refNode = node.getPrevSibling() || node.getParent(); node.remove(); if (refNode) { refNode.setActive(); } break; // Add, edit (requires ext-edit): case "addChild": this._callMethod("edit.createNode", "prependChild"); break; case "addSibling": this._callMethod("edit.createNode", "after"); break; case "rename": node.startEditTitle(); break; // Simple clipboard simulation: // case "cut": // clipboard = { mode: cmd, data: node }; // break; // case "copy": // clipboard = { // mode: cmd, // data: node.toDict(function(d, n) { // delete d.key; // }), // }; // break; // case "clear": // clipboard = null; // break; // case "paste": // if (clipboard.mode === "cut") { // // refNode = node.getPrevSibling(); // clipboard.data.moveTo(node, "child"); // clipboard.data.setActive(); // } else if (clipboard.mode === "copy") { // node.addChildren(clipboard.data).setActive(); // } // break; // Navigation commands: case "down": case "first": case "last": case "left": case "nextMatch": case "pageDown": case "pageUp": case "parent": case "prevMatch": case "right": case "up": return node.navigate(cmd); default: error(`Unhandled command: '${cmd}'`); } } /** Delete all nodes. */ clear() { this.root.removeChildren(); this.root.children = null; this.keyMap.clear(); this.refKeyMap.clear(); this.treeRowCount = 0; this._activeNode = null; this._focusNode = null; // this.types = {}; // this. columns =[]; // this._columnsById = {}; // Modification Status // this.changedSince = 0; // this.changes.clear(); // this.changedNodes.clear(); // // --- FILTER --- // public filterMode: FilterModeType = null; // // --- KEYNAV --- // public activeColIdx = 0; // public cellNavMode = false; // public lastQuicksearchTime = 0; // public lastQuicksearchTerm = ""; this.update(ChangeType.structure); } /** * Clear nodes and markup and detach events and observers. * * This method may be useful to free up resources before re-creating a tree * on an existing div, for example in unittest suites. * Note that this Wunderbaum instance becomes unusable afterwards. */ destroy() { this.logInfo("destroy()..."); this.clear(); this.resizeObserver.disconnect(); this.element.innerHTML = ""; // Remove all event handlers this.element.outerHTML = this.element.outerHTML; // eslint-disable-line } /** * Return `tree.option.NAME` (also resolving if this is a callback). * * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. * * @param name option name (use dot notation to access extension option, e.g. * `filter.mode`) */ getOption(name, defaultValue) { let ext; let opts = this.options; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [ext, name] = name.split("."); opts = opts[ext]; } let value = opts[name]; // A callback resolver always takes precedence if (typeof value === "function") { value = value({ type: "resolve", tree: this }); } // Use value from value options dict, fallback do default // console.info(name, value, opts) return value !== null && value !== void 0 ? value : defaultValue; } /** * Set tree option. * Use dot notation to set plugin option, e.g. "filter.mode". */ setOption(name, value) { // this.log(`setOption(${name}, ${value})`); if (name.indexOf(".") >= 0) { const parts = name.split("."); const ext = this.extensions[parts[0]]; ext.setPluginOption(parts[1], value); return; } this.options[name] = value; switch (name) { case "checkbox": this.update(ChangeType.any); break; case "enabled": this.setEnabled(!!value); break; case "fixedCol": this.element.classList.toggle("wb-fixed-col", !!value); break; } } /** Return true if the tree (or one of its nodes) has the input focus. */ hasFocus() { return this.element.contains(document.activeElement); } /** * Return true if the tree displays a header. Grids have a header unless the * `header` option is set to `false`. Plain trees have a header if the `header` * option is a string or `true`. */ hasHeader() { const header = this.options.header; return this.isGrid() ? header !== false : !!header; } /** Run code, but defer rendering of viewport until done. * * ```js * const res = tree.runWithDeferredUpdate(() => { * return someFunctionThatWouldUpdateManyNodes(); * }); * ``` */ runWithDeferredUpdate(func) { try { this.enableUpdate(false); const res = func(); assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`); return res; } finally { this.enableUpdate(true); } } /** Run code, but defer rendering of viewport until done. * * ```js * const res = await tree.runWithDeferredUpdate(async () => { * return someAsyncFunctionThatWouldUpdateManyNodes(); * }); * ``` */ async runWithDeferredUpdateAsync(func) { try { this.enableUpdate(false); return await func(); } finally { this.enableUpdate(true); } } /** Recursively expand all expandable nodes (triggers lazy load if needed). */ async expandAll(flag = true, options) { await this.root.expandAll(flag, options); } /** Recursively select all nodes. */ selectAll(flag = true) { return this.root.setSelected(flag, { propagateDown: true }); } /** Toggle select all nodes. */ toggleSelect() { this.selectAll(this.root._anySelectable()); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents = false) { return this.root.getSelectedNodes(stopOnParents); } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false) { return this.root.getRefKeys(selected); } /* * Return an array of selected nodes. */ _selectRange(eventInfo) { this.logDebug("_selectRange", eventInfo); error("Not yet implemented."); // const mode = this.options.selectMode!; // if (mode !== "multi") { // this.logDebug(`Range selection only available for selectMode 'multi'`); // return; // } // if (eventInfo.canonicalName === "Meta+click") { // eventInfo.node?.toggleSelected(); // return false; // don't // } else if (eventInfo.canonicalName === "Shift+click") { // let from = this.activeNode; // let to = eventInfo.node; // if (!from || !to || from === to) { // return; // } // this.runWithDeferredUpdate(() => { // this.visitRows( // (node) => { // node.setSelected(); // }, // { // includeHidden: true, // includeSelf: false, // start: from, // reverse: from!._rowIdx! > to!._rowIdx!, // } // ); // }); // return false; // } } /** Return the number of nodes in the data model. * @param visible if true, nodes that are hidden due to collapsed parents are ignored. */ count(visible = false) { return visible ? this.treeRowCount : this.keyMap.size; } /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`. */ countUnique() { return this.refKeyMap.size; } /** @internal sanity check. */ _check() { let i = 0; this.visit((n) => { i++; }); if (this.keyMap.size !== i) { this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`); } // util.assert(this.keyMap.size === i); } /** * Find all nodes that match condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findAll} */ findAll(match) { return this.root.findAll(match); } /** * Find all nodes with a given _refKey_ (aka a list of clones). * * @param refKey a `node.refKey` value to search for. * @returns an array of matching nodes with at least two element or `[]` * if nothing found. * * @see {@link WunderbaumNode.getCloneList} */ findByRefKey(refKey) { const clones = this.refKeyMap.get(refKey); return clones ? Array.from(clones) : []; } /** * Find first node that matches condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findFirst} */ findFirst(match) { return this.root.findFirst(match); } /** * Find first node that matches condition. * * @see {@link WunderbaumNode.findFirst} * */ findKey(key) { return this.keyMap.get(key) || null; } /** * Find the next visible node that starts with `match`, starting at `startNode` * and wrap-around at the end. * Used by quicksearch and keyboard navigation. */ findNextNode(match, startNode, reverse = false) { //, visibleOnly) { let res = null; const firstNode = this.getFirstChild(); // Last visible node (calculation is expensive, so do only if we need it): const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null; const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match; startNode = startNode || (reverse ? lastNode : firstNode); function _checkNode(n) { // console.log("_check " + n) if (matcher(n)) { res = n; } if (res || n === startNode) { return false; } } this.visitRows(_checkNode, { start: startNode, includeSelf: false, reverse: reverse, }); // Wrap around search if (!res && startNode !== firstNode) { this.visitRows(_checkNode, { start: reverse ? lastNode : firstNode, includeSelf: true, reverse: reverse, }); } return res; } /** * Find a node relative to another node. * * @param node * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the keyCode that would normally trigger this move, * e.g. `$.ui.keyCode.LEFT` = 'left'. * @param includeHidden Not yet implemented */ findRelatedNode(node, where, includeHidden = false) { const rowHeight = this.options.rowHeightPx; let res = null; const pageSize = Math.floor(this.listContainerElement.clientHeight / rowHeight); switch (where) { case "parent": if (node.parent && node.parent.parent) { res = node.parent; } break; case "first": // First visible node this.visit((n) => { if (n.isVisible()) { res = n; return false; } }); break; case "last": this.visit((n) => { // last visible node if (n.isVisible()) { res = n; } }); break; case "left": if (node.parent && node.parent.parent) { res = node.parent; } // if (node.expanded) { // node.setExpanded(false); // } else if (node.parent && node.parent.parent) { // res = node.parent; // } break; case "right": if (node.children && node.children.length) { res = node.children[0]; } // if (this.cellNavMode) { // throw new Error("Not implemented"); // } else { // if (!node.expanded && (node.children || node.lazy)) { // node.setExpanded(); // res = node; // } else if (node.children && node.children.length) { // res = node.children[0]; // } // } break; case "up": res = this._getNextNodeInView(node, { reverse: true }); break; case "down": res = this._getNextNodeInView(node); break; case "pageDown": { const bottomNode = this.getLowestVpNode(); // this.logDebug(`${where}(${node}) -> ${bottomNode}`); if (node._rowIdx < bottomNode._rowIdx) { res = bottomNode; } else { res = this._getNextNodeInView(node, { reverse: false, ofs: pageSize, }); } } break; case "pageUp": if (node._rowIdx === 0) { res = node; } else { const topNode = this.getTopmostVpNode(); // this.logDebug(`${where}(${node}) -> ${topNode}`); if (node._rowIdx > topNode._rowIdx) { res = topNode; } else { res = this._getNextNodeInView(node, { reverse: true, ofs: pageSize, }); } } break; case "prevMatch": // fallthrough case "nextMatch": if (!this.isFilterActive) { this.logWarn(`${where}: Filter is not active.`); break; } res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch"); res === null || res === void 0 ? void 0 : res.setActive(); break; default: this.logWarn("Unknown relation '" + where + "'."); } return res; } /** * Iterator version of {@link Wunderbaum.format}. */ *format_iter(name_cb, connectors) { yield* this.root.format_iter(name_cb, connectors); } /** * Return multiline string representation of the node hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.format((n)=>n.title)); * ``` * logs * ``` * Playground * ├─ Books * | ├─ Art of War * | ╰─ Don Quixote * ├─ Music * ... * ``` * * @see {@link Wunderbaum.format_iter} and {@link WunderbaumNode.format}. */ format(name_cb, connectors) { return this.root.format(name_cb, connectors); } /** * Always returns null (so a tree instance behaves as `tree.root`). */ get parent() { return null; } /** * Return a list of top-level nodes. */ get children() { return this.root.children || []; } /** * Return the active cell (`span.wb-col`) of the currently active node or null. */ getActiveColElem() { if (this.activeNode && this.activeColIdx >= 0) { return this.activeNode.getColElem(this.activeColIdx); } return null; } /** * Return the currently active node or null (alias for `tree.activeNode`). * Alias for {@link Wunderbaum.activeNode}. * * @see {@link WunderbaumNode.setActive} * @see {@link WunderbaumNode.isActive} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getActiveNode() { return this.activeNode; } /** * Return the first top level node if any (not the invisible root node). */ getFirstChild() { return this.root.getFirstChild(); } /** * Return the last top level node if any (not the invisible root node). */ getLastChild() { return this.root.getLastChild(); } /** * Return the node that currently has keyboard focus or null. * Alias for {@link Wunderbaum.focusNode}. * @see {@link WunderbaumNode.setFocus} * @see {@link WunderbaumNode.hasFocus} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getFocusNode() { return this.focusNode; } /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ static getEventInfo(event) { const target = event.target; const cl = target.classList; const parentCol = target.closest("span.wb-col"); const node = Wunderbaum.getNode(target); const tree = node ? node.tree : Wunderbaum.getTree(event); const res = { event: event, canonicalName: eventToString(event), tree: tree, node: node, region: NodeRegion.unknown, colDef: undefined, colIdx: -1, colId: undefined, colElem: parentCol, }; if (cl.contains("wb-title")) { res.region = NodeRegion.title; } else if (cl.contains("wb-expander")) { res.region = node.isExpandable() ? NodeRegion.expander : NodeRegion.prefix; } else if (cl.contains("wb-checkbox")) { res.region = NodeRegion.checkbox; } else if (cl.contains("wb-icon")) { //|| cl.contains("wb-custom-icon")) { res.region = NodeRegion.icon; } else if (cl.contains("wb-node")) { res.region = NodeRegion.title; } else if (parentCol) { res.region = NodeRegion.column; const idx = Array.prototype.indexOf.call(parentCol.parentNode.children, parentCol); res.colIdx = idx; } else if (cl.contains("wb-row")) { // Plain tree res.region = NodeRegion.title; } else { // Somewhere near the title if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) { tree === null || tree === void 0 ? void 0 : tree.logWarn("getEventInfo(): not found", event, res); } return res; } if (res.colIdx === -1) { res.colIdx = 0; } res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx]; res.colDef != null ? (res.colId = res.colDef.id) : 0; // this.log("Event", event, res); return res; } /** * Return readable string representation for this instance. * @internal */ toString() { return `Wunderbaum<'${this.id}'>`; } /** Return true if any node title or grid cell is currently beeing edited. * * See also {@link isEditingTitle}. */ isEditing() { const focusElem = this.nodeListElement.querySelector("input:focus,select:focus"); return !!focusElem; } /** Return true if any node is currently in edit-title mode. * * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}. */ isEditingTitle() { return this._callMethod("edit.isEditingTitle"); } /** * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. */ isLoading() { let res = false; this.root.visit((n) => { // also visit rootNode if (n._isLoading || n._requestId) { res = true; return false; } }, true); return res; } /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4. * @see {@link logDebug} */ log(...args) { if (this.options.debugLevel >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4. * and browser console level includes debug/verbose messages. * @see {@link log} */ logDebug(...args) { if (this.options.debugLevel >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with tree name as prefix. */ logError(...args) { if (this.options.debugLevel >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */ logInfo(...args) { if (this.options.debugLevel >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** @internal */ logTime(label) { if (this.options.debugLevel >= 4) { console.time(this + ": " + label); // eslint-disable-line no-console } return label; } /** @internal */ logTimeEnd(label) { if (this.options.debugLevel >= 4) { console.timeEnd(this + ": " + label); // eslint-disable-line no-console } } /** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */ logWarn(...args) { if (this.options.debugLevel >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Emit a warning for deprecated methods. @internal */ logDeprecate(method, options) { if (this.options.debugLevel >= 2) { let msg = `${this}: ${method} is deprecated`; if (options === null || options === void 0 ? void 0 : options.since) { msg += ` since ${options.since}`; } if (options === null || options === void 0 ? void 0 : options.hint) { msg += ` (${options.since})`; } console.warn(msg + "."); // eslint-disable-line no-console } } /** Reset column widths to default. @since 0.10.0 */ resetColumns() { this.columns.forEach((col) => { delete col.customWidthPx; }); this.update(ChangeType.colStructure); } // /** Renumber nodes `_nativeIndex`. @see {@link WunderbaumNode.resetNativeChildOrder} */ // resetNativeChildOrder(options?: ResetOrderOptions) { // this.root.resetNativeChildOrder(options); // } /** * Make sure that this node is vertically scrolled into the viewport. * * Nodes that are above the visible area become the top row, nodes that are * below the viewport become the bottom row. */ scrollTo(nodeOrOpts) { const PADDING = 2; // leave some pixels between viewport bounds let node; // WunderbaumNode; let options; if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { options = nodeOrOpts; node = options.node; } assert(node && node._rowIdx != null, `Invalid node: ${node}`); const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const vpHeight = scrollParent.clientHeight; const rowTop = node._rowIdx * rowHeight + headerHeight; const vpTop = headerHeight; const vpRowTop = rowTop - scrollTop; const vpRowBottom = vpRowTop + rowHeight; const topNode = options === null || options === void 0 ? void 0 : options.topNode; // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts , options); let newScrollTop = null; if (vpRowTop >= vpTop) { if (vpRowBottom <= vpHeight) ; else { // Node is below viewport // this.log("Below viewport"); newScrollTop = rowTop + rowHeight - vpHeight + PADDING; // leave some pixels between viewport bounds } } else { // Node is above viewport // this.log("Above viewport"); newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between viewport bounds } if (newScrollTop != null) { this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`); scrollParent.scrollTop = newScrollTop; if (topNode) { // Make sure the topNode is always visible this.scrollTo(topNode); } // this.update(ChangeType.scroll); } } /** * Make sure that this node is horizontally scrolled into the viewport. * Called by {@link setColumn}. */ scrollToHorz() { // const PADDING = 1; const fixedWidth = this.columns[0]._widthPx; const vpWidth = this.element.clientWidth; const scrollLeft = this.element.scrollLeft; const colElem = this.getActiveColElem(); const colLeft = Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.left, 10); const colRight = colLeft + Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.width, 10); let newLeft = scrollLeft; if (colLeft - scrollLeft < fixedWidth) { // The current column is scrolled behind the left fixed column newLeft = colLeft - fixedWidth; } else if (colRight - scrollLeft > vpWidth) { // The current column is scrolled outside the right side newLeft = colRight - vpWidth; } newLeft = Math.max(0, newLeft); // util.assert(node._rowIdx != null); this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`); this.element.scrollLeft = newLeft; // this.update(ChangeType.scroll); } /** * Set column #colIdx to 'active'. * * This higlights the column header and -cells by adding the `wb-active` * class to all grid cells of the active column.
* Available in cell-nav mode only. * * If _options.edit_ is true, the embedded input element is focused, or if * colIdx is 0, the node title is put into edit mode. */ setColumn(colIdx, options) { var _a, _b, _c; const edit = options === null || options === void 0 ? void 0 : options.edit; const scroll = (options === null || options === void 0 ? void 0 : options.scrollIntoView) !== false; assert(this.isCellNav(), "Expected cellNav mode"); if (typeof colIdx === "string") { const cid = colIdx; colIdx = this.columns.findIndex((c) => c.id === colIdx); assert(colIdx >= 0, `Invalid colId: ${cid}`); } assert(0 <= colIdx && colIdx < this.columns.length, `Invalid colIdx: ${colIdx}`); this.activeColIdx = colIdx; // Update `wb-active` class for all headers if (this.hasHeader()) { for (const rowDiv of this.headerElement.children) { let i = 0; for (const colDiv of rowDiv.children) { colDiv.classList.toggle("wb-active", i++ === colIdx); } } } (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status); // Update `wb-active` class for all cell spans for (const rowDiv of this.nodeListElement.children) { let i = 0; for (const colDiv of rowDiv.children) { colDiv.classList.toggle("wb-active", i++ === colIdx); } } // Horizontically scroll into view if (scroll || edit) { this.scrollToHorz(); } if (edit && this.activeNode) { // this.activeNode.setFocus(); // Blur prev. input if any if (colIdx === 0) { this.activeNode.startEditTitle(); } else { (_c = (_b = this.getActiveColElem()) === null || _b === void 0 ? void 0 : _b.querySelector("input,select")) === null || _c === void 0 ? void 0 : _c.focus(); } } } /* Set or remove keyboard focus to the tree container. @internal */ _setActiveNode(node) { this._activeNode = node; } /** Set or remove keyboard focus to the tree container. */ setActiveNode(key, flag = true, options) { var _a; (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setActive(flag, options); } /** Set or remove keyboard focus to the tree container. */ setFocus(flag = true) { if (flag) { this.element.focus(); } else { this.element.blur(); } } /* Set or remove keyboard focus to the tree container. @internal */ _setFocusNode(node) { this._focusNode = node; } /** Return the current selection/expansion/activation status. @experimental */ getState(options = {}) { var _a, _b; const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options; const expandSet = new Set(); if (expandedKeys) { for (const node of this) { if (node.isExpanded() && node.hasChildren()) { expandSet.add(node.key); } } } // Parents of active node are always expanded if (activeKey && this.activeNode) { this.activeNode.visitParents((n) => { if (n.parent) { expandSet.add(n.key); } }, false); } const state = { expandedKeys: expandSet.size ? Array.from(expandSet) : undefined, activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null, activeColIdx: this.activeColIdx, selectedKeys: selectedKeys ? this.getSelectedNodes().flatMap((n) => n.key) : undefined, }; return state; } /** Apply selection/expansion/activation status. @experimental */ async setState(state, options = {}) { const { expandLazy = true } = options; return this.runWithDeferredUpdateAsync(async () => { var _a, _b; if (state.expandedKeys && state.expandedKeys.length) { if (expandLazy) { // Expand all keys recursively, even if they are not in the tree yet await this._loadLazyNodes(state.expandedKeys, { expand: true, noEvents: true, }); } else { for (const key of state.expandedKeys) { (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true); } } } if (state.activeKey) { this.setActiveNode(state.activeKey); } if (state.selectedKeys) { this.selectAll(false); for (const key of state.selectedKeys) { (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true); } } if (this.isCellNav() && state.activeColIdx != null) { this.setColumn(state.activeColIdx); } }); } update(change, node, options) { // this.log(`update(${change}) node=${node}`); if (!(node instanceof WunderbaumNode)) { options = node; node = undefined; } const immediate = !!getOption(options, "immediate"); const RF = RenderFlag; const pending = this.pendingChangeTypes; if (this._disableUpdateCount) { // Assuming that we redraw all when enableUpdate() is re-enabled. // this.log( // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})` // ); this._disableUpdateIgnoreCount++; return; } switch (change) { case ChangeType.any: case ChangeType.colStructure: pending.add(RF.header); pending.add(RF.clearMarkup); pending.add(RF.redraw); pending.add(RF.scroll); break; case ChangeType.resize: // case ChangeType.colWidth: pending.add(RF.header); pending.add(RF.redraw); break; case ChangeType.structure: pending.add(RF.redraw); break; case ChangeType.scroll: pending.add(RF.scroll); break; case ChangeType.row: case ChangeType.data: case ChangeType.status: assert(node, `Option '${change}' requires a node.`); // Single nodes are immediately updated if already inside the viewport // (otherwise we can ignore) if (node._rowElem) { node._render({ change: change }); } break; default: error(`Invalid change type '${change}'.`); } if (change === ChangeType.colStructure) { const isGrid = this.isGrid(); this.element.classList.toggle("wb-grid", isGrid); if (!isGrid && this.isCellNav()) { this.setCellNav(false); } } if (pending.size > 0) { if (immediate) { this._updateViewportImmediately(); } else { this._updateViewportThrottled(); } } } /** Disable mouse and keyboard interaction (return prev. state). */ setEnabled(flag = true) { const prev = this.enabled; this.enabled = !!flag; this.element.classList.toggle("wb-disabled", !flag); return prev; } /** Return false if tree is disabled. */ isEnabled() { return this.enabled; } /** Return true if tree has more than one column, i.e. has additional data columns. */ isGrid() { return this.columns && this.columns.length > 1; } /** Return true if cell-navigation mode is active. */ isCellNav() { return !!this._cellNavMode; } /** Return true if row-navigation mode is active. */ isRowNav() { return !this._cellNavMode; } /** Set the tree's navigation mode. */ setCellNav(flag = true) { var _a; const prev = this._cellNavMode; // if (flag === prev) { // return; // } this._cellNavMode = !!flag; if (flag && !prev) { // switch from row to cell mode this.setColumn(0); } this.element.classList.toggle("wb-cell-mode", flag); (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status); } /** Set the tree's navigation mode option. */ setNavigationOption(mode, reset = false) { if (!this.isGrid() && mode !== NavModeEnum.row) { this.logWarn("Plain trees only support row navigation mode."); return; } this.options.navigationModeOption = mode; switch (mode) { case NavModeEnum.cell: this.setCellNav(true); break; case NavModeEnum.row: this.setCellNav(false); break; case NavModeEnum.startCell: if (reset) { this.setCellNav(true); } break; case NavModeEnum.startRow: if (reset) { this.setCellNav(false); } break; default: error(`Invalid mode '${mode}'.`); } } /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */ setStatus(status, options) { return this.root.setStatus(status, options); } /** Add or redefine node type definitions. */ setTypes(types, replace = true) { assert(isPlainObject(types), `Expected plain objext: ${types}`); if (replace) { this.types = types; } else { extend(this.types, types); } // Convert `TYPE.classes` to a Set for (const t of Object.values(this.types)) { if (t.classes) { t.classes = toSet(t.classes); } } } /** * Sort nodes list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp = nodeTitleSorter, deep = false) { this.logDeprecate("sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep, propName: "title", }); } /** * Convenience method to implement column sorting. * @see {@link WunderbaumNode.sortByProperty}. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options) { this.logDeprecate("sortByProperty()", { since: "0.14.0" }); this.root.sortByProperty(options); } /** * Sort nodes list by title or custom criteria. * @since 0.14.0 */ sort(options) { this.root.sort(options); } /** Convert tree to an array of plain objects. * * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link WunderbaumNode.toDict}. */ toDictArray(callback) { var _a; const res = this.root.toDict(true, callback); return (_a = res.children) !== null && _a !== void 0 ? _a : []; } /** * Update column headers and column width. * Return true if at least one column width changed. */ // _updateColumnWidths(options?: UpdateColumnsOptions): boolean { _updateColumnWidths() { // options = Object.assign({ updateRows: true, renderMarkup: false }, options); const defaultMinWidth = 4; const vpWidth = this.element.clientWidth; // Shorten last column width to avoid h-scrollbar // (otherwise resizbing the demo would display a void scrollbar?) const FIX_ADJUST_LAST_COL = 1; const columns = this.columns; const col0 = columns[0]; let totalWidth = 0; let totalWeight = 0; let fixedWidth = 0; let modified = false; // this.element.classList.toggle("wb-grid", isGrid); // if (!isGrid && this.isCellNav()) { // this.setCellNav(false); // } // if (options.calculateCols) { if (col0.id !== "*") { throw new Error(`First column must have id '*': got '${col0.id}'.`); } // Gather width definitions this._columnsById = {}; for (const col of columns) { this._columnsById[col.id] = col; const cw = col.customWidthPx ? `${col.customWidthPx}px` : col.width; if (col.id === "*" && col !== col0) { throw new Error(`Column id '*' must be defined only once: '${col.title}'.`); } if (!cw || cw === "*") { col._weight = 1.0; totalWeight += 1.0; } else if (typeof cw === "number") { col._weight = cw; totalWeight += cw; } else if (typeof cw === "string" && cw.endsWith("px")) { col._weight = 0; const px = parseFloat(cw.slice(0, -2)); if (col._widthPx != px) { modified = true; col._widthPx = px; } fixedWidth += px; } else { error(`Invalid column width: ${cw} (expected string ending with 'px' or number, e.g. "px" or ).`); } } // Share remaining space between non-fixed columns const restPx = Math.max(0, vpWidth - fixedWidth); let ofsPx = 0; for (const col of columns) { let minWidth; if (col._weight) { const cmw = col.minWidth; if (typeof cmw === "number") { minWidth = cmw; } else if (typeof cmw === "string" && cmw.endsWith("px")) { minWidth = parseFloat(cmw.slice(0, -2)); } else { minWidth = defaultMinWidth; } const px = Math.max(minWidth, (restPx * col._weight) / totalWeight); if (col._widthPx != px) { modified = true; col._widthPx = px; } } col._ofsPx = ofsPx; ofsPx += col._widthPx; } columns[columns.length - 1]._widthPx -= FIX_ADJUST_LAST_COL; totalWidth = ofsPx - FIX_ADJUST_LAST_COL; const tw = `${totalWidth}px`; this.headerElement.style.width = tw; this.listContainerElement.style.width = tw; // } // Every column has now a calculated `_ofsPx` and `_widthPx` // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element); // console.trace(); // util.error("BREAK"); // if (modified) { // this._renderHeaderMarkup(); // if (options.renderMarkup) { // this.update(ChangeType.header, { removeMarkup: true }); // } else if (options.updateRows) { // this._updateRows(); // } // } return modified; } // protected _insertIcon(icon: string, elem: HTMLElement) { // const iconElem = document.createElement("i"); // iconElem.className = icon; // elem.appendChild(iconElem); // } /** Create/update header markup from `this.columns` definition. * @internal */ _renderHeaderMarkup() { assert(this.headerElement, "Expected a headerElement"); const wantHeader = this.hasHeader(); setElemDisplay(this.headerElement, wantHeader); if (!wantHeader) { return; } const iconMap = this.iconMap; const colCount = this.columns.length; const headerRow = this.headerElement.querySelector(".wb-row"); assert(headerRow, "Expected a row in header element"); headerRow.innerHTML = "".repeat(colCount); for (let i = 0; i < colCount; i++) { const col = this.columns[i]; const colElem = headerRow.children[i]; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; // Add classes from `columns` definition to `` cells if (typeof col.headerClasses === "string") { col.headerClasses ? colElem.classList.add(...col.headerClasses.split(" ")) : 0; } else { col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; } // Add tooltip to column title let tooltip = ""; if (col.tooltip) { tooltip = escapeTooltip(col.tooltip); tooltip = ` title="${tooltip}"`; } // Add column header icons let addMarkup = ""; // NOTE: we use CSS float: right to align icons, so they must be added in // reverse order if (toBool(col.menu, this.options.columnsMenu, false)) { const iconClass = "wb-col-icon-menu " + iconMap.colMenu; const icon = ``; addMarkup += icon; } if (toBool(col.sortable, this.options.columnsSortable, false)) { let iconClass = "wb-col-icon-sort " + iconMap.colSortable; if (col.sortOrder) { iconClass += `wb-col-sort-${col.sortOrder}`; iconClass += col.sortOrder === "asc" ? iconMap.colSortAsc : iconMap.colSortDesc; } const icon = ``; addMarkup += icon; } if (toBool(col.filterable, this.options.columnsFilterable, false)) { colElem.classList.toggle("wb-col-filter", !!col.filterActive); let iconClass = "wb-col-icon-filter " + iconMap.colFilter; if (col.filterActive) { iconClass += iconMap.colFilterActive; } const icon = ``; addMarkup += icon; } // Add resizer to all but the last column if (i < colCount - 1) { if (toBool(col.resizable, this.options.columnsResizable, false)) { addMarkup += ''; } else { addMarkup += ''; } } // Create column header const title = escapeHtml(col.title || col.id); colElem.innerHTML = `${title}${addMarkup}`; // Highlight active column if (this.isCellNav()) { colElem.classList.toggle("wb-active", i === this.activeColIdx); } } } /** * Render pending changes that were scheduled using {@link WunderbaumNode.update} if any. * * This is hardly ever neccessary, since we normally either * - call `update(ChangeType.TYPE)` (async, throttled), or * - call `update(ChangeType.TYPE, {immediate: true})` (synchronous) * * `updatePendingModifications()` will only force immediate execution of * pending async changes if any. */ updatePendingModifications() { if (this.pendingChangeTypes.size > 0) { this._updateViewportImmediately(); } } /** @internal */ _createNodeIcon(node, showLoading, showBadge) { const iconMap = this.iconMap; let iconElem; let icon = node.getOption("icon"); if (node._errorInfo) { icon = iconMap.error; } else if (node._isLoading && showLoading) { // Status nodes, or nodes without expander (< minExpandLevel) should // display the 'loading' status with the i.wb-icon span icon = iconMap.loading; } if (icon === false) { return null; // explicitly disabled: don't try default icons } if (typeof icon === "string") ; else if (node.statusNodeType) { icon = iconMap[node.statusNodeType]; } else if (node.expanded) { icon = iconMap.folderOpen; } else if (node.children) { icon = iconMap.folder; } else if (node.lazy) { icon = iconMap.folderLazy; } else { icon = iconMap.doc; } if (!icon) { iconElem = document.createElement("i"); iconElem.className = "wb-icon"; } else if (TEST_HTML.test(icon)) { iconElem = elemFromHtml(icon); } else if (TEST_FILE_PATH.test(icon)) { iconElem = elemFromHtml(``); } else { // Class name iconElem = document.createElement("i"); iconElem.className = "wb-icon " + icon; } // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem }); let badge = null; if (cbRes != null && cbRes !== false) { let classes = ""; let tooltip = ""; if (isPlainObject(cbRes)) { badge = "" + cbRes.badge; classes = cbRes.badgeClass ? " " + cbRes.badgeClass : ""; tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : ""; } else if (typeof cbRes === "number") { badge = "" + cbRes; } else { badge = cbRes; // string or HTMLSpanElement } if (typeof badge === "string") { badge = elemFromHtml(`${escapeHtml(badge)}`); } if (badge) { iconElem.append(badge); } } return iconElem; } _updateTopBreadcrumb() { const breadcrumb = this.breadcrumb; const topmost = this.getTopmostVpNode(true); const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false); if (parentList === null || parentList === void 0 ? void 0 : parentList.length) { breadcrumb.innerHTML = ""; for (const n of topmost.getParentList(false, false)) { const icon = this._createNodeIcon(n, false, false); if (icon) { breadcrumb.append(icon, " "); } const part = document.createElement("a"); part.textContent = n.title; part.href = "#"; part.classList.add("wb-breadcrumb"); part.dataset.key = n.key; breadcrumb.append(part, this.options.strings.breadcrumbDelimiter); } } else { breadcrumb.innerHTML = " "; } } /** * This is the actual update method, which is wrapped inside a throttle method. * It calls `updateColumns()` and `_updateRows()`. * * This protected method should not be called directly but via * {@link WunderbaumNode.update}`, {@link Wunderbaum.update}, * or {@link Wunderbaum.updatePendingModifications}. * @internal */ _updateViewportImmediately() { if (this._disableUpdateCount) { this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`); this._disableUpdateIgnoreCount++; return; } if (this._updateViewportThrottled.pending()) { // this.logWarn(`_updateViewportImmediately() cancel pending timer.`); this._updateViewportThrottled.cancel(); } // Shorten container height to avoid v-scrollbar const FIX_ADJUST_HEIGHT = 1; const RF = RenderFlag; const pending = new Set(this.pendingChangeTypes); this.pendingChangeTypes.clear(); const scrollOnly = pending.has(RF.scroll) && pending.size === 1; if (scrollOnly) { this._updateRows({ newNodesOnly: true }); // this.log("_updateViewportImmediately(): scroll only."); } else { this.log("_updateViewportImmediately():", pending); if (this.options.adjustHeight !== false) { let height = this.listContainerElement.clientHeight; const headerHeight = this.headerElement.clientHeight; // May be 0 const wantHeight = this.element.clientHeight - headerHeight - FIX_ADJUST_HEIGHT; if (Math.abs(height - wantHeight) > 1.0) { // this.log("resize", height, wantHeight); this.listContainerElement.style.height = wantHeight + "px"; height = wantHeight; } } // console.profile(`_updateViewportImmediately()`) if (pending.has(RF.clearMarkup)) { this.visit((n) => { n.removeMarkup(); }); } // let widthModified = false; if (pending.has(RF.header)) { // widthModified = this._updateColumnWidths(); this._updateColumnWidths(); this._renderHeaderMarkup(); } this._updateRows(); // console.profileEnd(`_updateViewportImmediately()`) } if (this.breadcrumb) { this._updateTopBreadcrumb(); } this._callEvent("update"); } // /** // * Assert that TR order matches the natural node order // * @internal // */ // protected _validateRows(): boolean { // let trs = this.nodeListElement.childNodes; // let i = 0; // let prev = -1; // let ok = true; // trs.forEach((element) => { // const tr = element as HTMLTableRowElement; // const top = Number.parseInt(tr.style.top); // const n = (tr)._wb_node; // // if (i < 4) { // // console.info( // // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'` // // ); // // } // if (prev >= 0 && top !== prev + ROW_HEIGHT) { // n.logWarn( // `TR order mismatch at index ${i}: top=${top}px != ${ // prev + ROW_HEIGHT // }` // ); // // throw new Error("fault"); // ok = false; // } // prev = top; // i++; // }); // return ok; // } /* * - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes. * - Store count of rows to `tree.treeRowCount`. * - Renumber `node._rowIdx` for all visible nodes. * - Calculate the index range that must be rendered to fill the viewport * (including upper and lower prefetch) * - */ _updateRows(options) { // const label = this.logTime("_updateRows"); // this.log("_updateRows", opts) options = Object.assign({ newNodesOnly: false }, options); const newNodesOnly = !!options.newNodesOnly; const rowHeight = this.options.rowHeightPx; const vpHeight = this.element.clientHeight; const prefetch = RENDER_MAX_PREFETCH; // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH; const ofs = this.element.scrollTop; let startIdx = Math.max(0, ofs / rowHeight - prefetch); startIdx = Math.floor(startIdx); // Make sure start is always even, so the alternating row colors don't // change when scrolling: if (startIdx % 2) { startIdx--; } let endIdx = Math.max(0, (ofs + vpHeight) / rowHeight + prefetch); endIdx = Math.ceil(endIdx); // this.debug("render", opts); const obsoleteNodes = new Set(); this.nodeListElement.childNodes.forEach((elem) => { if (elem._wb_node) { obsoleteNodes.add(elem._wb_node); } }); let idx = 0; let top = 0; let modified = false; let prevElem = "first"; this.visitRows(function (node) { // node.log("visit") const rowDiv = node._rowElem; // Renumber all expanded nodes if (node._rowIdx !== idx) { node._rowIdx = idx; modified = true; } if (idx < startIdx || idx > endIdx) { // row is outside viewport bounds if (rowDiv) { prevElem = rowDiv; } } else if (rowDiv && newNodesOnly) { obsoleteNodes.delete(node); // no need to update existing node markup rowDiv.style.top = idx * rowHeight + "px"; prevElem = rowDiv; } else { obsoleteNodes.delete(node); // Create new markup if (rowDiv) { rowDiv.style.top = idx * rowHeight + "px"; } node._render({ top: top, after: prevElem }); // node.log("render", top, prevElem, "=>", node._rowElem); prevElem = node._rowElem; } idx++; top += rowHeight; }); this.treeRowCount = idx; for (const n of obsoleteNodes) { n._callEvent("discard"); n.removeMarkup(); } // Resize tree container this.nodeListElement.style.height = `${top}px`; // this.log( // `_updateRows(scrollOfs:${ofs}, ${startIdx}..${endIdx})`, // this.nodeListElement.style.height // ); // this.logTimeEnd(label); // this._validateRows(); return modified; } /** * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order). * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link WunderbaumNode.visit}. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * children only. * @returns {boolean} false, if the iterator was stopped. */ visit(callback) { return this.root.visit(callback, false); } /** * Call callback(node) for all nodes in vertical order, top down (or bottom up). * * Note that this considers expansion state, i.e. filtered nodes and children * of collapsed nodes are skipped, unless `includeHidden` is set. * * Stop iteration if callback() returns false.
* Return false if iteration was stopped. * * @returns {boolean} false if iteration was canceled */ visitRows(callback, options) { if (!this.root.hasChildren()) { return false; } if (options && options.reverse) { delete options.reverse; return this._visitRowsUp(callback, options); } options = options || {}; let i, nextIdx, parent, res, siblings, stopNode, siblingOfs = 0, skipFirstNode = options.includeSelf === false, node = options.start || this.root.children[0]; const includeHidden = !!options.includeHidden; const checkFilter = !includeHidden && this.filterMode === "hide"; parent = node.parent; while (parent) { // visit siblings siblings = parent.children; nextIdx = siblings.indexOf(node) + siblingOfs; assert(nextIdx >= 0, `Could not find ${node} in parent's children: ${parent}`); for (i = nextIdx; i < siblings.length; i++) { node = siblings[i]; if (node === stopNode) { return false; } if (checkFilter && !node.statusNodeType && !node.match && !node.subMatchCount) { continue; } if (!skipFirstNode && callback(node) === false) { return false; } skipFirstNode = false; // Dive into node's child nodes if (node.children && node.children.length && (includeHidden || node.expanded)) { res = node.visit((n) => { if (n === stopNode) { return false; } if (checkFilter && !n.match && !n.subMatchCount) { return "skip"; } if (callback(n) === false) { return false; } if (!includeHidden && n.children && !n.expanded) { return "skip"; } }, false); if (res === false) { return false; } } } // Visit parent nodes (bottom up) node = parent; parent = parent.parent; siblingOfs = 1; // if (!parent && options.wrap) { this.logDebug("visitRows(): wrap around"); assert(options.start, "`wrap` option requires `start`"); stopNode = options.start; options.wrap = false; parent = this.root; siblingOfs = 0; } } return true; } /** * Call fn(node) for all nodes in vertical order, bottom up. * @internal */ _visitRowsUp(callback, options) { let children, idx, parent, node = options.start || this.root.children[0]; const includeHidden = !!options.includeHidden; if (options.includeSelf !== false) { if (callback(node) === false) { return false; } } while (true) { parent = node.parent; children = parent.children; if (children[0] === node) { // If this is already the first sibling, goto parent node = parent; if (!node.parent) { break; // first node of the tree } children = parent.children; } else { // Otherwise, goto prev. sibling idx = children.indexOf(node); node = children[idx - 1]; // If the prev. sibling has children, follow down to last descendant while ((includeHidden || node.expanded) && node.children && node.children.length) { children = node.children; parent = node; node = children[children.length - 1]; } } // Skip invisible if (!includeHidden && !node.isVisible()) { continue; } if (callback(node) === false) { return false; } } return true; } /** * Reload the tree with a new source. * * Previous data is cleared. Note that also column- and type defintions may * be passed with the `source` object. * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request * and restore the previous state. */ async load(source) { this.clear(); this._initialSource = source; return this.root.load(source); } /** Reload the tree and optionally restore state. * Source defaults to last ajax url if any. * Restoring the active node requires stable keys * @see {@link WunderbaumOptions.autoKeys} * @see {@link Wunderbaum.load} * @experimental */ async reload(options = {}) { const { source = this._initialSource, reactivate = true } = options; if (!source) { this.logWarn("No previous ajax source to reload."); return; } if (!reactivate) { return this.load(source); } const state = this.getState(); await this.load(source); return this.setState(state); } /** * Make sure that all nodes in the given keyList are accessible. * This may include loading lazy parent nodes. * Recursively load (and optionally expand) all requested node paths. */ async _loadLazyNodes(keyList, options = {}) { const { expand = true } = options; const keySet = new Set(keyList); // Make sure that all parent nodes are loaded (and expand if requested) while (keySet.size > 0) { const pendingNodes = []; const curSet = new Set(keySet); for (const key of curSet) { const node = this.findKey(key); if (!node) { continue; // key not yet found (need to load lazy parent?) } keySet.delete(key); if (expand) { pendingNodes.push(node.setExpanded(true)); } else if (node.isUnloaded()) { pendingNodes.push(node.loadLazy()); } if (node._rowElem) { node._render(); // show spinner even is update is suppressed } } if (pendingNodes.length === 0) { // will not load any more nodes, so if if there are still keys // left in the set, we will never find them this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet); break; } await Promise.allSettled(pendingNodes); } } /** * Disable render requests during operations that would trigger many updates. * * ```js * try { * tree.enableUpdate(false); * // ... (long running operation that would trigger many updates) * foo(); * // ... NOTE: make sure that async operations have finished, e.g. * await foo(); * } finally { * tree.enableUpdate(true); * } * ``` */ enableUpdate(flag) { /* 5 7 9 20 25 30 1 >-------------------------------------< 2 >--------------------< 3 >--------------------------< */ if (flag) { assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often"); this._disableUpdateCount--; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); if (this._disableUpdateCount === 0) { this.logDebug(`enableUpdate(): active again. Re-painting to catch up with ${this._disableUpdateIgnoreCount} ignored update requests...`); this._disableUpdateIgnoreCount = 0; this.update(ChangeType.any, { immediate: true }); } } else { this._disableUpdateCount++; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); // this._disableUpdate = Date.now(); } // return !flag; // return previous value } /* --------------------------------------------------------------------------- * FILTER * -------------------------------------------------------------------------*/ /** * Dim or hide unmatched nodes. * @param filter a string to match against node titles, or a callback function. * @param options filter options. Defaults to the `tree.options.filter` settings. * @returns the number of nodes that match the filter. * @example * ```ts * tree.filterNodes("foo", {mode: 'dim', fuzzy: true}); * // or pass a callback * tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'}); * ``` */ filterNodes(filter, options) { return this.extensions.filter.filterNodes(filter, options); } /** * Return the number of nodes that match the current filter. * @see {@link Wunderbaum.filterNodes} * @since 0.9.0 */ countMatches() { return this.extensions.filter.countMatches(); } /** * Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches(filter, options) { return this.extensions.filter.filterBranches(filter, options); } /** * Reset the filter. */ clearFilter() { return this.extensions.filter.clearFilter(); } /** * Return true if a filter is currently applied. */ isFilterActive() { return !!this.filterMode; } /** * Re-apply current filter. */ updateFilter() { return this.extensions.filter.updateFilter(); } } Wunderbaum.sequence = 0; /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */ Wunderbaum.version = "v0.14.1"; // Set to semver by 'grunt release' /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */ Wunderbaum.util = util; /** A map of default iconMaps. * May be used as default, when passing partial icon definition maps: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` */ Wunderbaum.iconMaps = defaultIconMaps; export { Wunderbaum }; ================================================ FILE: dist/wunderbaum.umd.js ================================================ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mar10 = {})); })(this, (function (exports) { 'use strict'; /*! * Wunderbaum - debounce.ts * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /* * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21 * MIT License: https://raw.githubusercontent.com/lodash/lodash/4.17.21-npm/LICENSE * Modified for TypeScript type annotations. */ /* --- */ /** Detect free variable `global` from Node.js. */ const freeGlobal = typeof global === "object" && global !== null && global.Object === Object && global; /** Detect free variable `globalThis` */ const freeGlobalThis = typeof globalThis === "object" && globalThis !== null && globalThis.Object == Object && globalThis; /** Detect free variable `self`. */ const freeSelf = typeof self === "object" && self !== null && self.Object === Object && self; /** Used as a reference to the global object. */ const root = freeGlobalThis || freeGlobal || freeSelf || Function("return this")(); /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * isObject({}) * // => true * * isObject([1, 2, 3]) * // => true * * isObject(Function) * // => true * * isObject(null) * // => false */ function isObject(value) { const type = typeof value; return value != null && (type === "object" || type === "function"); } /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', debounce(calculateLayout, 150)) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })) * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * jQuery(source).on('message', debounced) * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ function debounce(func, wait = 0, options = {}) { let lastArgs, lastThis, maxWait, result, timerId, lastCallTime; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === "function"; if (typeof func !== "function") { throw new TypeError("Expected a function"); } wait = +wait || 0; if (isObject(options)) { leading = !!options.leading; maxing = "maxWait" in options; maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; trailing = "trailing" in options ? !!options.trailing : trailing; } function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function startTimer(pendingFunc, wait) { if (useRAF) { root.cancelAnimationFrame(timerId); return root.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } function cancelTimer(id) { if (useRAF) { return root.cancelAnimationFrame(id); } clearTimeout(id); } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait)); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)); } function trailingEdge(time) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(Date.now()); } function pending() { return timerId !== undefined; } function debounced(...args) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; // eslint-disable-next-line @typescript-eslint/no-this-alias lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; } /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds (or once per browser frame). The throttled function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `throttle` and `debounce`. * * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] * The number of milliseconds to throttle invocations to; if omitted, * `requestAnimationFrame` is used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', throttle(updatePosition, 100)) * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * const throttled = throttle(renewToken, 300000, { 'trailing': false }) * jQuery(element).on('click', throttled) * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel) */ function throttle(func, wait = 0, options = {}) { let leading = true; let trailing = true; if (typeof func !== "function") { throw new TypeError("Expected a function"); } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { leading, trailing, maxWait: wait, }); } /*! * Wunderbaum - util * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** @module util */ /** Readable names for `MouseEvent.button` */ const MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right", 4: "back", 5: "forward", }; const MAX_INT = 9007199254740991; const userInfo = _getUserInfo(); /**True if the client is using a macOS platform. */ const isMac = userInfo.isMac; const REX_HTML = /[&<>"'/]/g; // Escape those characters const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips const ENTITY_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; /** A generic error that can be thrown to indicate a validation error when * handling the `apply` event for a node title or the `change` event for a * grid cell. */ class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } /** * A ES6 Promise, that exposes the resolve()/reject() methods. * * TODO: See [Promise.withResolvers()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers#description) * , a proposed standard, but not yet implemented in any browser. */ let Deferred$1 = class Deferred { constructor() { this.thens = []; this.catches = []; this.status = ""; } resolve(value) { if (this.status) { throw new Error("already settled"); } this.status = "resolved"; this.resolvedValue = value; this.thens.forEach((t) => t(value)); this.thens = []; // Avoid memleaks. } reject(error) { if (this.status) { throw new Error("already settled"); } this.status = "rejected"; this.rejectedError = error; this.catches.forEach((c) => c(error)); this.catches = []; // Avoid memleaks. } then(cb) { if (status === "resolved") { cb(this.resolvedValue); } else { this.thens.unshift(cb); } } catch(cb) { if (this.status === "rejected") { cb(this.rejectedError); } else { this.catches.unshift(cb); } } promise() { return { then: this.then, catch: this.catch, }; } }; /**Throw an `Error` if `cond` is falsey. */ function assert(cond, msg) { if (!cond) { msg = msg || "Assertion failed."; throw new Error(msg); } } function _getUserInfo() { const nav = navigator; // const ua = nav.userAgentData; const res = { isMac: /Mac/.test(nav.platform), }; return res; } /** Run `callback` when document was loaded. */ function documentReady(callback) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { callback(); } } /** Resolve when document was loaded. */ function documentReadyPromise() { return new Promise((resolve) => { documentReady(resolve); }); } /** * Iterate over Object properties or array elements. * * @param obj `Object`, `Array` or null * @param callback called for every item. * `this` also contains the item. * Return `false` to stop the iteration. */ function each(obj, callback) { if (obj == null) { // accept `null` or `undefined` return obj; } const length = obj.length; let i = 0; if (typeof length === "number") { for (; i < length; i++) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } else { for (const k in obj) { if (callback.call(obj[i], k, obj[k]) === false) { break; } } } return obj; } /** Shortcut for `throw new Error(msg)`. */ function error(msg) { throw new Error(msg); } /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */ function escapeHtml(s) { return ("" + s).replace(REX_HTML, function (s) { return ENTITY_MAP[s]; }); } // export function escapeRegExp(s: string) { // return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string // } /**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */ function escapeRegex(s) { return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); } /** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */ function escapeTooltip(s) { return ("" + s).replace(REX_TOOLTIP, function (s) { return ENTITY_MAP[s]; }); } /** TODO */ function extractHtmlText(s) { if (s.indexOf(">") >= 0) { error("Not implemented"); // return $("
").html(s).text(); } return s; } /** * Read the value from an HTML input element. * * If a `` is passed, the first child input is used. * Depending on the target element type, `value` is interpreted accordingly. * For example for a checkbox, a value of true, false, or null is returned if * the element is checked, unchecked, or indeterminate. * For datetime input control a numerical value is assumed, etc. * * Common use case: store the new user input in a `change` event handler: * * ```ts * change: (e) => { * const tree = e.tree; * const node = e.node; * // Read the value from the input control that triggered the change event: * let value = tree.getValueFromElem(e.element); * // and store it to the node model (assuming the column id matches the property name) * node.data[e.info.colId] = value; * }, * ``` * @param elem `` or `` or `` handle it node.logDebug(`Ignored ${eventName} inside focused input`); return; } // const curInputType = curInput.type || curInput.tagName; // const breakoutKeys = INPUT_KEYS[curInputType]; // if (!breakoutKeys.includes(eventName)) { // node.logDebug(`Ignored ${eventName} inside ${curInputType} input`); // return; // } } else if (curInput) { // On a cell that has an embedded, unfocused if (eventName.length === 1 && inputCanFocus) { // Typing a single char curInput.focus(); curInput.value = ""; node.logDebug(`Focus input: ${eventName}`); return false; } } if (eventName === "Tab") { eventName = "ArrowRight"; handled = true; } else if (eventName === "Shift+Tab") { eventName = tree.activeColIdx > 0 ? "ArrowLeft" : ""; handled = true; } switch (eventName) { case "+": case "Add": // case "=": // 187: '+' @ Chrome, Safari node.setExpanded(true); break; case "-": case "Subtract": node.setExpanded(false); break; case " ": // Space if (tree.activeColIdx === 0 && node.getOption("checkbox")) { node.toggleSelected(); handled = true; } else if (curInput && curInputType === "checkbox") { curInput.click(); // toggleCheckbox(curInput) // new Event("change") // curInput.change handled = true; } break; case "F2": if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Enter": tree.setFocus(); // Blur prev. input if any if ((tree.activeColIdx === 0 || isColspan) && node.isExpandable()) { node.setExpanded(!node.isExpanded()); handled = true; } else if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Escape": tree.setFocus(); // Blur prev. input if any node.log(`keynav: focus tree...`); if (tree.isCellNav() && navModeOption !== NavModeEnum.cell) { node.log(`keynav: setCellNav(false)`); tree.setCellNav(false); // row-nav mode tree.setFocus(); // handled = true; } break; case "ArrowLeft": tree.setFocus(); // Blur prev. input if any if (isColspan && node.isExpanded()) { node.setExpanded(false); } else if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(tree.activeColIdx - 1); } else if (navModeOption !== NavModeEnum.cell) { tree.setCellNav(false); // row-nav mode } handled = true; break; case "ArrowRight": tree.setFocus(); // Blur prev. input if any if (isColspan && !node.isExpanded()) { node.setExpanded(); } else if (!isColspan && tree.activeColIdx < tree.columns.length - 1) { tree.setColumn(tree.activeColIdx + 1); } handled = true; break; case "Home": // Generated by [Fn] + ArrowLeft on Mac // case "Meta+ArrowLeft": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(0); } handled = true; break; case "End": // Generated by [Fn] + ArrowRight on Mac // case "Meta+ArrowRight": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx < tree.columns.length - 1) { tree.setColumn(tree.columns.length - 1); } handled = true; break; case "ArrowDown": case "ArrowUp": case "Backspace": case "Control+End": // Generated by Control + [Fn] + ArrowRight on Mac case "Control+Home": // Generated by Control + [Fn] + Arrowleft on Mac case "Meta+ArrowDown": // [⌘] + ArrowDown on Mac case "Meta+ArrowUp": // [⌘] + ArrowUp on Mac case "PageDown": // Generated by [Fn] + ArrowDown on Mac case "PageUp": // Generated by [Fn] + ArrowUp on Mac node.navigate(eventName, { activate: activate, event: event }); // if (isCellEditMode) { // this._getEmbeddedInputElem(null, true); // set focus to input // } handled = true; break; default: handled = false; } } if (handled) { event.preventDefault(); } return; } } /*! * Wunderbaum - ext-logger * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ class LoggerExtension extends WunderbaumExtension { constructor(tree) { super(tree, "logger", {}); this.ignoreEvents = new Set([ "iconBadge", // "enhanceTitle", "render", "discard", ]); this.prefix = tree + ".ext-logger"; } init() { const tree = this.tree; // this.ignoreEvents.add(); if (tree.getOption("debugLevel") >= 4) { // const self = this; const ignoreEvents = this.ignoreEvents; const prefix = this.prefix; overrideMethod(tree, "callEvent", function (name, extra) { /* eslint-disable prefer-rest-params */ if (ignoreEvents.has(name)) { return tree._superApply(arguments); } const start = Date.now(); const res = tree._superApply(arguments); tree.logDebug(`${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`, arguments[1]); return res; }); } } onKeyEvent(data) { // this.tree.logInfo("onKeyEvent", eventToString(data.event), data); this.tree.logDebug(`${this.prefix}: onKeyEvent()`, data); return; } } /*! * Wunderbaum - ext-dnd * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ const nodeMimeType = "application/x-wunderbaum-node"; class DndExtension extends WunderbaumExtension { constructor(tree) { super(tree, "dnd", { autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering // dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" // dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) // #1021 `document.body` is not available yet // dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (override in drag, dragOver). guessDropEffect: true, // Calculate from `effectAllowed` and modifier keys) preventForeignNodes: false, // Prevent dropping nodes from different Wunderbaum trees preventLazyParents: true, // Prevent dropping items on unloaded lazy Wunderbaum tree nodes preventNonNodes: false, // Prevent dropping items other than Wunderbaum tree nodes preventRecursion: true, // Prevent dropping nodes on own descendants preventSameParent: false, // Prevent dropping nodes under same direct parent preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. (move only) serializeClipboardData: true, // Serialize node data to dataTransfer object scroll: true, // Enable auto-scrolling while dragging scrollSensitivity: 20, // Active top/bottom margin in pixel // scrollnterval: 50, // Generate event every 50 ms scrollSpeed: 5, // Scroll pixel per 50 ms // setTextTypeJson: false, // Allow dragging of nodes to different IE windows sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 // Events (drag support) dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag drag: null, // Callback(sourceNode, data) dragEnd: null, // Callback(sourceNode, data) // Events (drop support) dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop dragOver: null, // Callback(targetNode, data) dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand drop: null, // Callback(targetNode, data) dragLeave: null, // Callback(targetNode, data) }); // public dropMarkerElem?: HTMLElement; this.srcNode = null; this.lastTargetNode = null; this.lastEnterStamp = 0; this.lastAllowedDropRegions = null; this.lastDropEffect = null; this.lastDropRegion = false; this.currentScrollDir = 0; this.applyScrollDirThrottled = throttle(this._applyScrollDir, 50); } init() { super.init(); // Store the current scroll parent, which may be the tree // container, any enclosing div, or the document. // #761: scrollParent() always needs a container child // $temp = $("").appendTo(this.$container); // this.$scrollParent = $temp.scrollParent(); // $temp.remove(); const tree = this.tree; const dndOpts = tree.options.dnd; // Enable drag support if dragStart() is specified: if (dndOpts.dragStart) { onEvent(tree.element, "dragstart drag dragend", this.onDragEvent.bind(this)); } // Enable drop support if dragEnter() is specified: if (dndOpts.dragEnter) { onEvent(tree.element, "dragenter dragover dragleave drop", this.onDropEvent.bind(this)); } } /** Cleanup classes after target node is no longer hovered. */ _leaveNode() { // We remove the marker on dragenter from the previous target: const ltn = this.lastTargetNode; this.lastEnterStamp = 0; if (ltn) { ltn.setClass("wb-drop-target wb-drop-over wb-drop-after wb-drop-before", false); this.lastTargetNode = null; } } /** */ unifyDragover(res) { if (res === false) { return false; } else if (res instanceof Set) { return res.size > 0 ? res : false; } else if (res === true) { return new Set(["over", "before", "after"]); } else if (typeof res === "string" || isArray(res)) { res = toSet(res); return res.size > 0 ? res : false; } throw new Error("Unsupported drop region definition: " + res); } /** * Calculates the drop region based on the drag event and the allowed drop regions. */ _calcDropRegion(e, allowed) { const rowHeight = this.tree.options.rowHeightPx; const dy = e.offsetY; if (!allowed) { return false; } else if (allowed.size === 3) { return dy < 0.25 * rowHeight ? "before" : dy > 0.75 * rowHeight ? "after" : "over"; } else if (allowed.size === 1 && allowed.has("over")) { return "over"; } else { // Only 'before' and 'after': return dy > rowHeight / 2 ? "after" : "before"; } // return "over"; } /** * Guess drop effect (copy/link/move) using opinionated conventions. * * Default: dnd.dropEffectDefault */ _guessDropEffect(e) { // const nativeDropEffect = e.dataTransfer?.dropEffect; var _a; // if (nativeDropEffect && nativeDropEffect !== "none") { // return nativeDropEffect; // } const dndOpts = this.treeOpts.dnd; const ea = (_a = dndOpts.effectAllowed) !== null && _a !== void 0 ? _a : "all"; const canCopy = ["all", "copy", "copyLink", "copyMove"].includes(ea); const canLink = ["all", "link", "copyLink", "linkMove"].includes(ea); const canMove = ["all", "move", "copyMove", "linkMove"].includes(ea); let res = dndOpts.dropEffectDefault; if (dndOpts.guessDropEffect) { if (isMac) { if (e.altKey && canCopy) { res = "copy"; } if (e.metaKey && canMove) { res = "move"; // command key } if (e.altKey && e.metaKey && canLink) { res = "link"; } } else { if (e.ctrlKey && canCopy) { res = "copy"; } if (e.shiftKey && canMove) { res = "move"; } if (e.altKey && canLink) { res = "link"; } } } return res; } /** Don't allow void operation ('drop on self').*/ _isVoidDrop(targetNode, srcNode, dropRegion) { // this.tree.logDebug( // `_isVoidDrop: ${srcNode} -> ${dropRegion} ${targetNode}` // ); // TODO: should be checked on move only if (!this.treeOpts.dnd.preventVoidMoves || !srcNode) { return false; } if ((dropRegion === "before" && targetNode === srcNode.getNextSibling()) || (dropRegion === "after" && targetNode === srcNode.getPrevSibling())) { // this.tree.logDebug("Prevented before/after self"); return true; } // Don't allow dropping nodes on own parent (or self) return srcNode === targetNode || srcNode.parent === targetNode; } /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ _applyScrollDir() { if (this.isDragging() && this.currentScrollDir) { const dndOpts = this.tree.options.dnd; const sp = this.tree.element; // scroll parent const scrollTop = sp.scrollTop; if (this.currentScrollDir < 0) { sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed); } else if (this.currentScrollDir > 0) { sp.scrollTop = scrollTop + dndOpts.scrollSpeed; } } } /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ _autoScroll(viewportY) { const tree = this.tree; const dndOpts = tree.options.dnd; const sensitivity = dndOpts.scrollSensitivity; const sp = tree.element; // scroll parent const headerHeight = tree.headerElement.clientHeight; // May be 0 // const height = sp.clientHeight - headerHeight; // const height = sp.offsetHeight + headerHeight; const height = sp.offsetHeight; const scrollTop = sp.scrollTop; // tree.logDebug( // `autoScroll: height=${height}, scrollTop=${scrollTop}, viewportY=${viewportY}` // ); this.currentScrollDir = 0; if (scrollTop > 0 && viewportY > 0 && viewportY <= sensitivity + headerHeight) { // Mouse in top 20px area: scroll up // sp.scrollTop = Math.max(0, scrollTop - dndOpts.scrollSpeed); this.currentScrollDir = -1; } else if (scrollTop < sp.scrollHeight - height && viewportY >= height - sensitivity) { // Mouse in bottom 20px area: scroll down // sp.scrollTop = scrollTop + dndOpts.scrollSpeed; this.currentScrollDir = 1; } if (this.currentScrollDir) { this.applyScrollDirThrottled(); } return sp.scrollTop - scrollTop; } /** Return true if a drag operation currently in progress. */ isDragging() { return !!this.srcNode; } /** * Handle dragstart, drag and dragend events for the source node. */ onDragEvent(e) { var _a; const dndOpts = this.treeOpts.dnd; const srcNode = Wunderbaum.getNode(e); if (!srcNode) { this.tree.logWarn(`onDragEvent.${e.type}: no node`); return; } if (["dragstart", "dragend"].includes(e.type)) { this.tree.logDebug(`onDragEvent.${e.type} srcNode: ${srcNode}`, e); } // --- dragstart --- if (e.type === "dragstart") { // Set a default definition of allowed effects e.dataTransfer.effectAllowed = dndOpts.effectAllowed; //"copyMove"; // "all"; if (srcNode.isEditingTitle()) { srcNode.logDebug("Prevented dragging node in edit mode."); e.preventDefault(); return false; } // Let user cancel the drag operation, override effectAllowed, etc.: const res = srcNode._callEvent("dnd.dragStart", { event: e }); if (!res) { e.preventDefault(); return false; } const nodeData = srcNode.toDict(true, (n) => { // We don't want to reuse the key on drop: n._orgKey = n.key; delete n.key; }); nodeData._treeId = srcNode.tree.id; if (dndOpts.serializeClipboardData) { if (typeof dndOpts.serializeClipboardData === "function") { e.dataTransfer.setData(nodeMimeType, dndOpts.serializeClipboardData(nodeData, srcNode)); } else { e.dataTransfer.setData(nodeMimeType, JSON.stringify(nodeData)); } } // e.dataTransfer!.setData("text/html", $(node.span).html()); if (!((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.types.includes("text/plain"))) { e.dataTransfer.setData("text/plain", srcNode.title); } this.srcNode = srcNode; setTimeout(() => { // Decouple this call, so the CSS is applied to the node, but not to // the system generated drag image srcNode.setClass("wb-drag-source"); }, 0); // --- drag --- } else if (e.type === "drag") { if (dndOpts.drag) { srcNode._callEvent("dnd.drag", { event: e }); } // --- dragend --- } else if (e.type === "dragend") { srcNode.setClass("wb-drag-source", false); this.srcNode = null; if (this.lastTargetNode) { this._leaveNode(); } srcNode._callEvent("dnd.dragEnd", { event: e }); } return true; } /** * Handle dragenter, dragover, dragleave, drop events. */ onDropEvent(e) { var _a; // const isLink = event.dataTransfer.types.includes("text/uri-list"); const srcNode = this.srcNode; const srcTree = srcNode ? srcNode.tree : null; const targetNode = Wunderbaum.getNode(e); const dndOpts = this.treeOpts.dnd; const dt = e.dataTransfer; const dropRegion = this._calcDropRegion(e, this.lastAllowedDropRegions); /** Helper to log a message if predicate is false. */ const _t = (pred, msg) => { if (pred) { this.tree.log(`Prevented drop operation (${msg}).`); } return pred; }; if (!targetNode) { this._leaveNode(); e.preventDefault(); // Don't open file in browser when dropped in empty area return; } if (["drop"].includes(e.type)) { this.tree.logDebug(`onDropEvent.${e.type} targetNode: ${targetNode}, ea: ${dt === null || dt === void 0 ? void 0 : dt.effectAllowed}, ` + `de: ${dt === null || dt === void 0 ? void 0 : dt.dropEffect}, cy: ${e.offsetY}, r: ${dropRegion}, srcNode: ${srcNode}`, e); } // --- dragenter --- if (e.type === "dragenter") { // this.tree.logWarn(` onDropEvent.${e.type} targetNode: ${targetNode}`, e); this.lastAllowedDropRegions = null; // `dragleave` is not reliable with event delegation, so we generate it // from dragenter: if (this.lastTargetNode && this.lastTargetNode !== targetNode) { this._leaveNode(); } this.lastTargetNode = targetNode; this.lastEnterStamp = Date.now(); if ( // Don't drop on status node: _t(targetNode.isStatusNode(), "is status node") || // Prevent dropping nodes from different Wunderbaum trees: _t(dndOpts.preventForeignNodes && targetNode.tree !== srcTree, "preventForeignNodes") || // Prevent dropping items on unloaded lazy Wunderbaum tree nodes: _t(dndOpts.preventLazyParents && !targetNode.isLoaded(), "preventLazyParents") || // Prevent dropping items other than Wunderbaum tree nodes: _t(dndOpts.preventNonNodes && !srcNode, "preventNonNodes") || // Prevent dropping nodes on own descendants: _t(dndOpts.preventRecursion && (srcNode === null || srcNode === void 0 ? void 0 : srcNode.isAncestorOf(targetNode)), "preventRecursion") || // Prevent dropping nodes under same direct parent: _t(dndOpts.preventSameParent && srcNode && targetNode.parent === srcNode.parent, "preventSameParent") || // Don't allow void operation ('drop on self'): TODO: should be checked on move only _t(dndOpts.preventVoidMoves && targetNode === srcNode, "preventVoidMoves")) { dt.dropEffect = "none"; // this.tree.log("Prevented drop operation"); return true; // Prevent drop operation } // User may return a set of regions (or `false` to prevent drop) // Figure out a drop effect (copy/link/move) using opinated conventions. dt.dropEffect = this._guessDropEffect(e) || "none"; let regionSet = targetNode._callEvent("dnd.dragEnter", { event: e, sourceNode: srcNode, }); // regionSet = this.unifyDragover(regionSet); if (!regionSet) { dt.dropEffect = "none"; return true; // Prevent drop operation } this.lastAllowedDropRegions = regionSet; this.lastDropEffect = dt.dropEffect; const region = this._calcDropRegion(e, this.lastAllowedDropRegions); targetNode.setClass("wb-drop-target"); targetNode.setClass("wb-drop-over", region === "over"); targetNode.setClass("wb-drop-before", region === "before"); targetNode.setClass("wb-drop-after", region === "after"); e.preventDefault(); // Allow drop (Drop operation is denied by default) return false; // --- dragover --- } else if (e.type === "dragover") { const viewportY = e.clientY - this.tree.element.offsetTop; this._autoScroll(viewportY); dt.dropEffect = this._guessDropEffect(e) || "none"; targetNode._callEvent("dnd.dragOver", { event: e, sourceNode: srcNode }); const region = this._calcDropRegion(e, this.lastAllowedDropRegions); this.lastDropRegion = region; this.lastDropEffect = dt.dropEffect; if (dndOpts.autoExpandMS > 0 && targetNode.isExpandable(true) && !targetNode._isLoading && Date.now() - this.lastEnterStamp > dndOpts.autoExpandMS && targetNode._callEvent("dnd.dragExpand", { event: e, sourceNode: srcNode, }) !== false) { targetNode.setExpanded(); } if (!region || this._isVoidDrop(targetNode, srcNode, region)) { return; // We already rejected in dragenter } targetNode.setClass("wb-drop-over", region === "over"); targetNode.setClass("wb-drop-before", region === "before"); targetNode.setClass("wb-drop-after", region === "after"); e.preventDefault(); // Allow drop (Drop operation is denied by default) return false; // --- dragleave --- } else if (e.type === "dragleave") { // NOTE: we cannot trust this event, since it is always fired, // Instead we remove the marker on dragenter targetNode._callEvent("dnd.dragLeave", { event: e, sourceNode: srcNode }); // --- drop --- } else if (e.type === "drop") { e.stopPropagation(); // prevent browser from opening links? e.preventDefault(); // #69 prevent iOS browser from opening links this._leaveNode(); const region = this.lastDropRegion; let nodeData = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData(nodeMimeType); nodeData = nodeData ? JSON.parse(nodeData) : null; const srcNode = this.srcNode; const lastDropEffect = this.lastDropEffect; /* Before v0.14.0, we decoupled `_callEvent` like so: Decouple this call, because drop actions may prevent the dragend event from being fired on some browsers. setTimeout(() => {...}, 10); however this made e.dataTransfer.items inaccessible */ targetNode._callEvent("dnd.drop", { event: e, region: region, suggestedDropMode: region === "over" ? "appendChild" : region, suggestedDropEffect: lastDropEffect, sourceNode: srcNode, sourceNodeData: nodeData, dataTransfer: e.dataTransfer, }); } return false; } } /*! * Wunderbaum - drag_observer * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'. */ class DragObserver { constructor(opts) { this.start = { event: null, x: 0, y: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, }; this.dragElem = null; this.dragging = false; this.customData = {}; // TODO: touch events this.events = ["mousedown", "mouseup", "mousemove", "keydown"]; if (!opts.root) { throw new Error("Missing `root` option."); } this.opts = Object.assign({ thresh: 5 }, opts); this.root = opts.root; this._handler = this.handleEvent.bind(this); this.events.forEach((type) => { this.root.addEventListener(type, this._handler); }); } /** Unregister all event listeners. */ disconnect() { this.events.forEach((type) => { this.root.removeEventListener(type, this._handler); }); } getDragElem() { return this.dragElem; } isDragging() { return this.dragging; } stopDrag(cb_event) { if (this.dragging && this.opts.dragstop && cb_event) { cb_event.type = "dragstop"; try { this.opts.dragstop(cb_event); } catch (err) { console.error("dragstop error", err); // eslint-disable-line no-console } } this.dragElem = null; this.dragging = false; this.start.event = null; this.customData = {}; } handleEvent(e) { const type = e.type; const opts = this.opts; const cb_event = { type: e.type, startEvent: type === "mousedown" ? e : this.start.event, event: e, customData: this.customData, dragElem: this.dragElem, dx: e.pageX - this.start.x, dy: e.pageY - this.start.y, apply: undefined, }; // console.log("handleEvent", type, cb_event); switch (type) { case "keydown": this.stopDrag(cb_event); break; case "mousedown": if (this.dragElem) { this.stopDrag(cb_event); break; } if (opts.selector) { let elem = e.target; if (elem.matches(opts.selector)) { this.dragElem = elem; } else { elem = elem.closest(opts.selector); if (elem) { this.dragElem = elem; } else { break; // no event delegation selector matched } } } this.start.event = e; this.start.x = e.pageX; this.start.y = e.pageY; this.start.altKey = e.altKey; this.start.ctrlKey = e.ctrlKey; this.start.metaKey = e.metaKey; this.start.shiftKey = e.shiftKey; break; case "mousemove": // TODO: debounce/throttle? // TODO: horizontal mode: ignore if dx unchanged if (!this.dragElem) { break; } if (!this.dragging) { if (opts.thresh) { const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy; if (dist2 < opts.thresh * opts.thresh) { break; } } cb_event.type = "dragstart"; if (opts.dragstart(cb_event) === false) { this.stopDrag(cb_event); break; } this.dragging = true; } if (this.dragging && this.opts.drag) { cb_event.type = "drag"; this.opts.drag(cb_event); } break; case "mouseup": if (!this.dragging) { this.stopDrag(cb_event); break; } if (e.button === 0) { cb_event.apply = true; } else { cb_event.apply = false; } this.stopDrag(cb_event); break; } } } /*! * Wunderbaum - common * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ const DEFAULT_DEBUGLEVEL = 3; // Replaced by rollup script /** * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`. */ const DEFAULT_ROW_HEIGHT = 22; /** * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`. */ const ICON_WIDTH = 20; /** * Adjust the width of the title span, so overflow ellipsis work. * (2 x `$col-padding-x` + 3px rounding errors). */ const TITLE_SPAN_PAD_Y = 7; /** Render row markup for N nodes above and below the visible viewport. */ const RENDER_MAX_PREFETCH = 5; /** Minimum column width if not set otherwise. */ const DEFAULT_MIN_COL_WIDTH = 4; /** * A value for `node.type` that by convention may be used to mark a node as directory. * It may be used to sort 'directories' to the top. */ const NODE_TYPE_FOLDER = "folder"; /** Regular expression to detect if a string describes an image URL (in contrast * to a class name). Strings are considered image urls if they contain '.' or '/'. * `<` is ignored, because it is probably an html tag. */ const TEST_FILE_PATH = /^(?!.*<).*[/.]/; /** Regular expression to detect if a string describes an HTML element. */ const TEST_HTML = / Loading...
', // noData: "bi bi-search", noData: "bi bi-question-circle", expanderExpanded: "bi bi-chevron-down", // expanderExpanded: "bi bi-dash-square", expanderCollapsed: "bi bi-chevron-right", // expanderCollapsed: "bi bi-plus-square", expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander", // expanderLazy: "bi bi-chevron-bar-right", checkChecked: "bi bi-check-square", checkUnchecked: "bi bi-square", checkUnknown: "bi bi-dash-square-dotted", radioChecked: "bi bi-circle-fill", radioUnchecked: "bi bi-circle", radioUnknown: "bi bi-record-circle", folder: "bi bi-folder2", folderOpen: "bi bi-folder2-open", folderLazy: "bi bi-folder-symlink", doc: "bi bi-file-earmark", colSortable: "bi bi-chevron-expand", // colSortable: "bi bi-arrow-down-up", // colSortAsc: "bi bi-chevron-down", // colSortDesc: "bi bi-chevron-up", colSortAsc: "bi bi-arrow-down", colSortDesc: "bi bi-arrow-up", colFilter: "bi bi-filter-circle", colFilterActive: "bi bi-filter-circle-fill wb-helper-invalid", colMenu: "bi bi-three-dots-vertical", }, fontawesome6: { error: "fa-solid fa-triangle-exclamation", loading: "fa-solid fa-chevron-right fa-beat", noData: "fa-solid fa-circle-question", expanderExpanded: "fa-solid fa-chevron-down", expanderCollapsed: "fa-solid fa-chevron-right", expanderLazy: "fa-solid fa-chevron-right wb-helper-lazy-expander", checkChecked: "fa-regular fa-square-check", checkUnchecked: "fa-regular fa-square", checkUnknown: "fa-regular fa-square-minus", radioChecked: "fa-solid fa-circle", radioUnchecked: "fa-regular fa-circle", radioUnknown: "fa-regular fa-circle-question", folder: "fa-regular fa-folder-closed", folderOpen: "fa-regular fa-folder-open", folderLazy: "fa-solid fa-folder-plus", doc: "fa-regular fa-file", colSortable: "fa-solid fa-fw fa-sort", colSortAsc: "fa-solid fa-fw fa-sort-up", colSortDesc: "fa-solid fa-fw fa-sort-down", colFilter: "fa-solid fa-fw fa-filter", colFilterActive: "fa-solid fa-fw fa-filter wb-helper-invalid", colMenu: "fa-solid fa-fw fa-ellipsis-v", }, }; /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */ const RESERVED_TREE_SOURCE_KEYS = new Set([ "_format", // reserved for future use "_keyMap", // Used for compressed data format "_positional", // Used for compressed data format "_typeList", // Used for compressed data format @deprecated "_valueMap", // Used for compressed data format "_version", // reserved for future use "children", "columns", "types", ]); // /** Key codes that trigger grid navigation, even when inside an input element. */ // export const INPUT_BREAKOUT_KEYS: Set = new Set([ // // "ArrowDown", // // "ArrowUp", // "Enter", // "Escape", // ]); /** Map `KeyEvent.key` to navigation action. */ const KEY_TO_NAVIGATION_MAP = { ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", Backspace: "parent", End: "lastCol", Home: "firstCol", "Control+End": "last", "Control+Home": "first", "Meta+ArrowDown": "last", // macOs "Meta+ArrowUp": "first", // macOs PageDown: "pageDown", PageUp: "pageUp", }; /** Return a callback that returns true if the node title matches the string * or regular expression. * @see {@link WunderbaumNode.findAll} */ function makeNodeTitleMatcher(match) { if (match instanceof RegExp) { return function (node) { return match.test(node.title); }; } assert(typeof match === "string", `Expected a string or RegExp: ${match}`); // s = escapeRegex(s.toLowerCase()); return function (node) { return node.title === match; // console.log("match " + node, node.title.toLowerCase().indexOf(match)) // return node.title.toLowerCase().indexOf(match) >= 0; }; } /** Return a callback that returns true if the node title starts with a string (case-insensitive). */ function makeNodeTitleStartMatcher(s) { s = escapeRegex(s); const reMatch = new RegExp("^" + s, "i"); return function (node) { return reMatch.test(node.title); }; } /** Compare two nodes by title (case-insensitive). * @deprecated Use `key` option instead of `cmp` in sort methods. */ function nodeTitleSorter(a, b) { const x = a.title.toLowerCase(); const y = b.title.toLowerCase(); return x === y ? 0 : x > y ? 1 : -1; } // /** Compare nodes by title (case-insensitive). */ // export function nodeTitleKeyGetter( // node: WunderbaumNode // ): string | number | Array { // return node.title.toLowerCase(); // } /** * Convert 'flat' to 'nested' format. * * Flat node entry format: * [PARENT_IDX, {KEY_VALUE_ARGS}] * or, if N _positional re defined: * [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N] * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended: * [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}] * * 1. Parent-referencing list is converted to a list of nested dicts with * optional `children` properties. * 2. `[POSITIONAL_ARGS]` are added as dict attributes. */ function unflattenSource(source) { var _a, _b, _c; const { _format, _keyMap = {}, _positional = [], children } = source; const _positionalCount = _positional.length; if (_format !== "flat") { throw new Error(`Expected source._format: "flat", but got ${_format}`); } if (_positionalCount && _positional.includes("children")) { throw new Error(`source._positional must not include "children": ${_positional}`); } let longToShort = _keyMap; if (_keyMap.t) { // Inverse keyMap was used (pre 0.7.0) // TODO: raise Error on final 1.x release const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console longToShort = {}; for (const [key, value] of Object.entries(_keyMap)) { longToShort[value] = key; } } const positionalShort = _positional.map((e) => { var _a; return (_a = longToShort[e]) !== null && _a !== void 0 ? _a : e; }); const newChildren = []; const keyToNodeMap = {}; const indexToNodeMap = {}; const keyAttrName = (_a = longToShort["key"]) !== null && _a !== void 0 ? _a : "key"; const childrenAttrName = (_b = longToShort["children"]) !== null && _b !== void 0 ? _b : "children"; for (const [index, nodeTuple] of children.entries()) { // Node entry format: // [PARENT_ID, [POSITIONAL_ARGS]] // or // [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}] let kwargs; const [parentId, ...args] = nodeTuple; if (args.length === _positionalCount) { kwargs = {}; } else if (args.length === _positionalCount + 1) { kwargs = args.pop(); if (typeof kwargs !== "object") { throw new Error(`unflattenSource: Expected dict as last tuple element: ${nodeTuple}`); } } else { throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`); } // Free up some memory as we go nodeTuple[1] = null; if (nodeTuple[2] != null) { nodeTuple[2] = null; } // We keep `kwargs` as our new node definition. Then we add all positional // values to this object: args.forEach((val, positionalIdx) => { kwargs[positionalShort[positionalIdx]] = val; }); args.length = 0; // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric // index of the source.children list. If PARENT_ID is a string, we search // a parent with node.key of this value. indexToNodeMap[index] = kwargs; const key = kwargs[keyAttrName]; if (key != null) { keyToNodeMap[key] = kwargs; } let parentNode = null; if (parentId === null) ; else if (typeof parentId === "number") { parentNode = indexToNodeMap[parentId]; if (parentNode === undefined) { throw new Error(`unflattenSource: Could not find parent node by index: ${parentId}.`); } } else { parentNode = keyToNodeMap[parentId]; if (parentNode === undefined) { throw new Error(`unflattenSource: Could not find parent node by key: ${parentId}`); } } if (parentNode) { (_c = parentNode[childrenAttrName]) !== null && _c !== void 0 ? _c : (parentNode[childrenAttrName] = []); parentNode[childrenAttrName].push(kwargs); } else { newChildren.push(kwargs); } } source.children = newChildren; } /** * Decompresses the source data by * - converting from 'flat' to 'nested' format * - expanding short alias names to long names (if defined in _keyMap) * - resolving value indexes to value strings (if defined in _valueMap) * * @param source - The source object to be decompressed. * @returns void */ function decompressSourceData(source) { let { _format, _version = 1, _keyMap, _valueMap } = source; assert(_version === 1, `Expected file version 1 instead of ${_version}`); let longToShort = _keyMap; let shortToLong = {}; if (longToShort) { for (const [key, value] of Object.entries(longToShort)) { shortToLong[value] = key; } } // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction) // TODO: raise Error on final 1.x release if (longToShort && longToShort.t) { const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console [longToShort, shortToLong] = [shortToLong, longToShort]; } // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap) // TODO: raise Error on final 1.x release if (source._typeList != null) { const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`; if (_valueMap != null) { throw new Error(msg); } else { console.warn(msg); // eslint-disable-line no-console _valueMap = { type: source._typeList }; delete source._typeList; } } if (_format === "flat") { unflattenSource(source); } delete source._format; delete source._version; delete source._keyMap; delete source._valueMap; delete source._positional; function _iter(childList) { for (const node of childList) { // Iterate over a list of names, because we modify inside the loop // (for ... of ... does not allow this) Object.getOwnPropertyNames(node).forEach((propName) => { const value = node[propName]; // Replace short names with long names if defined in _keyMap let longName = propName; if (_keyMap && shortToLong[propName] != null) { longName = shortToLong[propName]; if (longName !== propName) { node[longName] = value; delete node[propName]; } } // Replace type index with type name if defined in _valueMap if (_valueMap && typeof value === "number" && _valueMap[longName] != null) { const newValue = _valueMap[longName][value]; if (newValue == null) { throw new Error(`Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]`); } node[longName] = newValue; } }); // Recursion if (node.children) { _iter(node.children); } } } if (_keyMap || _valueMap) { _iter(source.children); } } /*! * Wunderbaum - ext-grid * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ class GridExtension extends WunderbaumExtension { constructor(tree) { super(tree, "grid", { // throttle: 200, }); this.observer = new DragObserver({ root: window.document, selector: "span.wb-col-resizer-active", thresh: 4, // throttle: 400, dragstart: (e) => { const info = Wunderbaum.getEventInfo(e.startEvent); const colDef = info.colDef; const allow = colDef && this.tree.element.contains(e.dragElem) && toBool(colDef.resizable, tree.options.columnsResizable, false); // this.tree.log("dragstart", colDef, e, info); this.tree.element.classList.toggle("wb-col-resizing", !!allow); info.colElem.classList.toggle("wb-col-resizing", !!allow); // We start dagging, so we remember the actual width in *pixels* // (which may be 'auto' or '100%'). // Since we we re-create the markup on each update, we also cannot store // the original event or DOM element, but only the colDef object. if (allow) { // Store initial target column infos in customData e.customData.colDef = colDef; e.customData.orgCustomWidthPx = colDef.customWidthPx; const curWidthPx = Number.parseInt(info.colElem.style.width, 10); e.customData.orgWidthPx = curWidthPx; // Set custom width to current width, so that we can modify it colDef.customWidthPx = curWidthPx; // this.tree.log( // `dragstart customWidthPx=${colDef.customWidthPx}`, // e, // info // ); this.tree.update(ChangeType.colStructure); // this.tree.log( // `dragstart 2 customWidthPx=${colDef.customWidthPx}`, // e, // info // ); } return allow; }, drag: (e) => { // TODO: throttle return this.handleDrag(e); }, dragstop: (e) => { return this.handleDrag(e); }, }); } init() { super.init(); } /** * Handles drag and sragstop events for column resizing. */ handleDrag(e) { const custom = e.customData; const colDef = custom.colDef; // this.tree.log(`${e.type} (dx=${e.dx})`, e, info); if (e.type === "dragstop" || e.type === "drag") { this.tree.element.classList.remove("wb-col-resizing"); // info.colElem!.classList.remove("wb-col-resizing"); if (e.apply || e.type === "drag") { const minWidth = toPixel(colDef.minWidth, DEFAULT_MIN_COL_WIDTH); const newWidth = Math.max(minWidth, custom.orgWidthPx + e.dx); colDef.customWidthPx = newWidth; // this.tree.log( // `${e.type} minWidth=${minWidth}, newWidth=${newWidth}`, // colDef // ); } else { // Drag was cancelled this.tree.log("Column resize cancelled", e); colDef.customWidthPx = custom.orgCustomWidthPx; // Restore original width or undefined } this.tree.update(ChangeType.colStructure); } } } /*! * Wunderbaum - deferred * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** * Implement a ES6 Promise, that exposes a resolve() and reject() method. * * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}. * Example: * ```js * function foo() { * let dfd = new Deferred(), * ... * dfd.resolve('foo') * ... * return dfd.promise(); * } * ``` */ class Deferred { constructor() { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } /** Resolve the Promise. */ resolve(value) { this._resolve(value); } /** Reject the Promise. */ reject(reason) { this._reject(reason); } /** Return the native Promise instance.*/ promise() { return this._promise; } /** Call Promise.then on the embedded promise instance.*/ then(cb) { return this._promise.then(cb); } /** Call Promise.catch on the embedded promise instance.*/ catch(cb) { return this._promise.catch(cb); } /** Call Promise.finally on the embedded promise instance.*/ finally(cb) { return this._promise.finally(cb); } } /*! * Wunderbaum - wunderbaum_node * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ /** WunderbaumNode properties that can be passed with source data. * (Any other source properties will be stored as `node.data.PROP`.) */ const NODE_PROPS = new Set([ "checkbox", "classes", "expanded", "icon", "iconTooltip", "key", "lazy", "_partsel", "radiogroup", "refKey", "selected", "statusNodeType", "title", "tooltip", "type", "unselectable", ]); /** WunderbaumNode properties that will be returned by `node.toDict()`.) */ const NODE_DICT_PROPS = new Set(NODE_PROPS); NODE_DICT_PROPS.delete("_partsel"); NODE_DICT_PROPS.delete("unselectable"); // /** Node properties that are of type bool (or boolean & string). // * When parsing, we accept 0 for false and 1 for true for better JSON compression. // */ // export const NODE_BOOL_PROPS: Set = new Set([ // "checkbox", // "colspan", // "expanded", // "icon", // "iconTooltip", // "radiogroup", // "selected", // "tooltip", // "unselectable", // ]); /** * A single tree node. * * **NOTE:**
* Generally you should not modify properties directly, since this may break * the internal bookkeeping. */ class WunderbaumNode { constructor(tree, parent, data) { var _a; /** Reference key. Unlike {@link key}, a `refKey` may occur multiple * times within a tree (in this case we have 'clone nodes'). * @see Use {@link setKey} to modify. */ this.refKey = undefined; /** * Array of child nodes (null for leaf nodes). * For lazy nodes, this is `null` or ùndefined` until the children are loaded * and leaf nodes may be `[]` (empty array). * @see {@link hasChildren}, {@link addChildren}, {@link lazy}. */ this.children = null; /** Additional classes added to `div.wb-row`. * @see {@link hasClass}, {@link setClass}. */ this.classes = null; //new Set(); /** Custom data that was passed to the constructor */ this.data = {}; this._isLoading = false; this._requestId = 0; this._errorInfo = null; this._partsel = false; this._partload = false; this.subMatchCount = 0; this._rowIdx = 0; this._rowElem = undefined; assert(!parent || parent.tree === tree, `Invalid parent: ${parent}`); assert(!data.children, "'children' not allowed here"); this.tree = tree; this.parent = parent; this.key = tree._calculateKey(data, parent); this.title = "" + ((_a = data.title) !== null && _a !== void 0 ? _a : "<" + this.key + ">"); this.expanded = !!data.expanded; this.lazy = !!data.lazy; // We set the following node properties only if a matching data value is // passed data.refKey != null ? (this.refKey = "" + data.refKey) : 0; data.type != null ? (this.type = "" + data.type) : 0; data.icon != null ? (this.icon = intToBool(data.icon)) : 0; data.tooltip != null ? (this.tooltip = intToBool(data.tooltip)) : 0; data.iconTooltip != null ? (this.iconTooltip = intToBool(data.iconTooltip)) : 0; data.statusNodeType != null ? (this.statusNodeType = ("" + data.statusNodeType)) : 0; data.colspan != null ? (this.colspan = !!data.colspan) : 0; // Selection data.checkbox != null ? (this.checkbox = intToBool(data.checkbox)) : 0; data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0; data.selected != null ? (this.selected = !!data.selected) : 0; data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0; if (data.classes) { this.setClass(data.classes); } // Store custom fields as `node.data` for (const [key, value] of Object.entries(data)) { if (!NODE_PROPS.has(key)) { this.data[key] = value; } } if (parent && !this.statusNodeType) { // Don't register root node or status nodes tree._registerNode(this); } } /** * Return readable string representation for this instance. * @internal */ toString() { return `WunderbaumNode@${this.key}<'${this.title}'>`; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link WunderbaumNode.visit}. * * Example: * ```js * for(const n of node) { * ... * } * ``` */ *[Symbol.iterator]() { // let node: WunderbaumNode | null = this; const cl = this.children; if (cl) { for (let i = 0, l = cl.length; i < l; i++) { const n = cl[i]; yield n; if (n.children) { yield* n; } } // Slower: // for (let node of this.children) { // yield node; // yield* node : 0; // } } } // /** Return an option value. */ // protected _getOpt( // name: string, // nodeObject: any = null, // treeOptions: any = null, // defaultValue: any = null // ): any { // return evalOption( // name, // this, // nodeObject || this, // treeOptions || this.tree.options, // defaultValue // ); // } /** Call event handler if defined in tree.options. * Example: * ```js * node._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type, extra) { var _a; return (_a = this.tree) === null || _a === void 0 ? void 0 : _a._callEvent(type, extend({ node: this, typeInfo: this.type ? this.tree.types[this.type] : {}, }, extra)); } /** * Append (or insert) a list of child nodes. * * Tip: pass `{ before: 0 }` to prepend new nodes as first children. * * @returns first child added */ addChildren(nodeData, options) { const tree = this.tree; let { before = null, applyMinExpanLevel = true, _level } = options !== null && options !== void 0 ? options : {}; // let { before, loadLazy=true, _level } = options ?? {}; // const isTopCall = _level == null; _level !== null && _level !== void 0 ? _level : (_level = this.getLevel()); const nodeList = []; try { tree.enableUpdate(false); if (isPlainObject(nodeData)) { nodeData = [nodeData]; } const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel; for (const child of nodeData) { const subChildren = child.children; // Remove children property from source data because it should not be // passed to the constructor of WunderbaumNode: delete child.children; const n = new WunderbaumNode(tree, this, child); // Set `children` property again, so it can be used in `reload()` if (subChildren != null) { child.children = subChildren; } if (forceExpand && !n.isUnloaded()) { n.expanded = true; } nodeList.push(n); if (subChildren) { n.addChildren(subChildren, { _level: _level + 1 }); } } if (!this.children) { this.children = nodeList; } else if (before == null || this.children.length === 0) { this.children = this.children.concat(nodeList); } else { // Returns null if before is not a direct child: before = this.findDirectChild(before); const pos = this.children.indexOf(before); assert(pos >= 0, `options.before must be a direct child of ${this}`); // insert nodeList after children[pos] this.children.splice(pos, 0, ...nodeList); } // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null); tree.update(ChangeType.structure); } finally { // if (tree.options.selectMode === "hier") { // if (this.parent && this.parent.children) { // this.fixSelection3FromEndNodes(); // } else { // // may happen when loading __root__; // } // } tree.enableUpdate(true); } // if(isTopCall && loadLazy){ // this.logWarn("addChildren(): loadLazy is not yet implemented.") // } return nodeList[0]; } /** * Append or prepend a node, or append a child node. * * This a convenience function that calls addChildren() * * @param nodeData node definition * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') * @returns new node */ addNode(nodeData, mode = "appendChild") { if (mode === "over") { mode = "appendChild"; // compatible with drop region } switch (mode) { case "after": return this.parent.addChildren(nodeData, { before: this.getNextSibling(), }); case "before": return this.parent.addChildren(nodeData, { before: this }); case "prependChild": // Insert before the first child if any // let insertBefore = this.children ? this.children[0] : undefined; return this.addChildren(nodeData, { before: 0 }); case "appendChild": return this.addChildren(nodeData); } assert(false, `Invalid mode: ${mode}`); return undefined; } /** * Apply a modification (or navigation) operation. * * @see {@link Wunderbaum.applyCommand} */ applyCommand(cmd, options) { return this.tree.applyCommand(cmd, this, options); } /** * Collapse all expanded sibling nodes if any. * (Automatically called when `autoCollapse` is true.) */ collapseSiblings(options) { for (const node of this.parent.children) { if (node !== this && node.expanded) { node.setExpanded(false, options); } } } /** * Add/remove one or more classes to `
`. * * This also maintains `node.classes`, so the class will survive a re-render. * * @param className one or more class names. Multiple classes can be passed * as space-separated string, array of strings, or set of strings. */ setClass(className, flag = true) { const cnSet = toSet(className); if (flag) { if (this.classes === null) { this.classes = new Set(); } cnSet.forEach((cn) => { var _a; this.classes.add(cn); (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag); }); } else { if (this.classes === null) { return; } cnSet.forEach((cn) => { var _a; this.classes.delete(cn); (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag); }); if (this.classes.size === 0) { this.classes = null; } } } /** Start editing this node's title. */ startEditTitle() { this.tree._callMethod("edit.startEditTitle", this); } /** * Call `setExpanded()` on all descendant nodes. * * @param flag true to expand, false to collapse. * @param options Additional options. * @see {@link Wunderbaum.expandAll} * @see {@link WunderbaumNode.setExpanded} */ async expandAll(flag = true, options) { const tree = this.tree; const { collapseOthers, deep, depth, force, keepActiveNodeVisible = true, loadLazy, resetLazy, } = options !== null && options !== void 0 ? options : {}; // limit expansion level to `depth` (or tree.minExpandLevel). Default: unlimited const treeLevel = this.tree.options.minExpandLevel || null; // 0 -> null const minLevel = depth !== null && depth !== void 0 ? depth : (force ? null : treeLevel); const expandOpts = { deep: deep, force: force, loadLazy: loadLazy, resetLazy: resetLazy, scrollIntoView: false, // don't scroll every node while iterating }; this.logInfo(`expandAll(${flag}, depth=${depth}, minLevel=${minLevel})`); assert(!(flag && deep != null && !collapseOthers), "Expanding with `deep` option is not supported (implied by the `depth` option)."); // Expand all direct children in parallel: async function _iter(n, level) { var _a; // n.logInfo(` _iter(level=${level})`); const promises = []; (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach((cn) => { if (flag) { if (!cn.expanded && (minLevel == null || level < minLevel) && (cn.children || (loadLazy && cn.lazy))) { // Node is collapsed and may be expanded (i.e. has children or is lazy) // Expanding may be async, so we store the promise. // Also the recursion is delayed until expansion finished. const p = cn.setExpanded(true, expandOpts); promises.push(p); if (depth == null) { p.then(async () => { await _iter(cn, level + 1); }); } } else { // We don't expand the node, but still visit descendants. // There we may find lazy nodes, so we promises.push(_iter(cn, level + 1)); } } else { // Collapsing is always synchronous, so no promises required // Do not collapse until minExpandLevel if (minLevel == null || level >= minLevel) { cn.setExpanded(false, expandOpts); } if ((minLevel != null && level < minLevel) || deep) { _iter(cn, level + 1); // recursion, even if cn was already collapsed } } }); return new Promise((resolve) => { Promise.all(promises).then(() => { resolve(true); }); }); } const tag = tree.logTime(`${this}.expandAll(${flag}, depth=${depth})`); try { tree.enableUpdate(false); await _iter(this, 0); if (collapseOthers) { assert(flag, "Option `collapseOthers` requires flag=true"); assert(minLevel != null, "Option `collapseOthers` requires `depth` or `minExpandLevel`"); this.expandAll(false, { depth: minLevel }); } } finally { tree.enableUpdate(true); tree.logTimeEnd(tag); } if (tree.activeNode && keepActiveNodeVisible) { tree.activeNode.scrollIntoView(); } } /** * Find all descendant nodes that match condition (excluding self). * * If `match` is a string, search for exact node title. * If `match` is a RegExp expression, apply it to node.title, using * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test). * If `match` is a callback, match all nodes for that the callback(node) returns true. * * Returns an empty array if no nodes were found. * * Examples: * ```js * // Match all node titles that match exactly 'Joe': * nodeList = node.findAll("Joe") * // Match all node titles that start with 'Joe' case sensitive: * nodeList = node.findAll(/^Joe/) * // Match all node titles that contain 'oe', case insensitive: * nodeList = node.findAll(/oe/i) * // Match all nodes with `data.price` >= 99: * nodeList = node.findAll((n) => { * return n.data.price >= 99; * }) * ``` */ findAll(match) { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); const res = []; this.visit((n) => { if (matcher(n)) { res.push(n); } }); return res; } /** Return the direct child with a given key, index or null. */ findDirectChild(ptr) { const cl = this.children; if (!cl) { return null; } if (typeof ptr === "string") { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].key === ptr) { return cl[i]; } } } else if (typeof ptr === "number") { return cl[ptr]; } else if (ptr.parent === this) { // Return null if `ptr` is not a direct child return ptr; } return null; } /** * Find first descendant node that matches condition (excluding self) or null. * * @see {@link WunderbaumNode.findAll} for examples. */ findFirst(match) { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); let res = null; this.visit((n) => { if (matcher(n)) { res = n; return false; } }); return res; } /** Find a node relative to self. * * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()} */ findRelatedNode(where, includeHidden = false) { return this.tree.findRelatedNode(this, where, includeHidden); } /** * Iterator version of {@link WunderbaumNode.format}. */ *format_iter(name_cb, connectors) { connectors !== null && connectors !== void 0 ? connectors : (connectors = [" ", " | ", " ╰─ ", " ├─ "]); name_cb !== null && name_cb !== void 0 ? name_cb : (name_cb = (node) => "" + node); function _is_last(node) { const ca = node.parent.children; return node === ca[ca.length - 1]; } const _format_line = (node) => { // https://www.measurethat.net/Benchmarks/Show/12196/0/arr-unshift-vs-push-reverse-small-array const parts = [name_cb(node)]; parts.unshift(connectors[_is_last(node) ? 2 : 3]); let p = node.parent; while (p && p !== this) { // `this` is the top node parts.unshift(connectors[_is_last(p) ? 0 : 1]); p = p.parent; } return parts.join(""); }; yield name_cb(this); for (const node of this) { yield _format_line(node); } } /** * Return a multiline string representation of a node/subnode hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.getActiveNode().format((n)=>n.title)); * ``` * logs * ``` * Books * ├─ Art of War * ╰─ Don Quixote * ``` * @see {@link WunderbaumNode.format_iter} */ format(name_cb, connectors) { const a = []; for (const line of this.format_iter(name_cb, connectors)) { a.push(line); } return a.join("\n"); } /** Return the `` element with a given index or id. * @returns {WunderbaumNode | null} */ getColElem(colIdx) { var _a; if (typeof colIdx === "string") { colIdx = this.tree.columns.findIndex((value) => value.id === colIdx); } const colElems = (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.querySelectorAll("span.wb-col"); return colElems ? colElems[colIdx] : null; } /** * Return all nodes with the same refKey. * * @param includeSelf Include this node itself. * @see {@link Wunderbaum.findByRefKey} */ getCloneList(includeSelf = false) { if (!this.refKey) { return []; } const clones = this.tree.findByRefKey(this.refKey); if (includeSelf) { return clones; } return [...clones].filter((n) => n !== this); } /** Return the first child node or null. * @returns {WunderbaumNode | null} */ getFirstChild() { return this.children ? this.children[0] : null; } /** Return the last child node or null. * @returns {WunderbaumNode | null} */ getLastChild() { return this.children ? this.children[this.children.length - 1] : null; } /** Return node depth (starting with 1 for top level nodes). */ getLevel() { let i = 0, p = this.parent; while (p) { i++; p = p.parent; } return i; } /** Return the successive node (under the same parent) or null. */ getNextSibling() { const ac = this.parent.children; const idx = ac.indexOf(this); return ac[idx + 1] || null; } /** Return the parent node (null for the system root node). */ getParent() { // TODO: return null for top-level nodes? return this.parent; } /** Return an array of all parent nodes (top-down). * @param includeRoot Include the invisible system root node. * @param includeSelf Include the node itself. */ getParentList(includeRoot = false, includeSelf = false) { const l = []; let dtn = includeSelf ? this : this.parent; while (dtn) { if (includeRoot || dtn.parent) { l.unshift(dtn); } dtn = dtn.parent; } return l; } /** Return a string representing the hierarchical node path, e.g. "a/b/c". * @param includeSelf * @param part property name or callback * @param separator */ getPath(includeSelf = true, part = "title", separator = "/") { let val; const path = []; const isFunc = typeof part === "function"; this.visitParents((n) => { if (n.parent) { val = isFunc ? part(n) : n[part]; path.unshift(val); } return undefined; // TODO remove this line }, includeSelf); return path.join(separator); } /** Return the preceding node (under the same parent) or null. */ getPrevSibling() { const ac = this.parent.children; const idx = ac.indexOf(this); return ac[idx - 1] || null; } /** Return true if node has children. * Return undefined if not sure, i.e. the node is lazy and not yet loaded. */ hasChildren() { if (this.lazy) { if (this.children == null) { return undefined; // null or undefined: Not yet loaded } else if (this.children.length === 0) { return false; // Loaded, but response was empty } else if (this.children.length === 1 && this.children[0].isStatusNode()) { return undefined; // Currently loading or load error } return true; // One or more child nodes } return !!(this.children && this.children.length); } /** Return true if node has className set. */ hasClass(className) { return this.classes ? this.classes.has(className) : false; } /** Return true if node is the currently focused node. @since 0.9.0 */ hasFocus() { return this.tree.focusNode === this; } /** Return true if this node is the currently active tree node. */ isActive() { return this.tree.activeNode === this; } /** Return true if this node is a direct or indirect parent of `other`. * @see {@link WunderbaumNode.isParentOf} */ isAncestorOf(other) { return other && other.isDescendantOf(this); } /** Return true if this node is a **direct** subnode of `other`. * @see {@link WunderbaumNode.isDescendantOf} */ isChildOf(other) { return other && this.parent === other; } /** Return true if this node's refKey is used by at least one other node. */ isClone() { return !!this.refKey && this.tree.findByRefKey(this.refKey).length > 1; } /** Return true if this node's title spans all columns, i.e. the node has no * grid cells. */ isColspan() { return !!this.getOption("colspan"); } /** Return true if this node is a direct or indirect subnode of `other`. * @see {@link WunderbaumNode.isChildOf} */ isDescendantOf(other) { if (!other || other.tree !== this.tree) { return false; } let p = this.parent; while (p) { if (p === other) { return true; } if (p === p.parent) { error(`Recursive parent link: ${p}`); } p = p.parent; } return false; } /** Return true if this node has children, i.e. the node is generally expandable. * If `andCollapsed` is set, we also check if this node is collapsed, i.e. * an expand operation is currently possible. */ isExpandable(andCollapsed = false) { // `false` is never expandable (unofficial) if ((andCollapsed && this.expanded) || this.children === false) { return false; } if (this.children == null) { return !!this.lazy; // null or undefined can trigger lazy load } if (this.children.length === 0) { return !!this.tree.options.emptyChildListExpandable; } return true; } /** Return true if _this_ node is currently in edit-title mode. * * See {@link WunderbaumNode.startEditTitle}. */ isEditingTitle() { return this.tree._callMethod("edit.isEditingTitle", this); } /** Return true if this node is currently expanded. */ isExpanded() { return !!this.expanded; } /** Return true if this node is the first node of its parent's children. */ isFirstSibling() { const p = this.parent; return !p || p.children[0] === this; } /** Return true if this node is the last node of its parent's children. */ isLastSibling() { const p = this.parent; return !p || p.children[p.children.length - 1] === this; } /** Return true if this node is lazy (even if data was already loaded) */ isLazy() { return !!this.lazy; } /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */ isLoaded() { return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node } /** Return true if node is currently loading, i.e. a GET request is pending. */ isLoading() { return this._isLoading; } /** Return true if this node is a temporarily generated status node of type 'paging'. */ isPagingNode() { return this.statusNodeType === "paging"; } /** Return true if this node is a **direct** parent of `other`. * @see {@link WunderbaumNode.isAncestorOf} */ isParentOf(other) { return other && other.parent === this; } /** Return true if this node is partially loaded. @experimental */ isPartload() { return !!this._partload; } /** Return true if this node is partially selected (tri-state). */ isPartsel() { return !this.selected && !!this._partsel; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRadio() { return !!this.parent.radiogroup || this.getOption("checkbox") === "radio"; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRendered() { return !!this._rowElem; } /** Return true if this node is the (invisible) system root node. * @see {@link WunderbaumNode.isTopLevel} */ isRootNode() { return this.tree.root === this; } /** Return true if this node is selected, i.e. the checkbox is set. * `undefined` if partly selected (tri-state), false otherwise. */ isSelected() { return this.selected ? true : this._partsel ? undefined : false; } /** Return true if this node is a temporarily generated system node like * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). */ isStatusNode() { return !!this.statusNodeType; } /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */ isTopLevel() { return this.tree.root === this.parent; } /** Return true if node is marked lazy but not yet loaded. * For non-lazy nodes always return false. */ isUnloaded() { // Also checks if the only child is a status node: return this.hasChildren() === undefined; } /** Return true if all parent nodes are expanded. Note: this does not check * whether the node is scrolled into the visible part of the screen or viewport. */ isVisible() { const hasFilter = this.tree.filterMode === "hide"; const parents = this.getParentList(false, false); // TODO: check $(n.span).is(":visible") // i.e. return false for nodes (but not parents) that are hidden // by a filter if (hasFilter && !this.match && !this.subMatchCount) { // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); return false; } for (let i = 0, l = parents.length; i < l; i++) { const n = parents[i]; if (!n.expanded) { // this.debug("isVisible: HIDDEN (parent collapsed)"); return false; } // if (hasFilter && !n.match && !n.subMatchCount) { // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); // return false; // } } // this.debug("isVisible: VISIBLE"); return true; } _loadSourceObject(source, level) { var _a; const tree = this.tree; level !== null && level !== void 0 ? level : (level = this.getLevel()); // Let caller modify the parsed JSON response: const res = this._callEvent("receive", { response: source }); if (res != null) { source = res; } if (isArray(source)) { source = { children: source }; } assert(isPlainObject(source), `Expected an array or plain object: ${source}`); const format = (_a = source.format) !== null && _a !== void 0 ? _a : "nested"; assert(format === "nested" || format === "flat", `Expected source.format = 'nested' or 'flat': ${format}`); // Pre-rocess for 'nested' or 'flat' format decompressSourceData(source); assert(source.children, "If `source` is an object, it must have a `children` property"); if (source.types) { tree.logInfo("Redefine types", source.columns); tree.setTypes(source.types, false); delete source.types; } if (source.columns) { tree.logInfo("Redefine columns", source.columns); tree.columns = source.columns; delete source.columns; tree.update(ChangeType.colStructure); } this.addChildren(source.children); // Add extra data to `tree.data` for (const [key, value] of Object.entries(source)) { if (!RESERVED_TREE_SOURCE_KEYS.has(key)) { tree.data[key] = value; // tree.logDebug(`Add source.${key} to tree.data.${key}`); } } if (tree.options.selectMode === "hier") { this.fixSelection3FromEndNodes(); } // Allow to un-sort nodes after sorting this.resetNativeChildOrder(); this._callEvent("load"); } async _fetchWithOptions(source) { var _a, _b; // Either a URL string or an object with a `.url` property. let url, params, body, options, rest; let fetchOpts = {}; if (typeof source === "string") { // source is a plain URL string: assume GET request url = source; fetchOpts.method = "GET"; } else if (isPlainObject(source)) { // source is a plain object with `.url` property. ({ url, params, body, options, ...rest } = source); assert(!rest || Object.keys(rest).length === 0, `Unexpected source properties: ${Object.keys(rest)}. Use 'options' instead.`); assert(typeof url === "string", `expected source.url as string`); if (isPlainObject(options)) { fetchOpts = options; } if (isPlainObject(body)) { // we also accept 'body' as object... assert(!fetchOpts.body, "options.body should be passed as source.body"); fetchOpts.body = JSON.stringify(fetchOpts.body); (_a = fetchOpts.method) !== null && _a !== void 0 ? _a : (fetchOpts.method = "POST"); // set default } if (isPlainObject(params)) { url += "?" + new URLSearchParams(params); (_b = fetchOpts.method) !== null && _b !== void 0 ? _b : (fetchOpts.method = "GET"); // set default } } else { url = ""; // keep linter happy error(`Unsupported source format: ${source}`); } this.setStatus(NodeStatusType.loading); const response = await fetch(url, fetchOpts); if (!response.ok) { error(`GET ${url} returned ${response.status}, ${response}`); } return await response.json(); } /** Download data from the cloud, then call `.update()`. */ async load(source) { const tree = this.tree; const requestId = Date.now(); const prevParent = this.parent; const start = Date.now(); let elap = 0, elapLoad = 0, elapProcess = 0; // Check for overlapping requests if (this._requestId) { this.logWarn(`Recursive load request #${requestId} while #${this._requestId} is pending. ` + "The previous request will be ignored."); } this._requestId = requestId; // const timerLabel = tree.logTime(this + ".load()"); try { const url = typeof source === "string" ? source : source.url; if (!url) { // An array or a plain object (that does NOT contain a `.url` property) // will be treated as native Wunderbaum data if (typeof source.then === "function") { const msg = tree.logTime(`Resolve thenable ${source}`); source = await Promise.resolve(source); tree.logTimeEnd(msg); } this._loadSourceObject(source); elapProcess = Date.now() - start; } else { // Either a URL string or an object with a `.url` property. const data = await this._fetchWithOptions(source); elapLoad = Date.now() - start; if (this._requestId && this._requestId > requestId) { this.logWarn(`Ignored load response #${requestId} because #${this._requestId} is pending.`); return; } else { this.logDebug(`Received response for load request #${requestId}`); } if (this.parent === null && prevParent !== null) { this.logWarn("Lazy parent node was removed while loading: discarding response."); return; } this.setStatus(NodeStatusType.ok); // if (data.columns) { // tree.logInfo("Re-define columns", data.columns); // util.assert(!this.parent); // tree.columns = data.columns; // delete data.columns; // tree.updateColumns({ calculateCols: false }); // } const startProcess = Date.now(); this._loadSourceObject(data); elapProcess = Date.now() - startProcess; } } catch (error) { this.logError("Error during load()", source, error); this._callEvent("error", { error: error }); this.setStatus(NodeStatusType.error, { message: "" + error }); throw error; } finally { this._requestId = 0; elap = Date.now() - start; if (tree.options.debugLevel >= 3) { tree.logInfo(`Load source took ${elap / 1000} seconds ` + `(transfer: ${elapLoad / 1000}s, ` + `processing: ${elapProcess / 1000}s)`); } } } /** * Load content of a lazy node. * If the node is already loaded, nothing happens. * @param [forceReload=false] If true, reload even if already loaded. */ async loadLazy(forceReload = false) { const wasExpanded = this.expanded; assert(this.lazy, "load() requires a lazy node"); if (!forceReload && !this.isUnloaded()) { return; // Already loaded: nothing to do } if (this.isLoading()) { this.logWarn("loadLazy() called while already loading: ignored."); return; // Already loading: prevent duplicate requests } if (this.isLoaded()) { this.resetLazy(); // Also collapses if currently expanded } // `lazyLoad` may be long-running, so mark node as loading now. `this.load()` // will reset the status later. this.setStatus(NodeStatusType.loading); try { const source = await this._callEvent("lazyLoad"); if (source === false) { this.setStatus(NodeStatusType.ok); return; } assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false."); await this.load(source); this.setStatus(NodeStatusType.ok); // Also resets `this._isLoading` if (wasExpanded) { this.expanded = true; this.tree.update(ChangeType.structure); } else { this.update(); // Fix expander icon to 'loaded' } } catch (e) { this.logError("Error during loadLazy()", e); this._callEvent("error", { error: e }); // Also resets `this._isLoading`: this.setStatus(NodeStatusType.error, { message: "" + e }); } return; } /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4. * @see {@link WunderbaumNode.logDebug} */ log(...args) { if (this.tree.options.debugLevel >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4 * and browser console level includes debug/verbose messages. * @see {@link WunderbaumNode.log} */ logDebug(...args) { if (this.tree.options.debugLevel >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */ logError(...args) { if (this.tree.options.debugLevel >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */ logInfo(...args) { if (this.tree.options.debugLevel >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */ logWarn(...args) { if (this.tree.options.debugLevel >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Expand all parents and optionally scroll into visible area as neccessary. * Promise is resolved, when lazy loading and animations are done. * @param {object} [options] passed to `setExpanded()`. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} */ async makeVisible(options) { let i; const dfd = new Deferred(); const deferreds = []; const parents = this.getParentList(false, false); const len = parents.length; const noAnimation = getOption(options, "noAnimation", false); const scroll = getOption(options, "scrollIntoView", true); // Expand bottom-up, so only the top node is animated for (i = len - 1; i >= 0; i--) { // self.debug("pushexpand" + parents[i]); const seOpts = { noAnimation: noAnimation }; deferreds.push(parents[i].setExpanded(true, seOpts)); } Promise.all(deferreds).then(() => { // All expands have finished // self.debug("expand DONE", scroll); // Note: this.tree may be none when switching demo trees if (scroll && this.tree) { // Make sure markup and _rowIdx is updated before we do the scroll calculations this.tree.updatePendingModifications(); this.scrollIntoView().then(() => { // self.debug("scroll DONE"); dfd.resolve(); }); } else { dfd.resolve(); } }); return dfd.promise(); } /** Move this node to targetNode. */ moveTo(targetNode, mode = "appendChild", map) { if (mode === "over") { mode = "appendChild"; // compatible with drop region } if (mode === "prependChild") { if (targetNode.children && targetNode.children.length) { mode = "before"; targetNode = targetNode.children[0]; } else { mode = "appendChild"; } } let pos; const tree = this.tree; const prevParent = this.parent; const targetParent = mode === "appendChild" ? targetNode : targetNode.parent; if (this === targetNode) { return; } else if (!this.parent) { error("Cannot move system root"); } else if (targetParent.isDescendantOf(this)) { error("Cannot move a node to its own descendant"); } if (targetParent !== prevParent) { prevParent.triggerModifyChild("remove", this); } // Unlink this node from current parent if (this.parent.children.length === 1) { if (this.parent === targetParent) { return; // #258 } this.parent.children = this.parent.lazy ? [] : null; this.parent.expanded = false; } else { pos = this.parent.children.indexOf(this); assert(pos >= 0, "invalid source parent"); this.parent.children.splice(pos, 1); } // Insert this node to target parent's child list this.parent = targetParent; if (targetParent.hasChildren()) { switch (mode) { case "appendChild": // Append to existing target children targetParent.children.push(this); break; case "before": // Insert this node before target node pos = targetParent.children.indexOf(targetNode); assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos, 0, this); break; case "after": // Insert this node after target node pos = targetParent.children.indexOf(targetNode); assert(pos >= 0, "invalid target parent"); targetParent.children.splice(pos + 1, 0, this); break; default: error(`Invalid mode '${mode}'.`); } } else { targetParent.children = [this]; } // Let caller modify the nodes if (map) { targetNode.visit(map, true); } if (targetParent === prevParent) { targetParent.triggerModifyChild("move", this); } else { // prevParent.triggerModifyChild("remove", this); targetParent.triggerModifyChild("add", this); } // Handle cross-tree moves if (tree !== targetNode.tree) { // Fix node.tree for all source nodes // util.assert(false, "Cross-tree move is not yet implemented."); this.logWarn("Cross-tree moveTo is experimental!"); this.visit((n) => { // TODO: fix selection state and activation, ... n.tree = targetNode.tree; }, true); } // Make sure we update async, because discarding the markup would prevent // DragAndDrop to generate a dragend event on the source node setTimeout(() => { // Even indentation may have changed: tree.update(ChangeType.any); }, 0); // TODO: fix selection state // TODO: fix active state } /** Set focus relative to this node and optionally activate. * * 'left' collapses the node if it is expanded, or move to the parent * otherwise. * 'right' expands the node if it is collapsed, or move to the first * child otherwise. * * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the `event.key` that would normally trigger this move, * e.g. `ArrowLeft` = 'left'. * @param options */ async navigate(where, options) { var _a; // Allow to pass 'ArrowLeft' instead of 'left' const navType = ((_a = KEY_TO_NAVIGATION_MAP[where]) !== null && _a !== void 0 ? _a : where); // Otherwise activate or focus the related node const node = this.findRelatedNode(navType); if (!node) { this.logWarn(`Could not find related node '${where}'.`); return Promise.resolve(this); } // setFocus/setActive will scroll later (if autoScroll is specified) try { node.makeVisible({ scrollIntoView: false }); } catch (e) { // ignore } node.setFocus(); if ((options === null || options === void 0 ? void 0 : options.activate) === false) { return Promise.resolve(this); } return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event }); } /** Delete this node and all descendants. */ remove() { const tree = this.tree; const pos = this.parent.children.indexOf(this); this.triggerModify("remove"); this.parent.children.splice(pos, 1); this.visit((n) => { n.removeMarkup(); tree._unregisterNode(n); }, true); tree.update(ChangeType.structure); } /** Remove all descendants of this node. */ removeChildren() { var _a, _b; const tree = this.tree; if (!this.children) { return; } if ((_a = tree.activeNode) === null || _a === void 0 ? void 0 : _a.isDescendantOf(this)) { tree.activeNode.setActive(false); // TODO: don't fire events } if ((_b = tree.focusNode) === null || _b === void 0 ? void 0 : _b.isDescendantOf(this)) { tree._setFocusNode(null); } // TODO: persist must take care to clear select and expand cookies // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) this.triggerModifyChild("remove", null); this.visit((n) => { tree._unregisterNode(n); }); if (this.lazy) { // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes this.children = []; } else { this.children = null; } // util.assert(this.parent); // don't call this for root node if (!this.isRootNode()) { this.expanded = false; } this.tree.update(ChangeType.structure); } /** Remove all HTML markup from the DOM. */ removeMarkup() { if (this._rowElem) { delete this._rowElem._wb_node; this._rowElem.remove(); this._rowElem = undefined; } } _getRenderInfo() { const allColInfosById = {}; const renderColInfosById = {}; const isColspan = this.isColspan(); const colElems = this._rowElem ? (this._rowElem.querySelectorAll("span.wb-col")) : null; let idx = 0; for (const col of this.tree.columns) { allColInfosById[col.id] = { id: col.id, idx: idx, elem: colElems ? colElems[idx] : null, info: col, }; // renderColInfosById only contains columns that need rendering: if (!isColspan && col.id !== "*") { renderColInfosById[col.id] = allColInfosById[col.id]; } idx++; } return { allColInfosById: allColInfosById, renderColInfosById: renderColInfosById, }; } _createIcon(parentElem, replaceChild, showLoading) { const iconElem = this.tree._createNodeIcon(this, showLoading, true); if (iconElem) { if (replaceChild) { parentElem.replaceChild(iconElem, replaceChild); } else { parentElem.appendChild(iconElem); } } return iconElem; } /** * Create a whole new `
` element. * @see {@link WunderbaumNode._render} */ _render_markup(opts) { const tree = this.tree; const treeOptions = tree.options; const rowHeight = treeOptions.rowHeightPx; const checkbox = this.getOption("checkbox"); const columns = tree.columns; const level = this.getLevel(); const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx; let elem; let rowDiv = this._rowElem; let checkboxSpan = null; let expanderSpan = null; const isNew = !rowDiv; assert(isNew, "Expected unrendered node"); assert(!isNew || (opts && opts.after), "opts.after expected, unless updating"); assert(!this.isRootNode(), "Root node not allowed"); rowDiv = document.createElement("div"); rowDiv.classList.add("wb-row"); rowDiv.style.top = this._rowIdx * rowHeight + "px"; this._rowElem = rowDiv; // Attach a node reference to the DOM Element: rowDiv._wb_node = this; const nodeElem = document.createElement("span"); nodeElem.classList.add("wb-node", "wb-col"); rowDiv.appendChild(nodeElem); let ofsTitlePx = 0; if (checkbox) { checkboxSpan = document.createElement("i"); checkboxSpan.classList.add("wb-checkbox"); if (checkbox === "radio" || this.parent.radiogroup) { checkboxSpan.classList.add("wb-radio"); } nodeElem.appendChild(checkboxSpan); ofsTitlePx += ICON_WIDTH; } for (let i = level - 1; i > 0; i--) { elem = document.createElement("i"); elem.classList.add("wb-indent"); nodeElem.appendChild(elem); ofsTitlePx += ICON_WIDTH; } if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) { expanderSpan = document.createElement("i"); expanderSpan.classList.add("wb-expander"); nodeElem.appendChild(expanderSpan); ofsTitlePx += ICON_WIDTH; } // Render the icon (show a 'loading' icon if we do not have an expander that // we would prefer). const iconSpan = this._createIcon(nodeElem, null, !expanderSpan); if (iconSpan) { ofsTitlePx += ICON_WIDTH; } const titleSpan = document.createElement("span"); titleSpan.classList.add("wb-title"); nodeElem.appendChild(titleSpan); // this._callEvent("enhanceTitle", { titleSpan: titleSpan }); // Store the width of leading icons with the node, so we can calculate // the width of the embedded title span later nodeElem._ofsTitlePx = ofsTitlePx; // Support HTML5 drag-n-drop if (tree.options.dnd.dragStart) { nodeElem.draggable = true; } // Render columns const isColspan = this.isColspan(); if (!isColspan && columns.length > 1) { let colIdx = 0; for (const col of columns) { colIdx++; let colElem; if (col.id === "*") { colElem = nodeElem; } else { colElem = document.createElement("span"); colElem.classList.add("wb-col"); rowDiv.appendChild(colElem); } if (colIdx === activeColIdx) { colElem.classList.add("wb-active"); } // Add classes from `columns` definition to `` cells col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; if (isNew && col.html) { if (typeof col.html === "string") { colElem.innerHTML = col.html; } } } } // Attach to DOM as late as possible const after = opts ? opts.after : "last"; switch (after) { case "first": tree.nodeListElement.prepend(rowDiv); break; case "last": tree.nodeListElement.appendChild(rowDiv); break; default: opts.after.after(rowDiv); } // Now go on and fill in data and update classes opts.isNew = true; this._render_data(opts); } /** * Render `node.title`, `.icon` into an existing row. * * @see {@link WunderbaumNode._render} */ _render_data(opts) { assert(this._rowElem, "No _rowElem"); const tree = this.tree; const treeOptions = tree.options; const rowDiv = this._rowElem; const isNew = !!opts.isNew; // Called by _render_markup()? const preventScroll = !!opts.preventScroll; const columns = tree.columns; const isColspan = this.isColspan(); // Row markup already exists const nodeElem = rowDiv.querySelector("span.wb-node"); const titleSpan = nodeElem.querySelector("span.wb-title"); const scrollTop = tree.element.scrollTop; if (this.titleWithHighlight) { titleSpan.innerHTML = this.titleWithHighlight; } else { titleSpan.textContent = this.title; // TODO: this triggers scroll events } const tooltip = this.getOption("tooltip", false); if (tooltip) { titleSpan.title = tooltip === true ? this.title : tooltip; } // NOTE: At least on Safari, this render call triggers a scroll event // probably when a focused input is replaced. if (preventScroll) { tree.element.scrollTop = scrollTop; } // Set the width of the title span, so overflow ellipsis work if (!treeOptions.skeleton) { if (isColspan) { const vpWidth = tree.element.clientWidth; titleSpan.style.width = vpWidth - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } else { titleSpan.style.width = columns[0]._widthPx - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } } // Update row classes opts.isDataChange = true; this._render_status(opts); // Let user modify the result if (this.statusNodeType) { this._callEvent("renderStatusNode", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, }); } else if (this.parent) { // Skip root node const renderInfo = this._getRenderInfo(); this._callEvent("render", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, allColInfosById: renderInfo.allColInfosById, renderColInfosById: renderInfo.renderColInfosById, }); } } /** * Update row classes to reflect active, focuses, etc. * @see {@link WunderbaumNode._render} */ _render_status(opts) { // this.log("_render_status", opts); const tree = this.tree; const iconMap = tree.iconMap; const treeOptions = tree.options; const typeInfo = this.type ? tree.types[this.type] : null; const rowDiv = this._rowElem; // Row markup already exists const nodeSpan = rowDiv.querySelector("span.wb-node"); const expanderElem = nodeSpan.querySelector("i.wb-expander"); const checkboxElem = nodeSpan.querySelector("i.wb-checkbox"); const rowClasses = ["wb-row"]; this.expanded ? rowClasses.push("wb-expanded") : 0; this.lazy ? rowClasses.push("wb-lazy") : 0; this.selected ? rowClasses.push("wb-selected") : 0; this._partsel ? rowClasses.push("wb-partsel") : 0; this === tree.activeNode ? rowClasses.push("wb-active") : 0; this === tree.focusNode ? rowClasses.push("wb-focus") : 0; this._errorInfo ? rowClasses.push("wb-error") : 0; this._isLoading ? rowClasses.push("wb-loading") : 0; this.isColspan() ? rowClasses.push("wb-colspan") : 0; this.statusNodeType ? rowClasses.push("wb-status-" + this.statusNodeType) : 0; this.match ? rowClasses.push("wb-match") : 0; this.subMatchCount ? rowClasses.push("wb-submatch") : 0; treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0; // Replace previous classes: rowDiv.className = rowClasses.join(" "); // Add classes from `node.classes` this.classes ? rowDiv.classList.add(...this.classes) : 0; // Add classes from `tree.types[node.type]` if (typeInfo && typeInfo.classes) { rowDiv.classList.add(...typeInfo.classes); } if (expanderElem) { let image = null; if (this._isLoading) { image = iconMap.loading; } else if (this.isExpandable(false)) { if (this.expanded) { image = iconMap.expanderExpanded; } else { image = iconMap.expanderCollapsed; } } else if (this.lazy && this.children == null) { image = iconMap.expanderLazy; } if (image == null) { expanderElem.className = "wb-expander"; expanderElem.classList.add("wb-indent"); } else if (TEST_HTML.test(image)) { expanderElem.replaceWith(elemFromHtml(image)); } else if (TEST_FILE_PATH.test(image)) { expanderElem.style.backgroundImage = `url('${image}')`; } else { expanderElem.className = "wb-expander " + image; } } if (checkboxElem) { let cbclass = "wb-checkbox "; if (this.isRadio()) { cbclass += "wb-radio "; if (this.selected) { cbclass += iconMap.radioChecked; // } else if (this._partsel) { // cbclass += iconMap.radioUnknown; } else { cbclass += iconMap.radioUnchecked; } } else { if (this.selected) { cbclass += iconMap.checkChecked; } else if (this._partsel) { cbclass += iconMap.checkUnknown; } else { cbclass += iconMap.checkUnchecked; } } checkboxElem.className = cbclass; } // Fix active cell in cell-nav mode if (!opts.isNew) { let i = 0; for (const colSpan of rowDiv.children) { colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx); colSpan.classList.remove("wb-error", "wb-invalid"); } // Update icon (if not opts.isNew, which would rebuild markup anyway) const iconSpan = nodeSpan.querySelector("i.wb-icon"); if (iconSpan) { this._createIcon(nodeSpan, iconSpan, !expanderElem); } } // Adjust column width if (opts.resizeCols !== false && !this.isColspan()) { const colElems = rowDiv.querySelectorAll("span.wb-col"); let idx = 0; let ofs = 0; for (const colDef of this.tree.columns) { const colElem = colElems[idx]; colElem.style.left = `${ofs}px`; colElem.style.width = `${colDef._widthPx}px`; idx++; ofs += colDef._widthPx; } } } /* * Create or update node's markup. * * `options.change` defaults to ChangeType.data, which updates the title, * icon, and status. It also triggers the `render` event, that lets the user * create or update the content of embeded cell elements. * * If only the status or other class-only modifications have changed, * `options.change` should be set to ChangeType.status instead for best * efficiency. * * Calling `update()` is almost always a better alternative. * @see {@link WunderbaumNode.update} */ _render(options) { // this.log("render", options); const opts = Object.assign({ change: ChangeType.data }, options); if (!this._rowElem) { opts.change = ChangeType.row; } switch (opts.change) { case "status": this._render_status(opts); break; case "data": this._render_data(opts); break; case "row": // _rowElem is not yet created (asserted in _render_markup) this._render_markup(opts); break; default: error(`Invalid change type '${opts.change}'.`); } } /** * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad * event is triggered on next expand. */ resetLazy() { this.removeChildren(); this.expanded = false; this.lazy = true; this.children = null; this.tree.update(ChangeType.structure); } /** Convert node (or whole branch) into a plain object. * * The result is compatible with node.addChildren(). * * @param recursive include child nodes * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link Wunderbaum.toDictArray}. */ toDict(recursive = false, callback) { const dict = {}; NODE_DICT_PROPS.forEach((propName) => { const val = this[propName]; if (val instanceof Set) { // Convert Set to string (or skip if set is empty) val.size ? (dict[propName] = Array.prototype.join.call(val.keys(), " ")) : 0; } else if (val || val === false || val === 0) { dict[propName] = val; } }); if (!isEmptyObject(this.data)) { dict.data = extend({}, this.data); if (isEmptyObject(dict.data)) { delete dict.data; } } if (callback) { const res = callback(dict, this); if (res === false) { // Note: a return value of `false` is only used internally return false; // Don't include this node nor its children } if (res === "skip") { recursive = false; // Include this node, but not the children } } if (recursive) { if (isArray(this.children)) { dict.children = []; for (let i = 0, l = this.children.length; i < l; i++) { const node = this.children[i]; if (!node.isStatusNode()) { // Note: a return value of `false` is only used internally const res = node.toDict(true, callback); if (res !== false) { dict.children.push(res); } } } } } return dict; } /** Return an option value that has a default, but may be overridden by a * callback or a node instance attribute. * * Evaluation sequence: * * - If `tree.options.` is a callback that returns something, use that. * - Else if `node.` is defined, use that. * - Else if `tree.types[]` is a value, use that. * - Else if `tree.options.` is a value, use that. * - Else use `defaultValue`. * * @param name name of the option property (on node and tree) * @param defaultValue return this if nothing else matched * {@link Wunderbaum.getOption|Wunderbaum.getOption} */ getOption(name, defaultValue) { const tree = this.tree; let opts = tree.options; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [opts, name] = name.split("."); } const value = opts[name]; // ?? defaultValue; // A callback resolver always takes precedence if (typeof value === "function") { const res = value.call(tree, { type: "resolve", tree: tree, node: this, // typeInfo: this.type ? tree.types[this.type] : {}, }); if (res !== undefined) { return res; } } // If this node has an explicit local setting, use it: if (this[name] !== undefined) { return this[name]; } // Use value from type definition if defined const typeInfo = this.type ? tree.types[this.type] : undefined; const res = typeInfo ? typeInfo[name] : undefined; if (res !== undefined) { return res; } // Use value from value options dict, fallback do default return value !== null && value !== void 0 ? value : defaultValue; } /** Make sure that this node is visible in the viewport. * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo} */ async scrollIntoView(options) { const opts = Object.assign({ node: this }, options); return this.tree.scrollTo(opts); } /** * Activate this node, deactivate previous, send events, activate column and * scroll into viewport. */ async setActive(flag = true, options) { const tree = this.tree; const prev = tree.getActiveNode(); const retrigger = options === null || options === void 0 ? void 0 : options.retrigger; // Default: false const focusTree = options === null || options === void 0 ? void 0 : options.focusTree; // Default: false // const focusNode = options?.focusNode !== false; // Default: true const noEvents = options === null || options === void 0 ? void 0 : options.noEvents; // Default: false const orgEvent = options === null || options === void 0 ? void 0 : options.event; // Default: null const colIdx = options === null || options === void 0 ? void 0 : options.colIdx; // Default: null const edit = options === null || options === void 0 ? void 0 : options.edit; // Default: false // util.assert(!colIdx || tree.isCellNav(), "colIdx requires cellNav"); assert(!edit || colIdx != null, "edit requires colIdx"); if (!noEvents) { if (flag) { if (prev !== this || retrigger) { if ((prev === null || prev === void 0 ? void 0 : prev._callEvent("deactivate", { nextNode: this, event: orgEvent, })) === false || this._callEvent("beforeActivate", { prevNode: prev, event: orgEvent, }) === false) { return; } tree._setActiveNode(null); prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status); } } else if (prev === this || retrigger) { this._callEvent("deactivate", { nextNode: null, event: orgEvent }); } } if (prev !== this) { if (flag) { tree._setActiveNode(this); } prev === null || prev === void 0 ? void 0 : prev.update(ChangeType.status); this.update(ChangeType.status); } return this.makeVisible().then(() => { if (flag) { if (focusTree || edit) { tree.setFocus(); tree._setFocusNode(this); tree.focusNode.setFocus(); } // if (focusNode || edit) { // tree.focusNode = this; // tree.focusNode.setFocus(); // } if (colIdx != null && tree.isCellNav()) { tree.setColumn(colIdx, { edit: edit }); } if (!noEvents) { this._callEvent("activate", { prevNode: prev, event: orgEvent }); } } }); } /** * Expand or collapse this node. */ async setExpanded(flag = true, options) { const { force, scrollIntoView, immediate, resetLazy } = options !== null && options !== void 0 ? options : {}; const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events if (!flag && this.isExpanded() && this.getLevel() <= this.tree.getOption("minExpandLevel") && !force) { this.logDebug("Ignored collapse request below minExpandLevel."); return; } if (!flag === !this.expanded) { return; // Nothing to do } if (sendEvents && this._callEvent("beforeExpand", { flag: flag }) === false) { return; } // this.log("setExpanded()"); if (flag && this.getOption("autoCollapse")) { this.collapseSiblings(options); } if (flag && this.lazy && this.children == null) { await this.loadLazy(); } else if (!flag && resetLazy && this.lazy && this.children) { this.resetLazy(); } this.expanded = flag; const updateOpts = { immediate: immediate }; // const updateOpts = { immediate: !!util.getOption(options, "immediate") }; this.tree.update(ChangeType.structure, updateOpts); if (flag && scrollIntoView) { const lastChild = this.getLastChild(); if (lastChild) { this.tree.updatePendingModifications(); lastChild.scrollIntoView({ topNode: this }); } } if (sendEvents) { this._callEvent("expand", { flag: flag }); } } /** * Set keyboard focus here. * @see {@link setActive} */ setFocus(flag = true) { assert(!!flag, "Blur is not yet implemented"); const prev = this.tree.focusNode; this.tree._setFocusNode(this); prev === null || prev === void 0 ? void 0 : prev.update(); this.update(); } /** Set a new icon path or class. */ setIcon(icon) { this.icon = icon; this.update(); } /** Change node's {@link key} and/or {@link refKey}. */ setKey(key, refKey) { throw new Error("Not yet implemented"); } // /** // * Calculate a *stable*, unique key for this node from its refKey (or title). // * We also add information from the parent, because a refKey may occur multiple // * times in a tree. // */ // calcUniqueKey() { // // Assuming that the parent's key was calculated the same way, we implicitly // // involve the whole refKey-path: // const s = this.key + (this.refKey || this.title); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // const h1 = util.murmurHash3(s, true); // return "id_" + h1 + util.murmurHash3(h1 + s, true); // // const l = []; // // // eslint-disable-next-line @typescript-eslint/no-this-alias // // let node: WunderbaumNode = this; // // while (node.parent) { // // l.unshift(node.refKey || node.key); // // node = node.parent; // // } // // const path = l.join("/"); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // // const h1 = util.murmurHash3(path, true); // // return "id_" + h1 + util.murmurHash3(h1 + path, true); // } /** * Trigger a repaint, typically after a status or data change. * * `change` defaults to 'data', which handles modifcations of title, icon, * and column content. It can be reduced to 'ChangeType.status' if only * active/focus/selected state has changed. * * This method will eventually call {@link WunderbaumNode._render} with * default options, but may be more consistent with the tree's * {@link Wunderbaum.update} API. */ update(change = ChangeType.data) { assert(change === ChangeType.status || change === ChangeType.data, `Invalid change type ${change}`); this.tree.update(change, this); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents = false) { const nodeList = []; this.visit((node) => { if (node.selected) { nodeList.push(node); if (stopOnParents === true) { return "skip"; // stop processing this branch } } }); return nodeList; } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false) { const refKeys = new Set(); this.visit((node) => { if (node.refKey != null && (!selected || node.selected)) { refKeys.add(node.refKey); } }); return Array.from(refKeys); } /** Toggle the check/uncheck state. */ toggleSelected(options) { let flag = this.isSelected(); if (flag === undefined && !this.isRadio()) { flag = this._anySelectable(); } else { flag = !flag; } return this.setSelected(flag, options); } /** Return true if at least on selectable descendant end-node is unselected. @internal */ _anySelectable() { let found = false; this.visit((node) => { if (node.selected === false && !node.unselectable && !node.hasChildren() && !node.parent.radiogroup) { found = true; return false; // Stop iteration } }); return found; } /* Apply selection state to a single node. */ _changeSelectStatusProps(state) { let changed = false; switch (state) { case false: changed = this.selected || this._partsel; this.selected = false; this._partsel = false; break; case true: changed = !this.selected || !this._partsel; this.selected = true; this._partsel = true; break; case undefined: changed = this.selected || !this._partsel; this.selected = false; // #110: end nodess cannot have a `_partsel` flag this._partsel = this.hasChildren() ? true : false; break; default: error(`Invalid state: ${state}`); } if (changed) { this.update(); } return changed; } /** * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`. * This includes (de)selecting all descendants. */ fixSelection3AfterClick(opts) { const force = !!(opts === null || opts === void 0 ? void 0 : opts.force); const flag = this.isSelected(); this.visit((node) => { if (node.radiogroup) { return "skip"; // Don't (de)select this branch } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(flag); } }); this.fixSelection3FromEndNodes(); } /** * Fix selection status for multi-hier mode. * Only end-nodes are considered to update the descendants branch and parents. * Should be called after this node has loaded new children or after * children have been modified using the API. */ fixSelection3FromEndNodes(opts) { const force = !!(opts === null || opts === void 0 ? void 0 : opts.force); assert(this.tree.options.selectMode === "hier", "expected selectMode 'hier'"); // Visit all end nodes and adjust their parent's `selected` and `_partsel` // attributes. Return selection state true, false, or undefined. const _walk = (node) => { let state; const children = node.children; if (children && children.length) { // check all children recursively let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; // the selection state of a node is not relevant; we need the end-nodes const s = _walk(child); if (s !== false) { someSelected = true; } if (s !== true) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; } else { // This is an end-node: simply report the status state = !!node.selected; } // #939: Keep a `_partsel` flag that was explicitly set on a lazy node if (node._partsel && !node.selected && node.lazy && node.children == null) { state = undefined; } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(state); } return state; }; _walk(this); // Update parent's state this.visitParents((node) => { let state; const children = node.children; let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; state = !!child.selected; // When fixing the parents, we trust the sibling status (i.e. we don't recurse) if (state || child._partsel) { someSelected = true; } if (!state) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; node._changeSelectStatusProps(state); }); } /** Modify the check/uncheck state. */ setSelected(flag = true, options) { const tree = this.tree; const sendEvents = !(options === null || options === void 0 ? void 0 : options.noEvents); // Default: send events const prev = this.isSelected(); const isRadio = this.parent && this.parent.radiogroup; const selectMode = tree.options.selectMode; const canSelect = (options === null || options === void 0 ? void 0 : options.force) || !this.getOption("unselectable"); flag = !!flag; // this.logDebug(`setSelected(${flag})`, this); if (!canSelect) { return prev; } if ((options === null || options === void 0 ? void 0 : options.propagateDown) && selectMode === "multi") { tree.runWithDeferredUpdate(() => { this.visit((node) => { node.setSelected(flag); }); }); return prev; } if (flag === prev || (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false)) { return prev; } tree.runWithDeferredUpdate(() => { if (isRadio) { // Radiobutton Group if (!flag && !(options === null || options === void 0 ? void 0 : options.force)) { return prev; // don't uncheck radio buttons } for (const sibling of this.parent.children) { sibling.selected = sibling === this; } } else { this.selected = flag; if (selectMode === "hier") { this.fixSelection3AfterClick(); } else if (selectMode === "single" && flag) { tree.visit((n) => { if (n !== this) { n.selected = false; } }); } } }); if (sendEvents) { this._callEvent("select", { flag: flag }); } return prev; } /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */ setStatus(status, options) { const tree = this.tree; const message = options === null || options === void 0 ? void 0 : options.message; const details = options === null || options === void 0 ? void 0 : options.details; let statusNode = null; const _clearStatusNode = () => { // Remove dedicated dummy node, if any const children = this.children; if (children && children.length && children[0].isStatusNode()) { children[0].remove(); } }; const _setStatusNode = (data) => { // Create/modify the dedicated dummy node for 'loading...' or // 'error!' status. (only called for direct child of the invisible // system root) const children = this.children; const firstChild = children ? children[0] : null; assert(data.statusNodeType, "Not a status node"); assert(!firstChild || !firstChild.isStatusNode(), "Child must not be a status node"); statusNode = this.addNode(data, "prependChild"); statusNode.match = -1; // Mark as 'match' to avoid hiding tree.update(ChangeType.structure); return statusNode; }; _clearStatusNode(); switch (status) { case "ok": this._isLoading = false; this._errorInfo = null; break; case "loading": this._isLoading = true; this._errorInfo = null; if (this.parent) { this.update(ChangeType.status); } else { // If this is the invisible root, add a visible top-level node _setStatusNode({ statusNodeType: status, title: tree.options.strings.loading + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, tooltip: details, }); } // this.update(); break; case "error": _setStatusNode({ statusNodeType: status, title: tree.options.strings.loadError + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, // classes: "wb-center", tooltip: details, }); this._isLoading = false; this._errorInfo = { message: message, details: details }; break; case "noData": _setStatusNode({ statusNodeType: status, title: message || tree.options.strings.noData, checkbox: false, colspan: true, tooltip: details, }); this._isLoading = false; this._errorInfo = null; break; default: error("invalid node status " + status); } tree.update(ChangeType.structure); return statusNode; } /** Rename this node. */ setTitle(title) { this.title = title; this.update(); // this.triggerModify("rename"); // TODO } /** Set the node tooltip. */ setTooltip(tooltip) { this.tooltip = tooltip; this.update(); } /** * Sort child list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp = nodeTitleSorter, deep = false) { this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep }); } /** * Renumber nodes `_nativeIndex`. This is useful to allow to restore the * order after sorting a column. * This method is automatically called after loading new child nodes. * @since 0.11.0 */ resetNativeChildOrder(options) { const { recursive = true, propName = "_nativeIndex" } = options !== null && options !== void 0 ? options : {}; if (this.children) { this.children.forEach((child, i) => { child.data[propName] = i; if (recursive && child.children) { child.resetNativeChildOrder(options); } }); } } /** * Convenience method to implement column sorting. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options) { this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" }); return this.sort(options); } /** * Implement column sorting. * @since 0.14.0 */ sort(options) { const tree = this.tree; let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined, // Support click on column sort header: updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options; propName !== null && propName !== void 0 ? propName : (propName = colId); if (propName === "*") { propName = "title"; } const isFolder = tree.options.sortFoldersFirst === true ? (node) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER : tree.options.sortFoldersFirst; if (updateColInfo) { const colDef = this.tree["_columnsById"][options.colId]; assert(colDef, `Invalid colId specified: ${options.colId}`); order !== null && order !== void 0 ? order : (order = rotate(colDef.sortOrder, ["asc", "desc", undefined])); for (const col of this.tree.columns) { col.sortOrder = col === colDef ? order : undefined; } if (order === undefined) { propName = nativeOrderPropName; order = "asc"; } this.tree.update(ChangeType.colStructure); } else { propName !== null && propName !== void 0 ? propName : (propName = "title"); order !== null && order !== void 0 ? order : (order = "asc"); } this.logDebug(`sort(), propName=${propName}, ${order}`, options); assert(propName || cmp || key, "No `propName` or `key` specified"); // Define a key callback from the parameters we have if (key == null && cmp == null) { key = (node) => { let val; if (NODE_DICT_PROPS.has(propName)) { val = node[propName]; } else { val = node.data[propName]; } if (caseInsensitive && typeof val === "string") { val = val.toLowerCase(); } return val; }; } // Define a compare callback that uses the key callback if (cmp) { assert(!key, "`key` and `cmp` are mutually exclusive"); tree.logDeprecate("SortOptions.cmp", { since: "0.14.0", hint: "use the `key` callback instead", }); } else { if (options.propName || options.caseInsensitive) { tree.logWarn("sort(): ignoring propName, caseInsensitive"); } cmp = (a, b) => { if (isFolder) { const isFolderA = isFolder(a); if (isFolderA !== isFolder(b)) { return isFolderA ? -1 : 1; } } let x = key(a); let y = key(b); // Assure we have reasonable comparisons with null values: if (x == null) { x = typeof y === "string" ? "" : 0; } else if (typeof x === "boolean") { x = x ? 1 : 0; } if (y == null) { y = typeof x === "string" ? "" : 0; } else if (typeof y === "boolean") { y = y ? 1 : 0; } if (order === "desc") { return x === y ? 0 : x > y ? -1 : 1; } return x === y ? 0 : x > y ? 1 : -1; }; } function _sortChildren(cl) { if (!cl) { return; } cl.sort(cmp); if (deep) { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].children) { _sortChildren(cl[i].children); } } } } if (this.children) { _sortChildren(this.children); } this.tree.update(ChangeType.structure); // this.triggerModify("sort"); // TODO } /** * Re-apply current sorting if any (use after lazy load). * Example: * ```js * load: function (e) { * // Whe loading a lazy branch, apply current sort order if any * e.node.resort(); * }, * ``` * @since 0.14.0 */ resort(options = {}) { for (const colDef of this.tree.columns) { if (colDef.sortOrder) { options.colId = colDef.id; options.order = colDef.sortOrder; this.sort(options); break; } } } /** * Trigger `modifyChild` event on a parent to signal that a child was modified. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ triggerModifyChild(operation, child, extra) { this.logDebug(`modifyChild(${operation})`, extra, child); if (!this.tree.options.modifyChild) { return; } if (child && child.parent !== this) { error("child " + child + " is not a child of " + this); } this._callEvent("modifyChild", extend({ operation: operation, child: child }, extra)); } /** * Trigger `modifyChild` event on node.parent(!). * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... * @param {object} [extra] */ triggerModify(operation, extra) { // if (!this.parent) { // return; // } this.parent.triggerModifyChild(operation, this, extra); } /** * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order). * * Stop iteration, if fn() returns false. Skip current branch, if fn() * returns "skip".
* Return false if iteration was stopped. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * its children only. * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link Wunderbaum.visit}. */ visit(callback, includeSelf = false) { let res = true; const children = this.children; if (includeSelf === true) { res = callback(this); if (res === false || res === "skip") { return res; } } if (children) { for (let i = 0, l = children.length; i < l; i++) { res = children[i].visit(callback, true); if (res === false) { break; } } } return res; } /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.
* Stop iteration, if callback() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. Return false to stop iteration */ visitParents(callback, includeSelf = false) { if (includeSelf && callback(this) === false) { return false; } let p = this.parent; while (p) { if (callback(p) === false) { return false; } p = p.parent; } return true; } /** * Call fn(node) for all sibling nodes.
* Stop iteration, if fn() returns false.
* Return false if iteration was stopped. * * @param callback the callback function. * Return false to stop iteration. * @param includeSelf include this node in the iteration. */ visitSiblings(callback, includeSelf = false) { const ac = this.parent.children; for (let i = 0, l = ac.length; i < l; i++) { const n = ac[i]; if (includeSelf || n !== this) { if (callback(n) === false) { return false; } } } return true; } /** * [ext-filter] Return true if this node is matched by current filter (or no filter is active). */ isMatched() { return !(this.tree.filterMode && !this.match); } } WunderbaumNode.sequence = 0; /*! * Wunderbaum - ext-edit * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * v0.14.1, Sun, 22 Mar 2026 05:52:05 GMT (https://github.com/mar10/wunderbaum) */ // const START_MARKER = "\uFFF7"; class EditExtension extends WunderbaumExtension { constructor(tree) { super(tree, "edit", { debounce: 100, minlength: 1, maxlength: null, trigger: [], //["clickActive", "F2", "macEnter"], trim: true, select: true, slowClickDelay: 1000, // Handle 'clickActive' only if last click is less than this old (0: always) validity: true, //"Please enter a title", // --- Events --- // (note: there is also the `tree.change` event.) beforeEdit: null, edit: null, apply: null, }); this.curEditNode = null; this.relatedNode = null; this.debouncedOnChange = debounce(this._onChange.bind(this), this.getPluginOption("debounce")); } /* * Call an event handler, while marking the current node cell 'busy'. * Deal with returned promises and ValidationError. * Convert a ValidationError into a input.setCustomValidity() call and vice versa. */ async _applyChange(eventName, node, colElem, inputElem, extra) { node.log(`_applyChange(${eventName})`, extra); colElem.classList.add("wb-busy"); colElem.classList.remove("wb-error", "wb-invalid"); inputElem.setCustomValidity(""); // Call event handler either ('change' or 'edit.appy'), which may return a // promise or a scalar value or throw a ValidationError. return new Promise((resolve, reject) => { const res = node._callEvent(eventName, extra); // normalize to promise, even if a scalar value was returned and await it Promise.resolve(res) .then((res) => { resolve(res); }) .catch((err) => { reject(err); }); }) .then((res) => { if (!inputElem.checkValidity()) { // Native validation failed or handler called 'inputElem.setCustomValidity()' node.logWarn("inputElem.checkValidity() failed: throwing...."); throw new ValidationError(inputElem.validationMessage); } return res; }) .catch((err) => { if (err instanceof ValidationError) { node.logWarn("catched ", err); colElem.classList.add("wb-invalid"); if (inputElem.setCustomValidity && !inputElem.validationMessage) { inputElem.setCustomValidity(err.message); } if (inputElem.validationMessage) { inputElem.reportValidity(); } // throw err; } else { node.logError(`Error in ${eventName} event handler (throw e.util.ValidationError instead if this was intended)`, err); colElem.classList.add("wb-error"); throw err; } }) .finally(() => { colElem.classList.remove("wb-busy"); }); } /* * Called for when a control that is embedded in a cell fires a `change` event. */ _onChange(e) { const info = Wunderbaum.getEventInfo(e); const node = info.node; const colElem = info.colElem; if (!node || info.colIdx === 0) { this.tree.log("Ignored change event for removed element or node title"); return; } // See also WbChangeEventType this._applyChange("change", node, colElem, e.target, { info: info, event: e, inputElem: e.target, inputValue: Wunderbaum.util.getValueFromElem(e.target), inputValid: e.target.checkValidity(), }); } init() { super.init(); onEvent(this.tree.element, "change", //"change input", ".contenteditable,input,textarea,select", // #61: we must not debounce the `change`, event.target may be reset to null // when the debounced handler is called. // (e) => { // this.debouncedOnChange(e); // } (e) => this._onChange(e)); } /* Called by ext_keynav to pre-process input. */ _preprocessKeyEvent(data) { const event = data.event; const eventName = eventToString(event); const tree = this.tree; const trigger = this.getPluginOption("trigger"); // const inputElem = // event.target && event.target.closest("input,[contenteditable]"); // tree.logDebug(`_preprocessKeyEvent: ${eventName}, editing:${this.isEditingTitle()}`); // --- Title editing: apply/discard --- // if (inputElem) { if (this.isEditingTitle()) { switch (eventName) { case "Enter": this._stopEditTitle(true, { event: event }); return false; case "Escape": this._stopEditTitle(false, { event: event }); return false; } // If the event target is an input element or `contenteditable="true"`, // we ignore it as navigation command return false; } // --- Trigger title editing if (tree.isRowNav() || tree.activeColIdx === 0) { switch (eventName) { case "Enter": if (trigger.indexOf("macEnter") >= 0 && isMac) { this.startEditTitle(); return false; } break; case "F2": if (trigger.indexOf("F2") >= 0) { this.startEditTitle(); return false; } break; } return true; } return true; } /** Return true if a title is currently being edited. */ isEditingTitle(node) { return node ? this.curEditNode === node : !!this.curEditNode; } /** Start renaming, i.e. replace the title with an embedded ``. */ startEditTitle(node) { node = node !== null && node !== void 0 ? node : this.tree.getActiveNode(); const validity = this.getPluginOption("validity"); const select = this.getPluginOption("select"); if (!node) { return; } if (node.isStatusNode()) { node.logWarn("Cannot edit status node."); return; } this.tree.logDebug(`startEditTitle(node=${node})`); let inputHtml = node._callEvent("edit.beforeEdit"); if (inputHtml === false) { node.logDebug("beforeEdit canceled operation."); return; } // `beforeEdit(e)` may return an input HTML string. Otherwise use a default // (we also treat a `true` return value as 'use default'): if (inputHtml === true || !inputHtml) { const title = escapeHtml(node.title); let opt = this.getPluginOption("maxlength"); const maxlength = opt ? ` maxlength="${opt}"` : ""; opt = this.getPluginOption("minlength"); const minlength = opt ? ` minlength="${opt}"` : ""; const required = opt > 0 ? " required" : ""; inputHtml = ``; } const titleSpan = node .getColElem(0) .querySelector(".wb-title"); titleSpan.innerHTML = inputHtml; const inputElem = titleSpan.firstElementChild; if (validity) { // Permanently apply input validations (CSS and tooltip) inputElem.addEventListener("keydown", (e) => { inputElem.setCustomValidity(""); if (!inputElem.reportValidity()) { node.logWarn(`Invalid input: '${inputElem.value}'`); } }); } inputElem.focus(); if (select) { inputElem.select(); } this.curEditNode = node; node._callEvent("edit.edit", { inputElem: inputElem, }); } /** * * @param apply * @returns */ stopEditTitle(apply) { return this._stopEditTitle(apply, {}); } /* * * @param apply * @param opts.canKeepOpen */ _stopEditTitle(apply, options) { var _a; options !== null && options !== void 0 ? options : (options = {}); const focusElem = document.activeElement; let newValue = focusElem ? getValueFromElem(focusElem) : null; const node = this.curEditNode; const forceClose = !!options.forceClose; const validity = this.getPluginOption("validity"); if (newValue && this.getPluginOption("trim")) { newValue = newValue.trim(); } if (!node) { // this.tree.logDebug("stopEditTitle: not in edit mode."); return; } node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue); if (apply && newValue !== null && newValue !== node.title) { const errMsg = focusElem.validationMessage; if (errMsg) { // input element's native validation failed throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`); } const colElem = node.getColElem(0); this._applyChange("edit.apply", node, colElem, focusElem, { oldValue: node.title, newValue: newValue, inputElem: focusElem, inputValid: focusElem.checkValidity(), }).then((value) => { var _a; const errMsg = focusElem.validationMessage; if (validity && errMsg && value !== false) { // Handler called 'inputElem.setCustomValidity()' to signal error throw new Error(`Edit apply validation failed for "${newValue}": ${errMsg}.`); } // Discard the embedded `` // node.logDebug("applyChange:", value, forceClose) if (!forceClose && value === false) { // Keep open return; } node === null || node === void 0 ? void 0 : node.setTitle(newValue); // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; this.tree.setFocus(); // restore focus that was in the input element }); // .catch((err) => { // node.logError(err); // }); // Trigger 'change' event for embedded `` // focusElem.blur(); } else { // Discard the embedded `` // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. (_a = this.curEditNode) === null || _a === void 0 ? void 0 : _a._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; // We discarded the , so we have to acquire keyboard focus again this.tree.setFocus(); } } /** * Create a new child or sibling node and start edit mode. */ createNode(mode = "after", node, init) { const tree = this.tree; node = node !== null && node !== void 0 ? node : tree.getActiveNode(); assert(node, "No node was passed, or no node is currently active."); // const validity = this.getPluginOption("validity"); mode = mode || "prependChild"; if (init == null) { init = { title: "" }; } else if (typeof init === "string") { init = { title: init }; } else { assert(isPlainObject(init), `Expected a plain object: ${init}`); } // Make sure node is expanded (and loaded) in 'child' mode if ((mode === "prependChild" || mode === "appendChild") && (node === null || node === void 0 ? void 0 : node.isExpandable(true))) { node.setExpanded().then(() => { this.createNode(mode, node, init); }); return; } const newNode = node.addNode(init, mode); newNode.setClass("wb-edit-new"); this.relatedNode = node; // Don't filter new nodes: newNode.match = -1; newNode.makeVisible({ noAnimation: true }).then(() => { this.startEditTitle(newNode); }); } } /*! * wunderbaum.ts * * A treegrid control. * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). * https://github.com/mar10/wunderbaum * * Released under the MIT license. * @version v0.14.1 * @date Sun, 22 Mar 2026 05:52:05 GMT */ // import "./wunderbaum.scss"; class WbSystemRoot extends WunderbaumNode { constructor(tree) { super(tree, null, { key: "__root__", title: tree.id, }); } toString() { return `WbSystemRoot@${this.key}<'${this.tree.id}'>`; } } /** * A persistent plain object or array. * * See also {@link WunderbaumOptions}. */ class Wunderbaum { /** Currently active node if any. * Use {@link WunderbaumNode.setActive|setActive} to modify. */ get activeNode() { var _a; // Check for deleted node, i.e. node.tree === null return ((_a = this._activeNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._activeNode : null; } /** Current node hat has keyboard focus if any. * Use {@link WunderbaumNode.setFocus|setFocus()} to modify. */ get focusNode() { var _a; // Check for deleted node, i.e. node.tree === null return ((_a = this._focusNode) === null || _a === void 0 ? void 0 : _a.tree) ? this._focusNode : null; } constructor(options) { this.enabled = true; /** Contains additional data that was sent as response to an Ajax source load request. */ this.data = {}; this.extensionList = []; this.extensions = {}; this.keyMap = new Map(); this.refKeyMap = new Map(); this.treeRowCount = 0; this._disableUpdateCount = 0; this._disableUpdateIgnoreCount = 0; this._activeNode = null; this._focusNode = null; this._initialSource = null; /** Shared properties, referenced by `node.type`. */ this.types = {}; /** List of column definitions. */ this.columns = []; this._columnsById = {}; // Modification Status this.pendingChangeTypes = new Set(); /** Expose some useful methods of the util.ts module as `tree._util`. */ this._util = util; // --- SELECT --- // public selectRangeAnchor: WunderbaumNode | null = null; // --- BREADCRUMB --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ this.breadcrumb = null; // --- FILTER --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ this.filterMode = null; // --- KEYNAV --- /** @internal Use `setColumn()`/`getActiveColElem()` to access. */ this.activeColIdx = 0; /** @internal */ this._cellNavMode = false; /** @internal */ this.lastQuicksearchTime = 0; /** @internal */ this.lastQuicksearchTerm = ""; // --- EDIT --- this.lastClickTime = 0; // Set default options and merge with user options const initOptions = Object.assign({ id: undefined, source: [], // URL for GET/PUT, Ajax options, or callback element: unsafeCast(null), debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose header: null, // Show/hide header (pass bool or string) rowHeightPx: DEFAULT_ROW_HEIGHT, iconMap: "bootstrap", columns: [], //util.unsafeCast(null), types: {}, enabled: true, fixedCol: false, showSpinner: false, checkbox: false, minExpandLevel: 0, emptyChildListExpandable: false, skeleton: false, autoCollapse: false, adjustHeight: true, connectTopBreadcrumb: null, columnsFilterable: false, columnsMenu: false, columnsResizable: false, columnsSortable: false, selectMode: "multi", // SelectModeType scrollIntoViewOnExpandClick: true, // --- Extensions (actually set by exensions on init) dnd: unsafeCast(null), edit: unsafeCast(null), filter: unsafeCast(null), // --- KeyNav --- navigationModeOption: unsafeCast(null), quicksearch: true, // --- Events --- // iconBadge: null, // change: null, // ... // --- Strings --- strings: { loadError: "Error", loading: "Loading...", noData: "No data", breadcrumbDelimiter: " » ", queryResult: "Found ${matches} of ${count}", noMatch: "No results", matchIndex: "${match} of ${matches}", }, }, options); const opts = initOptions; this.options = opts; const readyDeferred = new Deferred(); this.ready = readyDeferred.promise(); let readyOk = false; this.ready .then(() => { readyOk = true; try { this._callEvent("init"); } catch (error) { // We re-raise in the reject handler, but Chrome resets the stack // frame then, so we log it here: this.logError("Exception inside `init(e)` event:", error); } }) .catch((err) => { if (readyOk) { // Error occurred in `init` handler. We can re-raise, but Chrome // resets the stack frame. throw err; } else { // Error in load process this._callEvent("init", { error: err }); } }); this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence; delete initOptions.id; this.root = new WbSystemRoot(this); this._registerExtension(new KeynavExtension(this)); this._registerExtension(new EditExtension(this)); this._registerExtension(new FilterExtension(this)); this._registerExtension(new DndExtension(this)); this._registerExtension(new GridExtension(this)); this._registerExtension(new LoggerExtension(this)); this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {}); // --- Evaluate options this.columns = initOptions.columns || []; delete initOptions.columns; if (!this.columns || !this.columns.length) { const title = typeof opts.header === "string" ? opts.header : this.id; this.columns = [{ id: "*", title: title, width: "*" }]; } if (initOptions.types) { this.setTypes(initOptions.types, true); } delete initOptions.types; // --- Create Markup this.element = elemFromSelector(initOptions.element); assert(!!this.element, `Invalid 'element' option: ${initOptions.element}`); delete initOptions.element; this.element.classList.add("wunderbaum"); if (!this.element.getAttribute("tabindex")) { this.element.tabIndex = 0; } if (opts.rowHeightPx !== DEFAULT_ROW_HEIGHT) { this.element.style.setProperty("--wb-row-outer-height", opts.rowHeightPx + "px"); this.element.style.setProperty("--wb-row-inner-height", opts.rowHeightPx - 2 + "px"); } // Attach tree instance to
this.element._wb_tree = this; // Create header markup, or take it from the existing html this.headerElement = this.element.querySelector("div.wb-header"); const wantHeader = opts.header == null ? this.columns.length > 1 : !!opts.header; if (this.headerElement) { // User existing header markup to define `this.columns` assert(!this.columns, "`opts.columns` must not be set if table markup already contains a header"); this.columns = []; const rowElement = this.headerElement.querySelector("div.wb-row"); for (const colDiv of rowElement.querySelectorAll("div")) { this.columns.push({ id: colDiv.dataset.id || `col_${this.columns.length}`, // id: colDiv.dataset.id || null, title: "" + colDiv.textContent, // text: "" + colDiv.textContent, width: "*", // TODO: read from header span }); } } else { // We need a row div, the rest will be computed from `this.columns` const coldivs = "".repeat(this.columns.length); this.element.innerHTML = `
${coldivs}
`; if (!wantHeader) { const he = this.element.querySelector("div.wb-header"); he.style.display = "none"; } } // this.element.innerHTML += `
`; this.listContainerElement = this.element.querySelector("div.wb-list-container"); this.nodeListElement = this.listContainerElement.querySelector("div.wb-node-list"); this.headerElement = this.element.querySelector("div.wb-header"); this.element.classList.toggle("wb-grid", this.columns.length > 1); if (this.options.connectTopBreadcrumb) { this.breadcrumb = elemFromSelector(this.options.connectTopBreadcrumb); assert(!this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.`); this.breadcrumb.addEventListener("click", (e) => { // const node = Wunderbaum.getNode(e)!; const elem = e.target; if (elem && elem.matches("a.wb-breadcrumb")) { const node = this.keyMap.get(elem.dataset.key); node === null || node === void 0 ? void 0 : node.setActive(); e.preventDefault(); } }); } this._initExtensions(); // --- apply initial options ["enabled", "fixedCol"].forEach((optName) => { if (opts[optName] != null) { this.setOption(optName, opts[optName]); } }); // --- Load initial data if (initOptions.source) { if (opts.showSpinner) { this.nodeListElement.innerHTML = `${opts.strings.loading}`; } this.load(initOptions.source) .then(() => { // The source may have defined columns, so we may adjust the nav mode if (opts.navigationModeOption == null) { if (this.isGrid()) { this.setNavigationOption(NavModeEnum.cell); } else { this.setNavigationOption(NavModeEnum.row); } } else { this.setNavigationOption(opts.navigationModeOption); } this.update(ChangeType.structure, { immediate: true }); readyDeferred.resolve(); }) .catch((error) => { readyDeferred.reject(error); }) .finally(() => { var _a; (_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove(); this.element.classList.remove("wb-initializing"); }); } else { readyDeferred.resolve(); } // Async mode is sometimes required, because this.element.clientWidth // has a wrong value at start??? this.update(ChangeType.any); // --- Bind listeners this._registerEventHandlers(); this.resizeObserver = new ResizeObserver((entries) => { // this.log("ResizeObserver: Size changed", entries); this.update(ChangeType.resize); }); this.resizeObserver.observe(this.element); } _registerEventHandlers() { this.element.addEventListener("scroll", (e) => { // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e); this.update(ChangeType.scroll); }); onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => { var _a, _b; const info = Wunderbaum.getEventInfo(e); const command = (_b = (_a = e.target) === null || _a === void 0 ? void 0 : _a.dataset) === null || _b === void 0 ? void 0 : _b.command; this._callEvent("buttonClick", { event: e, info: info, command: command, }); }); onEvent(this.nodeListElement, "click", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; const mouseEvent = e; // this.log("click", info); if (this._callEvent("click", { event: e, node: node, info: info }) === false) { this.lastClickTime = Date.now(); return false; } if (node) { if (mouseEvent.ctrlKey) { node.toggleSelected(); return; } // Edit title if 'clickActive' is triggered: const trigger = this.getOption("edit.trigger"); const slowClickDelay = this.getOption("edit.slowClickDelay"); if (trigger.indexOf("clickActive") >= 0 && info.region === "title" && node.isActive() && (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay)) { node.startEditTitle(); } if (info.region === NodeRegion.expander) { node.setExpanded(!node.isExpanded(), { scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false, }); } else if (info.region === NodeRegion.checkbox) { node.toggleSelected(); } else { if (info.colIdx >= 0) { node.setActive(true, { colIdx: info.colIdx, event: e }); } else { node.setActive(true, { event: e }); } } } this.lastClickTime = Date.now(); }); onEvent(this.nodeListElement, "dblclick", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; // this.log("dblclick", info, e); if (this._callEvent("dblclick", { event: e, node: node, info: info }) === false) { return false; } if (node && info.colIdx === 0 && node.isExpandable() && info.region !== NodeRegion.expander) { this._callMethod("edit._stopEditTitle"); node.setExpanded(!node.isExpanded()); } }); onEvent(this.element, "keydown", (e) => { const info = Wunderbaum.getEventInfo(e); const eventName = eventToString(e); const node = info.node || this.getFocusNode(); this._callHook("onKeyEvent", { event: e, node: node, info: info, eventName: eventName, }); }); onEvent(this.element, "focusin focusout", (e) => { const flag = e.type === "focusin"; const targetNode = Wunderbaum.getNode(e); this._callEvent("focus", { flag: flag, event: e }); if (flag && this.isRowNav() && !this.isEditingTitle()) { if (this.options.navigationModeOption === NavModeEnum.row) { targetNode === null || targetNode === void 0 ? void 0 : targetNode.setActive(); } else { this.setCellNav(); } } if (!flag) { this._callMethod("edit._stopEditTitle", true, { event: e, forceClose: true, }); } }); } /** * Return a Wunderbaum instance, from element, id, index, or event. * * ```js * getTree(); // Get first Wunderbaum instance on page * getTree(1); // Get second Wunderbaum instance on page * getTree(event); // Get tree for this mouse- or keyboard event * getTree("foo"); // Get tree for this `tree.options.id` * getTree("#tree"); // Get tree for first matching element selector * ``` */ static getTree(el) { if (el instanceof Wunderbaum) { return el; } else if (el instanceof WunderbaumNode) { return el.tree; } if (el === undefined) { el = 0; // get first tree } if (typeof el === "number") { el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element } else if (typeof el === "string") { // Search all trees for matching ID for (const treeElem of document.querySelectorAll(".wunderbaum")) { const tree = treeElem._wb_tree; if (tree && tree.id === el) { return tree; } } // Search by selector el = document.querySelector(el); if (!el) { return null; } } else if (el.target) { el = el.target; } assert(el instanceof Element, `Invalid el type: ${el}`); if (!el.matches(".wunderbaum")) { el = el.closest(".wunderbaum"); } if (el && el._wb_tree) { return el._wb_tree; } return null; } /** * Return the icon-function -> icon-definition mapping. * @deprecated Use {@link Wunderbaum.iconMaps} */ get iconMap() { const map = this.options.iconMap; if (typeof map === "string") { return defaultIconMaps[map]; } return map; } /** * Return a WunderbaumNode instance from element or event. */ static getNode(el) { if (!el) { return null; } else if (el instanceof WunderbaumNode) { return el; } else if (el.target !== undefined) { el = el.target; // el was an Event } // `el` is a DOM element // let nodeElem = obj.closest("div.wb-row"); while (el) { if (el._wb_node) { return el._wb_node; } el = el.parentElement; //.parentNode; } return null; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link Wunderbaum.visit}. * * Example: * ```js * for(const node of tree) { * ... * } * ``` */ *[Symbol.iterator]() { yield* this.root; } /** @internal */ _registerExtension(extension) { this.extensionList.push(extension); this.extensions[extension.id] = extension; // this.extensionMap.set(extension.id, extension); } /** Called on tree (re)init after markup is created, before loading. */ _initExtensions() { for (const ext of this.extensionList) { ext.init(); } } /** * Calculate a *stable*, unique key for a node from its refKey (or title). * We also add information from the parent, because a refKey may occur multiple * times in a tree (but not as child of the same parent). * @internal */ _calculateKey(data, parent) { if (data.key) { // Always use an explicitly passed key return data.key; } // Auto-keys are optional, use a monotonic counter by default: if (!this.options.autoKeys) { return "" + ++WunderbaumNode.sequence; } // Add the parent's key to the hash. Assuming this was generated by the // same algorithm, this should incorporate the whole path: const s = (parent ? parent.key : "") + (data.refKey || data.title); // 32-bit has a high probability of collisions, so we pump up to 64-bit // https://security.stackexchange.com/q/209882/207588 const h1 = murmurHash3(s, true); let key = "id_" + h1 + murmurHash3(h1 + s, true); // Check for collisions // (Most likely if the same title occurs multiple in the same parent). const existingNode = this.keyMap.get(key); if (existingNode) { key += "." + ++Wunderbaum.sequence; this.logWarn(`Node with existing key: '${existingNode}', using ${key}.`, data); } return key; } /** Add node to tree's bookkeeping data structures. @internal */ _registerNode(node) { const key = node.key; assert(key != null, `Missing key: '${node}'.`); assert(!this.keyMap.has(key), `Duplicate key: '${key}': ${node}.`); this.keyMap.set(key, node); const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); // Set of nodes with this refKey if (rks) { rks.add(node); } else { this.refKeyMap.set(rk, new Set([node])); } } } /** Remove node from tree's bookkeeping data structures. @internal */ _unregisterNode(node) { // Remove refKey reference from map (if any) const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); if (rks && rks.delete(node) && !rks.size) { // We just removed the last element this.refKeyMap.delete(rk); } } // Remove key reference from map this.keyMap.delete(node.key); // Mark as disposed node.tree = null; node.parent = null; // Remove HTML markup node.removeMarkup(); } /** Call all hook methods of all registered extensions.*/ _callHook(hook, data = {}) { let res; const d = extend({}, { tree: this, options: this.options, result: undefined }, data); for (const ext of this.extensionList) { res = ext[hook].call(ext, d); if (res === false) { break; } if (d.result !== undefined) { res = d.result; } } return res; } /** * Call tree method or extension method if defined. * * Example: * ```js * tree._callMethod("edit.startEdit", "arg1", "arg2") * ``` */ _callMethod(name, ...args) { const [p, n] = name.split("."); const obj = n ? this.extensions[p] : this; const func = obj[n]; if (func) { return func.apply(obj, args); } else { this.logError(`Calling undefined method '${name}()'.`); } } /** * Call event handler if defined in tree or tree.EXTENSION options. * * Example: * ```js * tree._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type, extra) { const [p, n] = type.split("."); const opts = this.options; const func = n ? opts[p][n] : opts[p]; if (func) { return func.call(this, extend({ type: type, tree: this, util: this._util }, extra)); // } else { // this.logError(`Triggering undefined event '${type}'.`) } } /** Return the node for given row index. */ _getNodeByRowIdx(idx) { // TODO: start searching from active node (reverse) let node = null; this.visitRows((n) => { if (n._rowIdx === idx) { node = n; return false; } }); return node; } /** Return the topmost visible node in the viewport. * @param complete If `false`, the node is considered visible if at least one * pixel is visible. */ getTopmostVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const gracePx = 1; // ignore subpixel scrolling const scrollParent = this.element; // const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; // + headerHeight; let topIdx; if (complete) { topIdx = Math.ceil((scrollTop - gracePx) / rowHeight); } else { topIdx = Math.floor(scrollTop / rowHeight); } return this._getNodeByRowIdx(topIdx); } /** Return the lowest visible node in the viewport. */ getLowestVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const clientHeight = scrollParent.clientHeight - headerHeight; let bottomIdx; if (complete) { bottomIdx = Math.floor((scrollTop + clientHeight) / rowHeight) - 1; } else { bottomIdx = Math.ceil((scrollTop + clientHeight) / rowHeight) - 1; } bottomIdx = Math.min(bottomIdx, this.count(true) - 1); return this._getNodeByRowIdx(bottomIdx); } /** Return preceding visible node in the viewport. */ _getPrevNodeInView(node, ofs = 1) { this.visitRows((n) => { node = n; if (ofs-- <= 0) { return false; } }, { reverse: true, start: node || this.getActiveNode() }); return node; } /** Return following visible node in the viewport. */ _getNextNodeInView(node, options) { let ofs = (options === null || options === void 0 ? void 0 : options.ofs) || 1; const reverse = !!(options === null || options === void 0 ? void 0 : options.reverse); this.visitRows((n) => { node = n; if ((options === null || options === void 0 ? void 0 : options.cb) && options.cb(n)) { return false; } if (ofs-- <= 0) { return false; } }, { reverse: reverse, start: node || this.getActiveNode() }); return node; } /** * Append (or insert) a list of toplevel nodes. * * @see {@link WunderbaumNode.addChildren} */ addChildren(nodeData, options) { return this.root.addChildren(nodeData, options); } /** * Apply a modification or navigation operation. * * Most of these commands simply map to a node or tree method. * This method is especially useful when implementing keyboard mapping, * context menus, or external buttons. * * Valid commands: * - 'moveUp', 'moveDown' * - 'indent', 'outdent' * - 'remove' * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate * */ applyCommand(cmd, nodeOrOpts, options) { let // clipboard, node, refNode; // options = $.extend( // { setActive: true, clipboard: CLIPBOARD }, // options_ // ); if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { node = this.getActiveNode(); assert(options === undefined, `Unexpected options: ${options}`); options = nodeOrOpts; } // clipboard = options.clipboard; switch (cmd) { // Sorting and indentation: case "moveUp": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "before"); node.setActive(); } break; case "moveDown": refNode = node.getNextSibling(); if (refNode) { node.moveTo(refNode, "after"); node.setActive(); } break; case "indent": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "appendChild"); refNode.setExpanded(); node.setActive(); } break; case "outdent": if (!node.isTopLevel()) { node.moveTo(node.getParent(), "after"); node.setActive(); } break; // Remove: case "remove": refNode = node.getPrevSibling() || node.getParent(); node.remove(); if (refNode) { refNode.setActive(); } break; // Add, edit (requires ext-edit): case "addChild": this._callMethod("edit.createNode", "prependChild"); break; case "addSibling": this._callMethod("edit.createNode", "after"); break; case "rename": node.startEditTitle(); break; // Simple clipboard simulation: // case "cut": // clipboard = { mode: cmd, data: node }; // break; // case "copy": // clipboard = { // mode: cmd, // data: node.toDict(function(d, n) { // delete d.key; // }), // }; // break; // case "clear": // clipboard = null; // break; // case "paste": // if (clipboard.mode === "cut") { // // refNode = node.getPrevSibling(); // clipboard.data.moveTo(node, "child"); // clipboard.data.setActive(); // } else if (clipboard.mode === "copy") { // node.addChildren(clipboard.data).setActive(); // } // break; // Navigation commands: case "down": case "first": case "last": case "left": case "nextMatch": case "pageDown": case "pageUp": case "parent": case "prevMatch": case "right": case "up": return node.navigate(cmd); default: error(`Unhandled command: '${cmd}'`); } } /** Delete all nodes. */ clear() { this.root.removeChildren(); this.root.children = null; this.keyMap.clear(); this.refKeyMap.clear(); this.treeRowCount = 0; this._activeNode = null; this._focusNode = null; // this.types = {}; // this. columns =[]; // this._columnsById = {}; // Modification Status // this.changedSince = 0; // this.changes.clear(); // this.changedNodes.clear(); // // --- FILTER --- // public filterMode: FilterModeType = null; // // --- KEYNAV --- // public activeColIdx = 0; // public cellNavMode = false; // public lastQuicksearchTime = 0; // public lastQuicksearchTerm = ""; this.update(ChangeType.structure); } /** * Clear nodes and markup and detach events and observers. * * This method may be useful to free up resources before re-creating a tree * on an existing div, for example in unittest suites. * Note that this Wunderbaum instance becomes unusable afterwards. */ destroy() { this.logInfo("destroy()..."); this.clear(); this.resizeObserver.disconnect(); this.element.innerHTML = ""; // Remove all event handlers this.element.outerHTML = this.element.outerHTML; // eslint-disable-line } /** * Return `tree.option.NAME` (also resolving if this is a callback). * * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. * * @param name option name (use dot notation to access extension option, e.g. * `filter.mode`) */ getOption(name, defaultValue) { let ext; let opts = this.options; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [ext, name] = name.split("."); opts = opts[ext]; } let value = opts[name]; // A callback resolver always takes precedence if (typeof value === "function") { value = value({ type: "resolve", tree: this }); } // Use value from value options dict, fallback do default // console.info(name, value, opts) return value !== null && value !== void 0 ? value : defaultValue; } /** * Set tree option. * Use dot notation to set plugin option, e.g. "filter.mode". */ setOption(name, value) { // this.log(`setOption(${name}, ${value})`); if (name.indexOf(".") >= 0) { const parts = name.split("."); const ext = this.extensions[parts[0]]; ext.setPluginOption(parts[1], value); return; } this.options[name] = value; switch (name) { case "checkbox": this.update(ChangeType.any); break; case "enabled": this.setEnabled(!!value); break; case "fixedCol": this.element.classList.toggle("wb-fixed-col", !!value); break; } } /** Return true if the tree (or one of its nodes) has the input focus. */ hasFocus() { return this.element.contains(document.activeElement); } /** * Return true if the tree displays a header. Grids have a header unless the * `header` option is set to `false`. Plain trees have a header if the `header` * option is a string or `true`. */ hasHeader() { const header = this.options.header; return this.isGrid() ? header !== false : !!header; } /** Run code, but defer rendering of viewport until done. * * ```js * const res = tree.runWithDeferredUpdate(() => { * return someFunctionThatWouldUpdateManyNodes(); * }); * ``` */ runWithDeferredUpdate(func) { try { this.enableUpdate(false); const res = func(); assert(!(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}`); return res; } finally { this.enableUpdate(true); } } /** Run code, but defer rendering of viewport until done. * * ```js * const res = await tree.runWithDeferredUpdate(async () => { * return someAsyncFunctionThatWouldUpdateManyNodes(); * }); * ``` */ async runWithDeferredUpdateAsync(func) { try { this.enableUpdate(false); return await func(); } finally { this.enableUpdate(true); } } /** Recursively expand all expandable nodes (triggers lazy load if needed). */ async expandAll(flag = true, options) { await this.root.expandAll(flag, options); } /** Recursively select all nodes. */ selectAll(flag = true) { return this.root.setSelected(flag, { propagateDown: true }); } /** Toggle select all nodes. */ toggleSelect() { this.selectAll(this.root._anySelectable()); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents = false) { return this.root.getSelectedNodes(stopOnParents); } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false) { return this.root.getRefKeys(selected); } /* * Return an array of selected nodes. */ _selectRange(eventInfo) { this.logDebug("_selectRange", eventInfo); error("Not yet implemented."); // const mode = this.options.selectMode!; // if (mode !== "multi") { // this.logDebug(`Range selection only available for selectMode 'multi'`); // return; // } // if (eventInfo.canonicalName === "Meta+click") { // eventInfo.node?.toggleSelected(); // return false; // don't // } else if (eventInfo.canonicalName === "Shift+click") { // let from = this.activeNode; // let to = eventInfo.node; // if (!from || !to || from === to) { // return; // } // this.runWithDeferredUpdate(() => { // this.visitRows( // (node) => { // node.setSelected(); // }, // { // includeHidden: true, // includeSelf: false, // start: from, // reverse: from!._rowIdx! > to!._rowIdx!, // } // ); // }); // return false; // } } /** Return the number of nodes in the data model. * @param visible if true, nodes that are hidden due to collapsed parents are ignored. */ count(visible = false) { return visible ? this.treeRowCount : this.keyMap.size; } /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`. */ countUnique() { return this.refKeyMap.size; } /** @internal sanity check. */ _check() { let i = 0; this.visit((n) => { i++; }); if (this.keyMap.size !== i) { this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`); } // util.assert(this.keyMap.size === i); } /** * Find all nodes that match condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findAll} */ findAll(match) { return this.root.findAll(match); } /** * Find all nodes with a given _refKey_ (aka a list of clones). * * @param refKey a `node.refKey` value to search for. * @returns an array of matching nodes with at least two element or `[]` * if nothing found. * * @see {@link WunderbaumNode.getCloneList} */ findByRefKey(refKey) { const clones = this.refKeyMap.get(refKey); return clones ? Array.from(clones) : []; } /** * Find first node that matches condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findFirst} */ findFirst(match) { return this.root.findFirst(match); } /** * Find first node that matches condition. * * @see {@link WunderbaumNode.findFirst} * */ findKey(key) { return this.keyMap.get(key) || null; } /** * Find the next visible node that starts with `match`, starting at `startNode` * and wrap-around at the end. * Used by quicksearch and keyboard navigation. */ findNextNode(match, startNode, reverse = false) { //, visibleOnly) { let res = null; const firstNode = this.getFirstChild(); // Last visible node (calculation is expensive, so do only if we need it): const lastNode = reverse ? this.findRelatedNode(firstNode, "last") : null; const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match; startNode = startNode || (reverse ? lastNode : firstNode); function _checkNode(n) { // console.log("_check " + n) if (matcher(n)) { res = n; } if (res || n === startNode) { return false; } } this.visitRows(_checkNode, { start: startNode, includeSelf: false, reverse: reverse, }); // Wrap around search if (!res && startNode !== firstNode) { this.visitRows(_checkNode, { start: reverse ? lastNode : firstNode, includeSelf: true, reverse: reverse, }); } return res; } /** * Find a node relative to another node. * * @param node * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the keyCode that would normally trigger this move, * e.g. `$.ui.keyCode.LEFT` = 'left'. * @param includeHidden Not yet implemented */ findRelatedNode(node, where, includeHidden = false) { const rowHeight = this.options.rowHeightPx; let res = null; const pageSize = Math.floor(this.listContainerElement.clientHeight / rowHeight); switch (where) { case "parent": if (node.parent && node.parent.parent) { res = node.parent; } break; case "first": // First visible node this.visit((n) => { if (n.isVisible()) { res = n; return false; } }); break; case "last": this.visit((n) => { // last visible node if (n.isVisible()) { res = n; } }); break; case "left": if (node.parent && node.parent.parent) { res = node.parent; } // if (node.expanded) { // node.setExpanded(false); // } else if (node.parent && node.parent.parent) { // res = node.parent; // } break; case "right": if (node.children && node.children.length) { res = node.children[0]; } // if (this.cellNavMode) { // throw new Error("Not implemented"); // } else { // if (!node.expanded && (node.children || node.lazy)) { // node.setExpanded(); // res = node; // } else if (node.children && node.children.length) { // res = node.children[0]; // } // } break; case "up": res = this._getNextNodeInView(node, { reverse: true }); break; case "down": res = this._getNextNodeInView(node); break; case "pageDown": { const bottomNode = this.getLowestVpNode(); // this.logDebug(`${where}(${node}) -> ${bottomNode}`); if (node._rowIdx < bottomNode._rowIdx) { res = bottomNode; } else { res = this._getNextNodeInView(node, { reverse: false, ofs: pageSize, }); } } break; case "pageUp": if (node._rowIdx === 0) { res = node; } else { const topNode = this.getTopmostVpNode(); // this.logDebug(`${where}(${node}) -> ${topNode}`); if (node._rowIdx > topNode._rowIdx) { res = topNode; } else { res = this._getNextNodeInView(node, { reverse: true, ofs: pageSize, }); } } break; case "prevMatch": // fallthrough case "nextMatch": if (!this.isFilterActive) { this.logWarn(`${where}: Filter is not active.`); break; } res = this.findNextNode((n) => n.isMatched(), node, where === "prevMatch"); res === null || res === void 0 ? void 0 : res.setActive(); break; default: this.logWarn("Unknown relation '" + where + "'."); } return res; } /** * Iterator version of {@link Wunderbaum.format}. */ *format_iter(name_cb, connectors) { yield* this.root.format_iter(name_cb, connectors); } /** * Return multiline string representation of the node hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.format((n)=>n.title)); * ``` * logs * ``` * Playground * ├─ Books * | ├─ Art of War * | ╰─ Don Quixote * ├─ Music * ... * ``` * * @see {@link Wunderbaum.format_iter} and {@link WunderbaumNode.format}. */ format(name_cb, connectors) { return this.root.format(name_cb, connectors); } /** * Always returns null (so a tree instance behaves as `tree.root`). */ get parent() { return null; } /** * Return a list of top-level nodes. */ get children() { return this.root.children || []; } /** * Return the active cell (`span.wb-col`) of the currently active node or null. */ getActiveColElem() { if (this.activeNode && this.activeColIdx >= 0) { return this.activeNode.getColElem(this.activeColIdx); } return null; } /** * Return the currently active node or null (alias for `tree.activeNode`). * Alias for {@link Wunderbaum.activeNode}. * * @see {@link WunderbaumNode.setActive} * @see {@link WunderbaumNode.isActive} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getActiveNode() { return this.activeNode; } /** * Return the first top level node if any (not the invisible root node). */ getFirstChild() { return this.root.getFirstChild(); } /** * Return the last top level node if any (not the invisible root node). */ getLastChild() { return this.root.getLastChild(); } /** * Return the node that currently has keyboard focus or null. * Alias for {@link Wunderbaum.focusNode}. * @see {@link WunderbaumNode.setFocus} * @see {@link WunderbaumNode.hasFocus} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getFocusNode() { return this.focusNode; } /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ static getEventInfo(event) { const target = event.target; const cl = target.classList; const parentCol = target.closest("span.wb-col"); const node = Wunderbaum.getNode(target); const tree = node ? node.tree : Wunderbaum.getTree(event); const res = { event: event, canonicalName: eventToString(event), tree: tree, node: node, region: NodeRegion.unknown, colDef: undefined, colIdx: -1, colId: undefined, colElem: parentCol, }; if (cl.contains("wb-title")) { res.region = NodeRegion.title; } else if (cl.contains("wb-expander")) { res.region = node.isExpandable() ? NodeRegion.expander : NodeRegion.prefix; } else if (cl.contains("wb-checkbox")) { res.region = NodeRegion.checkbox; } else if (cl.contains("wb-icon")) { //|| cl.contains("wb-custom-icon")) { res.region = NodeRegion.icon; } else if (cl.contains("wb-node")) { res.region = NodeRegion.title; } else if (parentCol) { res.region = NodeRegion.column; const idx = Array.prototype.indexOf.call(parentCol.parentNode.children, parentCol); res.colIdx = idx; } else if (cl.contains("wb-row")) { // Plain tree res.region = NodeRegion.title; } else { // Somewhere near the title if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) { tree === null || tree === void 0 ? void 0 : tree.logWarn("getEventInfo(): not found", event, res); } return res; } if (res.colIdx === -1) { res.colIdx = 0; } res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx]; res.colDef != null ? (res.colId = res.colDef.id) : 0; // this.log("Event", event, res); return res; } /** * Return readable string representation for this instance. * @internal */ toString() { return `Wunderbaum<'${this.id}'>`; } /** Return true if any node title or grid cell is currently beeing edited. * * See also {@link isEditingTitle}. */ isEditing() { const focusElem = this.nodeListElement.querySelector("input:focus,select:focus"); return !!focusElem; } /** Return true if any node is currently in edit-title mode. * * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}. */ isEditingTitle() { return this._callMethod("edit.isEditingTitle"); } /** * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. */ isLoading() { let res = false; this.root.visit((n) => { // also visit rootNode if (n._isLoading || n._requestId) { res = true; return false; } }, true); return res; } /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4. * @see {@link logDebug} */ log(...args) { if (this.options.debugLevel >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4. * and browser console level includes debug/verbose messages. * @see {@link log} */ logDebug(...args) { if (this.options.debugLevel >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with tree name as prefix. */ logError(...args) { if (this.options.debugLevel >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */ logInfo(...args) { if (this.options.debugLevel >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** @internal */ logTime(label) { if (this.options.debugLevel >= 4) { console.time(this + ": " + label); // eslint-disable-line no-console } return label; } /** @internal */ logTimeEnd(label) { if (this.options.debugLevel >= 4) { console.timeEnd(this + ": " + label); // eslint-disable-line no-console } } /** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */ logWarn(...args) { if (this.options.debugLevel >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Emit a warning for deprecated methods. @internal */ logDeprecate(method, options) { if (this.options.debugLevel >= 2) { let msg = `${this}: ${method} is deprecated`; if (options === null || options === void 0 ? void 0 : options.since) { msg += ` since ${options.since}`; } if (options === null || options === void 0 ? void 0 : options.hint) { msg += ` (${options.since})`; } console.warn(msg + "."); // eslint-disable-line no-console } } /** Reset column widths to default. @since 0.10.0 */ resetColumns() { this.columns.forEach((col) => { delete col.customWidthPx; }); this.update(ChangeType.colStructure); } // /** Renumber nodes `_nativeIndex`. @see {@link WunderbaumNode.resetNativeChildOrder} */ // resetNativeChildOrder(options?: ResetOrderOptions) { // this.root.resetNativeChildOrder(options); // } /** * Make sure that this node is vertically scrolled into the viewport. * * Nodes that are above the visible area become the top row, nodes that are * below the viewport become the bottom row. */ scrollTo(nodeOrOpts) { const PADDING = 2; // leave some pixels between viewport bounds let node; // WunderbaumNode; let options; if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { options = nodeOrOpts; node = options.node; } assert(node && node._rowIdx != null, `Invalid node: ${node}`); const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const vpHeight = scrollParent.clientHeight; const rowTop = node._rowIdx * rowHeight + headerHeight; const vpTop = headerHeight; const vpRowTop = rowTop - scrollTop; const vpRowBottom = vpRowTop + rowHeight; const topNode = options === null || options === void 0 ? void 0 : options.topNode; // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts , options); let newScrollTop = null; if (vpRowTop >= vpTop) { if (vpRowBottom <= vpHeight) ; else { // Node is below viewport // this.log("Below viewport"); newScrollTop = rowTop + rowHeight - vpHeight + PADDING; // leave some pixels between viewport bounds } } else { // Node is above viewport // this.log("Above viewport"); newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between viewport bounds } if (newScrollTop != null) { this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`); scrollParent.scrollTop = newScrollTop; if (topNode) { // Make sure the topNode is always visible this.scrollTo(topNode); } // this.update(ChangeType.scroll); } } /** * Make sure that this node is horizontally scrolled into the viewport. * Called by {@link setColumn}. */ scrollToHorz() { // const PADDING = 1; const fixedWidth = this.columns[0]._widthPx; const vpWidth = this.element.clientWidth; const scrollLeft = this.element.scrollLeft; const colElem = this.getActiveColElem(); const colLeft = Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.left, 10); const colRight = colLeft + Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.width, 10); let newLeft = scrollLeft; if (colLeft - scrollLeft < fixedWidth) { // The current column is scrolled behind the left fixed column newLeft = colLeft - fixedWidth; } else if (colRight - scrollLeft > vpWidth) { // The current column is scrolled outside the right side newLeft = colRight - vpWidth; } newLeft = Math.max(0, newLeft); // util.assert(node._rowIdx != null); this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`); this.element.scrollLeft = newLeft; // this.update(ChangeType.scroll); } /** * Set column #colIdx to 'active'. * * This higlights the column header and -cells by adding the `wb-active` * class to all grid cells of the active column.
* Available in cell-nav mode only. * * If _options.edit_ is true, the embedded input element is focused, or if * colIdx is 0, the node title is put into edit mode. */ setColumn(colIdx, options) { var _a, _b, _c; const edit = options === null || options === void 0 ? void 0 : options.edit; const scroll = (options === null || options === void 0 ? void 0 : options.scrollIntoView) !== false; assert(this.isCellNav(), "Expected cellNav mode"); if (typeof colIdx === "string") { const cid = colIdx; colIdx = this.columns.findIndex((c) => c.id === colIdx); assert(colIdx >= 0, `Invalid colId: ${cid}`); } assert(0 <= colIdx && colIdx < this.columns.length, `Invalid colIdx: ${colIdx}`); this.activeColIdx = colIdx; // Update `wb-active` class for all headers if (this.hasHeader()) { for (const rowDiv of this.headerElement.children) { let i = 0; for (const colDiv of rowDiv.children) { colDiv.classList.toggle("wb-active", i++ === colIdx); } } } (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status); // Update `wb-active` class for all cell spans for (const rowDiv of this.nodeListElement.children) { let i = 0; for (const colDiv of rowDiv.children) { colDiv.classList.toggle("wb-active", i++ === colIdx); } } // Horizontically scroll into view if (scroll || edit) { this.scrollToHorz(); } if (edit && this.activeNode) { // this.activeNode.setFocus(); // Blur prev. input if any if (colIdx === 0) { this.activeNode.startEditTitle(); } else { (_c = (_b = this.getActiveColElem()) === null || _b === void 0 ? void 0 : _b.querySelector("input,select")) === null || _c === void 0 ? void 0 : _c.focus(); } } } /* Set or remove keyboard focus to the tree container. @internal */ _setActiveNode(node) { this._activeNode = node; } /** Set or remove keyboard focus to the tree container. */ setActiveNode(key, flag = true, options) { var _a; (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setActive(flag, options); } /** Set or remove keyboard focus to the tree container. */ setFocus(flag = true) { if (flag) { this.element.focus(); } else { this.element.blur(); } } /* Set or remove keyboard focus to the tree container. @internal */ _setFocusNode(node) { this._focusNode = node; } /** Return the current selection/expansion/activation status. @experimental */ getState(options = {}) { var _a, _b; const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options; const expandSet = new Set(); if (expandedKeys) { for (const node of this) { if (node.isExpanded() && node.hasChildren()) { expandSet.add(node.key); } } } // Parents of active node are always expanded if (activeKey && this.activeNode) { this.activeNode.visitParents((n) => { if (n.parent) { expandSet.add(n.key); } }, false); } const state = { expandedKeys: expandSet.size ? Array.from(expandSet) : undefined, activeKey: (_b = (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null, activeColIdx: this.activeColIdx, selectedKeys: selectedKeys ? this.getSelectedNodes().flatMap((n) => n.key) : undefined, }; return state; } /** Apply selection/expansion/activation status. @experimental */ async setState(state, options = {}) { const { expandLazy = true } = options; return this.runWithDeferredUpdateAsync(async () => { var _a, _b; if (state.expandedKeys && state.expandedKeys.length) { if (expandLazy) { // Expand all keys recursively, even if they are not in the tree yet await this._loadLazyNodes(state.expandedKeys, { expand: true, noEvents: true, }); } else { for (const key of state.expandedKeys) { (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setExpanded(true); } } } if (state.activeKey) { this.setActiveNode(state.activeKey); } if (state.selectedKeys) { this.selectAll(false); for (const key of state.selectedKeys) { (_b = this.findKey(key)) === null || _b === void 0 ? void 0 : _b.setSelected(true); } } if (this.isCellNav() && state.activeColIdx != null) { this.setColumn(state.activeColIdx); } }); } update(change, node, options) { // this.log(`update(${change}) node=${node}`); if (!(node instanceof WunderbaumNode)) { options = node; node = undefined; } const immediate = !!getOption(options, "immediate"); const RF = RenderFlag; const pending = this.pendingChangeTypes; if (this._disableUpdateCount) { // Assuming that we redraw all when enableUpdate() is re-enabled. // this.log( // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})` // ); this._disableUpdateIgnoreCount++; return; } switch (change) { case ChangeType.any: case ChangeType.colStructure: pending.add(RF.header); pending.add(RF.clearMarkup); pending.add(RF.redraw); pending.add(RF.scroll); break; case ChangeType.resize: // case ChangeType.colWidth: pending.add(RF.header); pending.add(RF.redraw); break; case ChangeType.structure: pending.add(RF.redraw); break; case ChangeType.scroll: pending.add(RF.scroll); break; case ChangeType.row: case ChangeType.data: case ChangeType.status: assert(node, `Option '${change}' requires a node.`); // Single nodes are immediately updated if already inside the viewport // (otherwise we can ignore) if (node._rowElem) { node._render({ change: change }); } break; default: error(`Invalid change type '${change}'.`); } if (change === ChangeType.colStructure) { const isGrid = this.isGrid(); this.element.classList.toggle("wb-grid", isGrid); if (!isGrid && this.isCellNav()) { this.setCellNav(false); } } if (pending.size > 0) { if (immediate) { this._updateViewportImmediately(); } else { this._updateViewportThrottled(); } } } /** Disable mouse and keyboard interaction (return prev. state). */ setEnabled(flag = true) { const prev = this.enabled; this.enabled = !!flag; this.element.classList.toggle("wb-disabled", !flag); return prev; } /** Return false if tree is disabled. */ isEnabled() { return this.enabled; } /** Return true if tree has more than one column, i.e. has additional data columns. */ isGrid() { return this.columns && this.columns.length > 1; } /** Return true if cell-navigation mode is active. */ isCellNav() { return !!this._cellNavMode; } /** Return true if row-navigation mode is active. */ isRowNav() { return !this._cellNavMode; } /** Set the tree's navigation mode. */ setCellNav(flag = true) { var _a; const prev = this._cellNavMode; // if (flag === prev) { // return; // } this._cellNavMode = !!flag; if (flag && !prev) { // switch from row to cell mode this.setColumn(0); } this.element.classList.toggle("wb-cell-mode", flag); (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.update(ChangeType.status); } /** Set the tree's navigation mode option. */ setNavigationOption(mode, reset = false) { if (!this.isGrid() && mode !== NavModeEnum.row) { this.logWarn("Plain trees only support row navigation mode."); return; } this.options.navigationModeOption = mode; switch (mode) { case NavModeEnum.cell: this.setCellNav(true); break; case NavModeEnum.row: this.setCellNav(false); break; case NavModeEnum.startCell: if (reset) { this.setCellNav(true); } break; case NavModeEnum.startRow: if (reset) { this.setCellNav(false); } break; default: error(`Invalid mode '${mode}'.`); } } /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */ setStatus(status, options) { return this.root.setStatus(status, options); } /** Add or redefine node type definitions. */ setTypes(types, replace = true) { assert(isPlainObject(types), `Expected plain objext: ${types}`); if (replace) { this.types = types; } else { extend(this.types, types); } // Convert `TYPE.classes` to a Set for (const t of Object.values(this.types)) { if (t.classes) { t.classes = toSet(t.classes); } } } /** * Sort nodes list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren(cmp = nodeTitleSorter, deep = false) { this.logDeprecate("sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep, propName: "title", }); } /** * Convenience method to implement column sorting. * @see {@link WunderbaumNode.sortByProperty}. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options) { this.logDeprecate("sortByProperty()", { since: "0.14.0" }); this.root.sortByProperty(options); } /** * Sort nodes list by title or custom criteria. * @since 0.14.0 */ sort(options) { this.root.sort(options); } /** Convert tree to an array of plain objects. * * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link WunderbaumNode.toDict}. */ toDictArray(callback) { var _a; const res = this.root.toDict(true, callback); return (_a = res.children) !== null && _a !== void 0 ? _a : []; } /** * Update column headers and column width. * Return true if at least one column width changed. */ // _updateColumnWidths(options?: UpdateColumnsOptions): boolean { _updateColumnWidths() { // options = Object.assign({ updateRows: true, renderMarkup: false }, options); const defaultMinWidth = 4; const vpWidth = this.element.clientWidth; // Shorten last column width to avoid h-scrollbar // (otherwise resizbing the demo would display a void scrollbar?) const FIX_ADJUST_LAST_COL = 1; const columns = this.columns; const col0 = columns[0]; let totalWidth = 0; let totalWeight = 0; let fixedWidth = 0; let modified = false; // this.element.classList.toggle("wb-grid", isGrid); // if (!isGrid && this.isCellNav()) { // this.setCellNav(false); // } // if (options.calculateCols) { if (col0.id !== "*") { throw new Error(`First column must have id '*': got '${col0.id}'.`); } // Gather width definitions this._columnsById = {}; for (const col of columns) { this._columnsById[col.id] = col; const cw = col.customWidthPx ? `${col.customWidthPx}px` : col.width; if (col.id === "*" && col !== col0) { throw new Error(`Column id '*' must be defined only once: '${col.title}'.`); } if (!cw || cw === "*") { col._weight = 1.0; totalWeight += 1.0; } else if (typeof cw === "number") { col._weight = cw; totalWeight += cw; } else if (typeof cw === "string" && cw.endsWith("px")) { col._weight = 0; const px = parseFloat(cw.slice(0, -2)); if (col._widthPx != px) { modified = true; col._widthPx = px; } fixedWidth += px; } else { error(`Invalid column width: ${cw} (expected string ending with 'px' or number, e.g. "px" or ).`); } } // Share remaining space between non-fixed columns const restPx = Math.max(0, vpWidth - fixedWidth); let ofsPx = 0; for (const col of columns) { let minWidth; if (col._weight) { const cmw = col.minWidth; if (typeof cmw === "number") { minWidth = cmw; } else if (typeof cmw === "string" && cmw.endsWith("px")) { minWidth = parseFloat(cmw.slice(0, -2)); } else { minWidth = defaultMinWidth; } const px = Math.max(minWidth, (restPx * col._weight) / totalWeight); if (col._widthPx != px) { modified = true; col._widthPx = px; } } col._ofsPx = ofsPx; ofsPx += col._widthPx; } columns[columns.length - 1]._widthPx -= FIX_ADJUST_LAST_COL; totalWidth = ofsPx - FIX_ADJUST_LAST_COL; const tw = `${totalWidth}px`; this.headerElement.style.width = tw; this.listContainerElement.style.width = tw; // } // Every column has now a calculated `_ofsPx` and `_widthPx` // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element); // console.trace(); // util.error("BREAK"); // if (modified) { // this._renderHeaderMarkup(); // if (options.renderMarkup) { // this.update(ChangeType.header, { removeMarkup: true }); // } else if (options.updateRows) { // this._updateRows(); // } // } return modified; } // protected _insertIcon(icon: string, elem: HTMLElement) { // const iconElem = document.createElement("i"); // iconElem.className = icon; // elem.appendChild(iconElem); // } /** Create/update header markup from `this.columns` definition. * @internal */ _renderHeaderMarkup() { assert(this.headerElement, "Expected a headerElement"); const wantHeader = this.hasHeader(); setElemDisplay(this.headerElement, wantHeader); if (!wantHeader) { return; } const iconMap = this.iconMap; const colCount = this.columns.length; const headerRow = this.headerElement.querySelector(".wb-row"); assert(headerRow, "Expected a row in header element"); headerRow.innerHTML = "".repeat(colCount); for (let i = 0; i < colCount; i++) { const col = this.columns[i]; const colElem = headerRow.children[i]; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; // Add classes from `columns` definition to `` cells if (typeof col.headerClasses === "string") { col.headerClasses ? colElem.classList.add(...col.headerClasses.split(" ")) : 0; } else { col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; } // Add tooltip to column title let tooltip = ""; if (col.tooltip) { tooltip = escapeTooltip(col.tooltip); tooltip = ` title="${tooltip}"`; } // Add column header icons let addMarkup = ""; // NOTE: we use CSS float: right to align icons, so they must be added in // reverse order if (toBool(col.menu, this.options.columnsMenu, false)) { const iconClass = "wb-col-icon-menu " + iconMap.colMenu; const icon = ``; addMarkup += icon; } if (toBool(col.sortable, this.options.columnsSortable, false)) { let iconClass = "wb-col-icon-sort " + iconMap.colSortable; if (col.sortOrder) { iconClass += `wb-col-sort-${col.sortOrder}`; iconClass += col.sortOrder === "asc" ? iconMap.colSortAsc : iconMap.colSortDesc; } const icon = ``; addMarkup += icon; } if (toBool(col.filterable, this.options.columnsFilterable, false)) { colElem.classList.toggle("wb-col-filter", !!col.filterActive); let iconClass = "wb-col-icon-filter " + iconMap.colFilter; if (col.filterActive) { iconClass += iconMap.colFilterActive; } const icon = ``; addMarkup += icon; } // Add resizer to all but the last column if (i < colCount - 1) { if (toBool(col.resizable, this.options.columnsResizable, false)) { addMarkup += ''; } else { addMarkup += ''; } } // Create column header const title = escapeHtml(col.title || col.id); colElem.innerHTML = `${title}${addMarkup}`; // Highlight active column if (this.isCellNav()) { colElem.classList.toggle("wb-active", i === this.activeColIdx); } } } /** * Render pending changes that were scheduled using {@link WunderbaumNode.update} if any. * * This is hardly ever neccessary, since we normally either * - call `update(ChangeType.TYPE)` (async, throttled), or * - call `update(ChangeType.TYPE, {immediate: true})` (synchronous) * * `updatePendingModifications()` will only force immediate execution of * pending async changes if any. */ updatePendingModifications() { if (this.pendingChangeTypes.size > 0) { this._updateViewportImmediately(); } } /** @internal */ _createNodeIcon(node, showLoading, showBadge) { const iconMap = this.iconMap; let iconElem; let icon = node.getOption("icon"); if (node._errorInfo) { icon = iconMap.error; } else if (node._isLoading && showLoading) { // Status nodes, or nodes without expander (< minExpandLevel) should // display the 'loading' status with the i.wb-icon span icon = iconMap.loading; } if (icon === false) { return null; // explicitly disabled: don't try default icons } if (typeof icon === "string") ; else if (node.statusNodeType) { icon = iconMap[node.statusNodeType]; } else if (node.expanded) { icon = iconMap.folderOpen; } else if (node.children) { icon = iconMap.folder; } else if (node.lazy) { icon = iconMap.folderLazy; } else { icon = iconMap.doc; } if (!icon) { iconElem = document.createElement("i"); iconElem.className = "wb-icon"; } else if (TEST_HTML.test(icon)) { iconElem = elemFromHtml(icon); } else if (TEST_FILE_PATH.test(icon)) { iconElem = elemFromHtml(``); } else { // Class name iconElem = document.createElement("i"); iconElem.className = "wb-icon " + icon; } // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem }); let badge = null; if (cbRes != null && cbRes !== false) { let classes = ""; let tooltip = ""; if (isPlainObject(cbRes)) { badge = "" + cbRes.badge; classes = cbRes.badgeClass ? " " + cbRes.badgeClass : ""; tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : ""; } else if (typeof cbRes === "number") { badge = "" + cbRes; } else { badge = cbRes; // string or HTMLSpanElement } if (typeof badge === "string") { badge = elemFromHtml(`${escapeHtml(badge)}`); } if (badge) { iconElem.append(badge); } } return iconElem; } _updateTopBreadcrumb() { const breadcrumb = this.breadcrumb; const topmost = this.getTopmostVpNode(true); const parentList = topmost === null || topmost === void 0 ? void 0 : topmost.getParentList(false, false); if (parentList === null || parentList === void 0 ? void 0 : parentList.length) { breadcrumb.innerHTML = ""; for (const n of topmost.getParentList(false, false)) { const icon = this._createNodeIcon(n, false, false); if (icon) { breadcrumb.append(icon, " "); } const part = document.createElement("a"); part.textContent = n.title; part.href = "#"; part.classList.add("wb-breadcrumb"); part.dataset.key = n.key; breadcrumb.append(part, this.options.strings.breadcrumbDelimiter); } } else { breadcrumb.innerHTML = " "; } } /** * This is the actual update method, which is wrapped inside a throttle method. * It calls `updateColumns()` and `_updateRows()`. * * This protected method should not be called directly but via * {@link WunderbaumNode.update}`, {@link Wunderbaum.update}, * or {@link Wunderbaum.updatePendingModifications}. * @internal */ _updateViewportImmediately() { if (this._disableUpdateCount) { this.log(`_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).`); this._disableUpdateIgnoreCount++; return; } if (this._updateViewportThrottled.pending()) { // this.logWarn(`_updateViewportImmediately() cancel pending timer.`); this._updateViewportThrottled.cancel(); } // Shorten container height to avoid v-scrollbar const FIX_ADJUST_HEIGHT = 1; const RF = RenderFlag; const pending = new Set(this.pendingChangeTypes); this.pendingChangeTypes.clear(); const scrollOnly = pending.has(RF.scroll) && pending.size === 1; if (scrollOnly) { this._updateRows({ newNodesOnly: true }); // this.log("_updateViewportImmediately(): scroll only."); } else { this.log("_updateViewportImmediately():", pending); if (this.options.adjustHeight !== false) { let height = this.listContainerElement.clientHeight; const headerHeight = this.headerElement.clientHeight; // May be 0 const wantHeight = this.element.clientHeight - headerHeight - FIX_ADJUST_HEIGHT; if (Math.abs(height - wantHeight) > 1.0) { // this.log("resize", height, wantHeight); this.listContainerElement.style.height = wantHeight + "px"; height = wantHeight; } } // console.profile(`_updateViewportImmediately()`) if (pending.has(RF.clearMarkup)) { this.visit((n) => { n.removeMarkup(); }); } // let widthModified = false; if (pending.has(RF.header)) { // widthModified = this._updateColumnWidths(); this._updateColumnWidths(); this._renderHeaderMarkup(); } this._updateRows(); // console.profileEnd(`_updateViewportImmediately()`) } if (this.breadcrumb) { this._updateTopBreadcrumb(); } this._callEvent("update"); } // /** // * Assert that TR order matches the natural node order // * @internal // */ // protected _validateRows(): boolean { // let trs = this.nodeListElement.childNodes; // let i = 0; // let prev = -1; // let ok = true; // trs.forEach((element) => { // const tr = element as HTMLTableRowElement; // const top = Number.parseInt(tr.style.top); // const n = (tr)._wb_node; // // if (i < 4) { // // console.info( // // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'` // // ); // // } // if (prev >= 0 && top !== prev + ROW_HEIGHT) { // n.logWarn( // `TR order mismatch at index ${i}: top=${top}px != ${ // prev + ROW_HEIGHT // }` // ); // // throw new Error("fault"); // ok = false; // } // prev = top; // i++; // }); // return ok; // } /* * - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes. * - Store count of rows to `tree.treeRowCount`. * - Renumber `node._rowIdx` for all visible nodes. * - Calculate the index range that must be rendered to fill the viewport * (including upper and lower prefetch) * - */ _updateRows(options) { // const label = this.logTime("_updateRows"); // this.log("_updateRows", opts) options = Object.assign({ newNodesOnly: false }, options); const newNodesOnly = !!options.newNodesOnly; const rowHeight = this.options.rowHeightPx; const vpHeight = this.element.clientHeight; const prefetch = RENDER_MAX_PREFETCH; // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH; const ofs = this.element.scrollTop; let startIdx = Math.max(0, ofs / rowHeight - prefetch); startIdx = Math.floor(startIdx); // Make sure start is always even, so the alternating row colors don't // change when scrolling: if (startIdx % 2) { startIdx--; } let endIdx = Math.max(0, (ofs + vpHeight) / rowHeight + prefetch); endIdx = Math.ceil(endIdx); // this.debug("render", opts); const obsoleteNodes = new Set(); this.nodeListElement.childNodes.forEach((elem) => { if (elem._wb_node) { obsoleteNodes.add(elem._wb_node); } }); let idx = 0; let top = 0; let modified = false; let prevElem = "first"; this.visitRows(function (node) { // node.log("visit") const rowDiv = node._rowElem; // Renumber all expanded nodes if (node._rowIdx !== idx) { node._rowIdx = idx; modified = true; } if (idx < startIdx || idx > endIdx) { // row is outside viewport bounds if (rowDiv) { prevElem = rowDiv; } } else if (rowDiv && newNodesOnly) { obsoleteNodes.delete(node); // no need to update existing node markup rowDiv.style.top = idx * rowHeight + "px"; prevElem = rowDiv; } else { obsoleteNodes.delete(node); // Create new markup if (rowDiv) { rowDiv.style.top = idx * rowHeight + "px"; } node._render({ top: top, after: prevElem }); // node.log("render", top, prevElem, "=>", node._rowElem); prevElem = node._rowElem; } idx++; top += rowHeight; }); this.treeRowCount = idx; for (const n of obsoleteNodes) { n._callEvent("discard"); n.removeMarkup(); } // Resize tree container this.nodeListElement.style.height = `${top}px`; // this.log( // `_updateRows(scrollOfs:${ofs}, ${startIdx}..${endIdx})`, // this.nodeListElement.style.height // ); // this.logTimeEnd(label); // this._validateRows(); return modified; } /** * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order). * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link WunderbaumNode.visit}. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * children only. * @returns {boolean} false, if the iterator was stopped. */ visit(callback) { return this.root.visit(callback, false); } /** * Call callback(node) for all nodes in vertical order, top down (or bottom up). * * Note that this considers expansion state, i.e. filtered nodes and children * of collapsed nodes are skipped, unless `includeHidden` is set. * * Stop iteration if callback() returns false.
* Return false if iteration was stopped. * * @returns {boolean} false if iteration was canceled */ visitRows(callback, options) { if (!this.root.hasChildren()) { return false; } if (options && options.reverse) { delete options.reverse; return this._visitRowsUp(callback, options); } options = options || {}; let i, nextIdx, parent, res, siblings, stopNode, siblingOfs = 0, skipFirstNode = options.includeSelf === false, node = options.start || this.root.children[0]; const includeHidden = !!options.includeHidden; const checkFilter = !includeHidden && this.filterMode === "hide"; parent = node.parent; while (parent) { // visit siblings siblings = parent.children; nextIdx = siblings.indexOf(node) + siblingOfs; assert(nextIdx >= 0, `Could not find ${node} in parent's children: ${parent}`); for (i = nextIdx; i < siblings.length; i++) { node = siblings[i]; if (node === stopNode) { return false; } if (checkFilter && !node.statusNodeType && !node.match && !node.subMatchCount) { continue; } if (!skipFirstNode && callback(node) === false) { return false; } skipFirstNode = false; // Dive into node's child nodes if (node.children && node.children.length && (includeHidden || node.expanded)) { res = node.visit((n) => { if (n === stopNode) { return false; } if (checkFilter && !n.match && !n.subMatchCount) { return "skip"; } if (callback(n) === false) { return false; } if (!includeHidden && n.children && !n.expanded) { return "skip"; } }, false); if (res === false) { return false; } } } // Visit parent nodes (bottom up) node = parent; parent = parent.parent; siblingOfs = 1; // if (!parent && options.wrap) { this.logDebug("visitRows(): wrap around"); assert(options.start, "`wrap` option requires `start`"); stopNode = options.start; options.wrap = false; parent = this.root; siblingOfs = 0; } } return true; } /** * Call fn(node) for all nodes in vertical order, bottom up. * @internal */ _visitRowsUp(callback, options) { let children, idx, parent, node = options.start || this.root.children[0]; const includeHidden = !!options.includeHidden; if (options.includeSelf !== false) { if (callback(node) === false) { return false; } } while (true) { parent = node.parent; children = parent.children; if (children[0] === node) { // If this is already the first sibling, goto parent node = parent; if (!node.parent) { break; // first node of the tree } children = parent.children; } else { // Otherwise, goto prev. sibling idx = children.indexOf(node); node = children[idx - 1]; // If the prev. sibling has children, follow down to last descendant while ((includeHidden || node.expanded) && node.children && node.children.length) { children = node.children; parent = node; node = children[children.length - 1]; } } // Skip invisible if (!includeHidden && !node.isVisible()) { continue; } if (callback(node) === false) { return false; } } return true; } /** * Reload the tree with a new source. * * Previous data is cleared. Note that also column- and type defintions may * be passed with the `source` object. * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request * and restore the previous state. */ async load(source) { this.clear(); this._initialSource = source; return this.root.load(source); } /** Reload the tree and optionally restore state. * Source defaults to last ajax url if any. * Restoring the active node requires stable keys * @see {@link WunderbaumOptions.autoKeys} * @see {@link Wunderbaum.load} * @experimental */ async reload(options = {}) { const { source = this._initialSource, reactivate = true } = options; if (!source) { this.logWarn("No previous ajax source to reload."); return; } if (!reactivate) { return this.load(source); } const state = this.getState(); await this.load(source); return this.setState(state); } /** * Make sure that all nodes in the given keyList are accessible. * This may include loading lazy parent nodes. * Recursively load (and optionally expand) all requested node paths. */ async _loadLazyNodes(keyList, options = {}) { const { expand = true } = options; const keySet = new Set(keyList); // Make sure that all parent nodes are loaded (and expand if requested) while (keySet.size > 0) { const pendingNodes = []; const curSet = new Set(keySet); for (const key of curSet) { const node = this.findKey(key); if (!node) { continue; // key not yet found (need to load lazy parent?) } keySet.delete(key); if (expand) { pendingNodes.push(node.setExpanded(true)); } else if (node.isUnloaded()) { pendingNodes.push(node.loadLazy()); } if (node._rowElem) { node._render(); // show spinner even is update is suppressed } } if (pendingNodes.length === 0) { // will not load any more nodes, so if if there are still keys // left in the set, we will never find them this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet); break; } await Promise.allSettled(pendingNodes); } } /** * Disable render requests during operations that would trigger many updates. * * ```js * try { * tree.enableUpdate(false); * // ... (long running operation that would trigger many updates) * foo(); * // ... NOTE: make sure that async operations have finished, e.g. * await foo(); * } finally { * tree.enableUpdate(true); * } * ``` */ enableUpdate(flag) { /* 5 7 9 20 25 30 1 >-------------------------------------< 2 >--------------------< 3 >--------------------------< */ if (flag) { assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often"); this._disableUpdateCount--; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); if (this._disableUpdateCount === 0) { this.logDebug(`enableUpdate(): active again. Re-painting to catch up with ${this._disableUpdateIgnoreCount} ignored update requests...`); this._disableUpdateIgnoreCount = 0; this.update(ChangeType.any, { immediate: true }); } } else { this._disableUpdateCount++; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); // this._disableUpdate = Date.now(); } // return !flag; // return previous value } /* --------------------------------------------------------------------------- * FILTER * -------------------------------------------------------------------------*/ /** * Dim or hide unmatched nodes. * @param filter a string to match against node titles, or a callback function. * @param options filter options. Defaults to the `tree.options.filter` settings. * @returns the number of nodes that match the filter. * @example * ```ts * tree.filterNodes("foo", {mode: 'dim', fuzzy: true}); * // or pass a callback * tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'}); * ``` */ filterNodes(filter, options) { return this.extensions.filter.filterNodes(filter, options); } /** * Return the number of nodes that match the current filter. * @see {@link Wunderbaum.filterNodes} * @since 0.9.0 */ countMatches() { return this.extensions.filter.countMatches(); } /** * Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches(filter, options) { return this.extensions.filter.filterBranches(filter, options); } /** * Reset the filter. */ clearFilter() { return this.extensions.filter.clearFilter(); } /** * Return true if a filter is currently applied. */ isFilterActive() { return !!this.filterMode; } /** * Re-apply current filter. */ updateFilter() { return this.extensions.filter.updateFilter(); } } Wunderbaum.sequence = 0; /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */ Wunderbaum.version = "v0.14.1"; // Set to semver by 'grunt release' /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */ Wunderbaum.util = util; /** A map of default iconMaps. * May be used as default, when passing partial icon definition maps: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` */ Wunderbaum.iconMaps = defaultIconMaps; exports.Wunderbaum = Wunderbaum; })); ================================================ FILE: docs/.nojekyll ================================================ TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. ================================================ FILE: docs/_config.yml ================================================ theme: jekyll-theme-slate title: Wunderbaum description: A [Type|Java]Script [tree|grid|treegrid] control. show_downloads: true # ["true" or "false" to indicate whether to provide a download URL] # google_analytics: Ich Ich Ich [Your Google Analytics tracking ID] ================================================ FILE: docs/api/.nojekyll ================================================ TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. ================================================ FILE: docs/api/assets/hierarchy.js ================================================ window.hierarchyData = "eJydll1v2yAUhv8L17QLYOzad/2aFE3bpDXaLqZceIY0qC5YmKSzqvz3iUWqcnAuDrmIYjl5/LyHg4F34p0LI2l+M8kLyviiXlPi9abXXTDOjqR5J/Fu/LbtqyYN+fVn5bV+3GsbVtOgCSUvxirScFlSsvM9aYixQftN2+nxU5gGPV4nzPU2vPaEkq5vx5E0JIzqKj7k6gOMP25Nr7y2MRxnMVyxoIzXjDKxqCkTTFImimp9oCT+AUT85lR2RMDkR+SMMs7rmLOijEtJGS/jVRWvbkTMXlEmOKdMiFiFiFXIgrKSV5SVojiWwhko5bYLZt+G7HJmHKKkox92+37b2udse0Jh3cUCunvTvWSrAYQ2V8D8oNsLx/0Mic0gJcjwqEy4HYZ+yo0wB7EJymqW4P/nggCAw/qrZAS8dz5bDiCs+UZA89+htSpbDSmsu4Zv/GfX7cZcNYTQZtjvZefsXavyX/c5iEwgkr1lafNnG2CwXgZn2hc9Kfdmc9UphrVzDuxfnTKb6T5uJ7kJzqHYFALOux+602af3fsUQ9th5590PHTkyhMK605W+7tdCM5etNucQ7EpZJGMv1U6e7lLKKRb8lP3U+dd3y9tcD+Nfvs+HM9+GP9ZMvvwJHm9PqaqZ6lWLj/PB4McjZKfzocH3z7ndQEQWKcogNMNuc4TAuM8HP4B3BL+zA==" ================================================ FILE: docs/api/assets/highlight.css ================================================ :root { --light-hl-0: #0000FF; --dark-hl-0: #569CD6; --light-hl-1: #000000; --dark-hl-1: #D4D4D4; --light-hl-2: #001080; --dark-hl-2: #9CDCFE; --light-hl-3: #000000; --dark-hl-3: #C8C8C8; --light-hl-4: #AF00DB; --dark-hl-4: #C586C0; --light-hl-5: #267F99; --dark-hl-5: #4EC9B0; --light-hl-6: #795E26; --dark-hl-6: #DCDCAA; --light-hl-7: #A31515; --dark-hl-7: #CE9178; --light-hl-8: #0070C1; --dark-hl-8: #4FC1FF; --light-hl-9: #008000; --dark-hl-9: #6A9955; --light-hl-10: #098658; --dark-hl-10: #B5CEA8; --light-hl-11: #811F3F; --dark-hl-11: #D16969; --light-hl-12: #EE0000; --dark-hl-12: #DCDCAA; --light-hl-13: #000000FF; --dark-hl-13: #D4D4D4; --light-code-background: #FFFFFF; --dark-code-background: #1E1E1E; } @media (prefers-color-scheme: light) { :root { --hl-0: var(--light-hl-0); --hl-1: var(--light-hl-1); --hl-2: var(--light-hl-2); --hl-3: var(--light-hl-3); --hl-4: var(--light-hl-4); --hl-5: var(--light-hl-5); --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); --hl-9: var(--light-hl-9); --hl-10: var(--light-hl-10); --hl-11: var(--light-hl-11); --hl-12: var(--light-hl-12); --hl-13: var(--light-hl-13); --code-background: var(--light-code-background); } } @media (prefers-color-scheme: dark) { :root { --hl-0: var(--dark-hl-0); --hl-1: var(--dark-hl-1); --hl-2: var(--dark-hl-2); --hl-3: var(--dark-hl-3); --hl-4: var(--dark-hl-4); --hl-5: var(--dark-hl-5); --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); --hl-9: var(--dark-hl-9); --hl-10: var(--dark-hl-10); --hl-11: var(--dark-hl-11); --hl-12: var(--dark-hl-12); --hl-13: var(--dark-hl-13); --code-background: var(--dark-code-background); } } :root[data-theme='light'] { --hl-0: var(--light-hl-0); --hl-1: var(--light-hl-1); --hl-2: var(--light-hl-2); --hl-3: var(--light-hl-3); --hl-4: var(--light-hl-4); --hl-5: var(--light-hl-5); --hl-6: var(--light-hl-6); --hl-7: var(--light-hl-7); --hl-8: var(--light-hl-8); --hl-9: var(--light-hl-9); --hl-10: var(--light-hl-10); --hl-11: var(--light-hl-11); --hl-12: var(--light-hl-12); --hl-13: var(--light-hl-13); --code-background: var(--light-code-background); } :root[data-theme='dark'] { --hl-0: var(--dark-hl-0); --hl-1: var(--dark-hl-1); --hl-2: var(--dark-hl-2); --hl-3: var(--dark-hl-3); --hl-4: var(--dark-hl-4); --hl-5: var(--dark-hl-5); --hl-6: var(--dark-hl-6); --hl-7: var(--dark-hl-7); --hl-8: var(--dark-hl-8); --hl-9: var(--dark-hl-9); --hl-10: var(--dark-hl-10); --hl-11: var(--dark-hl-11); --hl-12: var(--dark-hl-12); --hl-13: var(--dark-hl-13); --code-background: var(--dark-code-background); } .hl-0 { color: var(--hl-0); } .hl-1 { color: var(--hl-1); } .hl-2 { color: var(--hl-2); } .hl-3 { color: var(--hl-3); } .hl-4 { color: var(--hl-4); } .hl-5 { color: var(--hl-5); } .hl-6 { color: var(--hl-6); } .hl-7 { color: var(--hl-7); } .hl-8 { color: var(--hl-8); } .hl-9 { color: var(--hl-9); } .hl-10 { color: var(--hl-10); } .hl-11 { color: var(--hl-11); } .hl-12 { color: var(--hl-12); } .hl-13 { color: var(--hl-13); } pre, code { background: var(--code-background); } ================================================ FILE: docs/api/assets/icons.js ================================================ (function() { addIcons(); function addIcons() { if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; svg.style.display = "none"; if (location.protocol === "file:") updateUseElements(); } function updateUseElements() { document.querySelectorAll("use").forEach(el => { if (el.getAttribute("href").includes("#icon-")) { el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); } }); } })() ================================================ FILE: docs/api/assets/main.js ================================================ "use strict"; window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings.","hierarchy_expand":"Expand","hierarchy_collapse":"Collapse","folder":"Folder","kind_1":"Project","kind_2":"Module","kind_4":"Namespace","kind_8":"Enumeration","kind_16":"Enumeration Member","kind_32":"Variable","kind_64":"Function","kind_128":"Class","kind_256":"Interface","kind_512":"Constructor","kind_1024":"Property","kind_2048":"Method","kind_4096":"Call Signature","kind_8192":"Index Signature","kind_16384":"Constructor Signature","kind_32768":"Parameter","kind_65536":"Type Literal","kind_131072":"Type Parameter","kind_262144":"Accessor","kind_524288":"Get Signature","kind_1048576":"Set Signature","kind_2097152":"Type Alias","kind_4194304":"Reference","kind_8388608":"Document"}; "use strict";(()=>{var De=Object.create;var le=Object.defineProperty;var Fe=Object.getOwnPropertyDescriptor;var Ne=Object.getOwnPropertyNames;var Ve=Object.getPrototypeOf,Be=Object.prototype.hasOwnProperty;var qe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var je=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Ne(e))!Be.call(t,i)&&i!==n&&le(t,i,{get:()=>e[i],enumerable:!(r=Fe(e,i))||r.enumerable});return t};var $e=(t,e,n)=>(n=t!=null?De(Ve(t)):{},je(e||!t||!t.__esModule?le(n,"default",{value:t,enumerable:!0}):n,t));var pe=qe((de,he)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,c],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. `,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[c+1]*i[d+1],c+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}s.str.length==1&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),m=s.str.charAt(1),p;m in s.node.edges?p=s.node.edges[m]:(p=new t.TokenSet,s.node.edges[m]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof de=="object"?he.exports=n():e.lunr=n()}(this,function(){return t})})()});window.translations||={copy:"Copy",copied:"Copied!",normally_hidden:"This member is normally hidden due to your filter settings.",hierarchy_expand:"Expand",hierarchy_collapse:"Collapse",folder:"Folder",kind_1:"Project",kind_2:"Module",kind_4:"Namespace",kind_8:"Enumeration",kind_16:"Enumeration Member",kind_32:"Variable",kind_64:"Function",kind_128:"Class",kind_256:"Interface",kind_512:"Constructor",kind_1024:"Property",kind_2048:"Method",kind_4096:"Call Signature",kind_8192:"Index Signature",kind_16384:"Constructor Signature",kind_32768:"Parameter",kind_65536:"Type Literal",kind_131072:"Type Parameter",kind_262144:"Accessor",kind_524288:"Get Signature",kind_1048576:"Set Signature",kind_2097152:"Type Alias",kind_4194304:"Reference",kind_8388608:"Document"};var ce=[];function G(t,e){ce.push({selector:e,constructor:t})}var J=class{alwaysVisibleMember=null;constructor(){this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){ce.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!ze(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function ze(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var ue=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var ge=$e(pe(),1);async function A(t){let e=Uint8Array.from(atob(t),s=>s.charCodeAt(0)),r=new Blob([e]).stream().pipeThrough(new DecompressionStream("deflate")),i=await new Response(r).text();return JSON.parse(i)}async function fe(t,e){if(!window.searchData)return;let n=await A(window.searchData);t.data=n,t.index=ge.Index.load(n.index),e.classList.remove("loading"),e.classList.add("ready")}function ve(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:document.documentElement.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{fe(e,t)}),fe(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");i.addEventListener("mouseup",()=>{re(t)}),r.addEventListener("focus",()=>t.classList.add("has-focus")),We(t,i,r,e)}function We(t,e,n,r){n.addEventListener("input",ue(()=>{Ue(t,e,n,r)},200)),n.addEventListener("keydown",i=>{i.key=="Enter"?Je(e,t):i.key=="ArrowUp"?(me(e,n,-1),i.preventDefault()):i.key==="ArrowDown"&&(me(e,n,1),i.preventDefault())}),document.body.addEventListener("keypress",i=>{i.altKey||i.ctrlKey||i.metaKey||!n.matches(":focus")&&i.key==="/"&&(i.preventDefault(),n.focus())}),document.body.addEventListener("keyup",i=>{t.classList.contains("has-focus")&&(i.key==="Escape"||!e.matches(":focus-within")&&!n.matches(":focus"))&&(n.blur(),re(t))})}function re(t){t.classList.remove("has-focus")}function Ue(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=ye(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` ${ye(l.parent,i)}.${d}`);let m=document.createElement("li");m.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=c+d,m.append(p),p.addEventListener("focus",()=>{e.querySelector(".current")?.classList.remove("current"),m.classList.add("current")}),e.appendChild(m)}}function me(t,e,n){let r=t.querySelector(".current");if(!r)r=t.querySelector(n==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let i=r;if(n===1)do i=i.nextElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);else do i=i.previousElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);i?(r.classList.remove("current"),i.classList.add("current")):n===-1&&(r.classList.remove("current"),e.focus())}}function Je(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),re(e)}}function ye(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ne(t.substring(s,o)),`${ne(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ne(t.substring(s))),i.join("")}var Ge={"&":"&","<":"<",">":">","'":"'",'"':"""};function ne(t){return t.replace(/[&<>"'"]/g,e=>Ge[e])}var I=class{el;app;constructor(e){this.el=e.el,this.app=e.app}};var H="mousedown",Ee="mousemove",B="mouseup",X={x:0,y:0},xe=!1,ie=!1,Xe=!1,D=!1,be=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(be?"is-mobile":"not-mobile");be&&"ontouchstart"in document.documentElement&&(Xe=!0,H="touchstart",Ee="touchmove",B="touchend");document.addEventListener(H,t=>{ie=!0,D=!1;let e=H=="touchstart"?t.targetTouches[0]:t;X.y=e.pageY||0,X.x=e.pageX||0});document.addEventListener(Ee,t=>{if(ie&&!D){let e=H=="touchstart"?t.targetTouches[0]:t,n=X.x-(e.pageX||0),r=X.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(B,()=>{ie=!1});document.addEventListener("click",t=>{xe&&(t.preventDefault(),t.stopImmediatePropagation(),xe=!1)});var Y=class extends I{active;className;constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(B,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(H,n=>this.onDocumentPointerDown(n)),document.addEventListener(B,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var se;try{se=localStorage}catch{se={getItem(){return null},setItem(){}}}var C=se;var Le=document.head.appendChild(document.createElement("style"));Le.dataset.for="filters";var Z=class extends I{key;value;constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),Le.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } `,this.app.updateIndexVisibility()}fromLocalStorage(){let e=C.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){C.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var oe=new Map,ae=class{open;accordions=[];key;constructor(e,n){this.key=e,this.open=n}add(e){this.accordions.push(e),e.open=this.open,e.addEventListener("toggle",()=>{this.toggle(e.open)})}toggle(e){for(let n of this.accordions)n.open=e;C.setItem(this.key,e.toString())}},K=class extends I{constructor(e){super(e);let n=this.el.querySelector("summary"),r=n.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)});let i=`tsd-accordion-${n.dataset.key??n.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`,s;if(oe.has(i))s=oe.get(i);else{let o=C.getItem(i),a=o?o==="true":this.el.open;s=new ae(i,a),oe.set(i,s)}s.add(this.el)}};function Se(t){let e=C.getItem("tsd-theme")||"os";t.value=e,we(e),t.addEventListener("change",()=>{C.setItem("tsd-theme",t.value),we(t.value)})}function we(t){document.documentElement.dataset.theme=t}var ee;function Ce(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Te),Te())}async function Te(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let e=await A(window.navigationData);ee=document.documentElement.dataset.base,ee.endsWith("/")||(ee+="/"),t.innerHTML="";for(let n of e)Ie(n,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Ie(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',ke(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let c of t.children)Ie(c,l,i)}else ke(t,r,t.class)}function ke(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));if(r.href=ee+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&r.classList.add("current"),t.kind){let i=window.translations[`kind_${t.kind}`].replaceAll('"',""");r.innerHTML=``}r.appendChild(document.createElement("span")).textContent=t.text}else{let r=e.appendChild(document.createElement("span")),i=window.translations.folder.replaceAll('"',""");r.innerHTML=``,r.appendChild(document.createElement("span")).textContent=t.text}}var te=document.documentElement.dataset.base;te.endsWith("/")||(te+="/");function Pe(){document.querySelector(".tsd-full-hierarchy")?Ye():document.querySelector(".tsd-hierarchy")&&Ze()}function Ye(){document.addEventListener("click",r=>{let i=r.target;for(;i.parentElement&&i.parentElement.tagName!="LI";)i=i.parentElement;i.dataset.dropdown&&(i.dataset.dropdown=String(i.dataset.dropdown!=="true"))});let t=new Map,e=new Set;for(let r of document.querySelectorAll(".tsd-full-hierarchy [data-refl]")){let i=r.querySelector("ul");t.has(r.dataset.refl)?e.add(r.dataset.refl):i&&t.set(r.dataset.refl,i)}for(let r of e)n(r);function n(r){let i=t.get(r).cloneNode(!0);i.querySelectorAll("[id]").forEach(s=>{s.removeAttribute("id")}),i.querySelectorAll("[data-dropdown]").forEach(s=>{s.dataset.dropdown="false"});for(let s of document.querySelectorAll(`[data-refl="${r}"]`)){let o=tt(),a=s.querySelector("ul");s.insertBefore(o,a),o.dataset.dropdown=String(!!a),a||s.appendChild(i.cloneNode(!0))}}}function Ze(){let t=document.getElementById("tsd-hierarchy-script");t&&(t.addEventListener("load",Qe),Qe())}async function Qe(){let t=document.querySelector(".tsd-panel.tsd-hierarchy:has(h4 a)");if(!t||!window.hierarchyData)return;let e=+t.dataset.refl,n=await A(window.hierarchyData),r=t.querySelector("ul"),i=document.createElement("ul");if(i.classList.add("tsd-hierarchy"),Ke(i,n,e),r.querySelectorAll("li").length==i.querySelectorAll("li").length)return;let s=document.createElement("span");s.classList.add("tsd-hierarchy-toggle"),s.textContent=window.translations.hierarchy_expand,t.querySelector("h4 a")?.insertAdjacentElement("afterend",s),s.insertAdjacentText("beforebegin",", "),s.addEventListener("click",()=>{s.textContent===window.translations.hierarchy_expand?(r.insertAdjacentElement("afterend",i),r.remove(),s.textContent=window.translations.hierarchy_collapse):(i.insertAdjacentElement("afterend",r),i.remove(),s.textContent=window.translations.hierarchy_expand)})}function Ke(t,e,n){let r=e.roots.filter(i=>et(e,i,n));for(let i of r)t.appendChild(_e(e,i,n))}function _e(t,e,n,r=new Set){if(r.has(e))return;r.add(e);let i=t.reflections[e],s=document.createElement("li");if(s.classList.add("tsd-hierarchy-item"),e===n){let o=s.appendChild(document.createElement("span"));o.textContent=i.name,o.classList.add("tsd-hierarchy-target")}else{for(let a of i.uniqueNameParents||[]){let l=t.reflections[a],c=s.appendChild(document.createElement("a"));c.textContent=l.name,c.href=te+l.url,c.className=l.class+" tsd-signature-type",s.append(document.createTextNode("."))}let o=s.appendChild(document.createElement("a"));o.textContent=t.reflections[e].name,o.href=te+i.url,o.className=i.class+" tsd-signature-type"}if(i.children){let o=s.appendChild(document.createElement("ul"));o.classList.add("tsd-hierarchy");for(let a of i.children){let l=_e(t,a,n,r);l&&o.appendChild(l)}}return r.delete(e),s}function et(t,e,n){if(e===n)return!0;let r=new Set,i=[t.reflections[e]];for(;i.length;){let s=i.pop();if(!r.has(s)){r.add(s);for(let o of s.children||[]){if(o===n)return!0;i.push(t.reflections[o])}}}return!1}function tt(){let t=document.createElementNS("http://www.w3.org/2000/svg","svg");return t.setAttribute("width","20"),t.setAttribute("height","20"),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.innerHTML='',t}G(Y,"a[data-toggle]");G(K,".tsd-accordion");G(Z,".tsd-filter-item input[type=checkbox]");var Oe=document.getElementById("tsd-theme");Oe&&Se(Oe);var nt=new J;Object.defineProperty(window,"app",{value:nt});ve();Ce();Pe();})(); /*! Bundled license information: lunr/lunr.js: (** * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 * Copyright (C) 2020 Oliver Nightingale * @license MIT *) (*! * lunr.utils * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.Set * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.tokenizer * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.Pipeline * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.Vector * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.stemmer * Copyright (C) 2020 Oliver Nightingale * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt *) (*! * lunr.stopWordFilter * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.trimmer * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.TokenSet * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.Index * Copyright (C) 2020 Oliver Nightingale *) (*! * lunr.Builder * Copyright (C) 2020 Oliver Nightingale *) */ ================================================ FILE: docs/api/assets/navigation.js ================================================ window.navigationData = "eJydm1tv47YSgP+Ln4Netqc9p/vm2EpixDfYSnIWRSHQ0thWI4kCRTlxi/73QvJFpETNjPuwL+HHjzRJDUcU97e/Bho+9eDrIJRpKrPB3SAXej/4OkhlVCZQfH/6+3d7nSaDu8F7nEWDr1/uBuE+TiIF2eDrb1fF2HsYvkz9YOzdvzxOvVdv2ugOQsViYwi7sN3GT1/+vuuYZ5N5MFpMg7fJ2H9iyC2e4V8t3oInb/L45DPkDYyYI9iKMtGTUGYzkReItkUizsloMSeHoIEw03z54gfP3rc1ZrpCiOnZ+xbMF+OhP0RMDUSY/EUwWsxmw/k4mA2XhNGGafN8+Dp5HPqTxZwnt3nEP1+MvcD/tvSCh8V07K0QdRtFrCtvPvZWwWz4/2C58h48f4RNu4NmuCfzW9wGjbrX3urVGwf+yvOC9eJlNfKopdZbB2nH99Z+8DCZesFyiD4RNkgZn/wZFr6uDOaZ+FMvWC+H82A5HAffMF0LRSNKKNNcQVGsZalCGAstGvO2zEIdy8wIK13ctv/yH8OeineYywj8WCcwEzrcg0LsLpxrX2uh9I1NmHWQdrIrL5VG7S2y7bwbhIkoinrYcwWh0BAN/v7daEofcyi622b9Z9auOdqLbAf+MYfGAlmZXhxNua37nxl5xGEmI/CyMnVLDACxyAhWsIvNNMCSXMtxx1oLXRb9v8hm+l0ryCJQD4nYuT1Neb9jGEWj86Av8nrmG1ecaVBbEV6nqwu3ZvDnX0x1nifHkUxTkUUMd5fG5COZlGk2hm2cxdqakY65jdJa7wCZnmRbSVqvJCYdK7GrQXvKO0qLw4Uy5wkNDhN6n7nIomGS0PPURjHtQ5xoUCOZZRBSfe2wtLh6Tgq6x10YUz+Crp49oL0tEpOek1ZiCAwKk02liKbizyPz57twTD8T7/AaF/EmYYxBF8bUc3GId6yxbZGotNqhjjmwYkEXxtQrSKRgxC6Lw4VVROYIDQ4XFqAXiiltsZh4HSqZJJNMy9cYPmi5k6cb8CVXfSVRKehhqOMDY4W1UUJ7CvcsrYUS2lM0BcYa68KEeg0JhJqpbsGUmhcg1/wAeUZLRjxro6hWKs0wNhQuq94Phn+ITyKO2yCtXGz+oPfHNoppfQV1AsmLiQ4ak7/kEWv2LQ4TVvuHXskPxty3UUz7tqmfb6GBkyo5aFx+X2ots1ESh+88v6sC3sTpzYZnb7GEmN/rW/o7BnHTiDt4vAEvinX9nsDzd3FaX/9j2y2akCslFdNsoYSW8apiUISs3mCYnbRZXPwgw7LgeW0U11YJ872Izut+BUWZ0A30VbqlqRsbYegz7rKzSFz6DMdIfmQ8bxvG1TMZxdtjfRjA07sq4E1Uqbp9dufQXiBaxeumReLSFYQQH5jeNkypq/Sfa7ZYXHxK83jiFouLqxyCp7VI7vGRba1VjmMjh/GHX//748/m6fC9lOeTixUUMjmYh5+mt8uxzGqtVZzt2C04eaql0R7C9438PNVy222GNLaOyKZxoXu8DpJnv+5BM5FjbpOjzOPzoXMsu69mprnLkebrKWT/2rMZ0lgdxm23EOphksgPQBa1E+X7OWKu8XSejRsb5jZj/yrrcreZ18ASr4H2HjORxmETDnq8bewWrxUEaL+FM9vhhAwnyvRX+Q7D3WBML3tk/sWY+FImOs4Zcouk7NU7ARk6WhDlPB1hz86Hl26lzfCMZD87GOV9VDEdOFsQ5aQWF39VTbIClJ6j42gzlPEZjpk4kL+4g1HeqdztGPPTwSjv+dvwSCTJRoTvbmsLopznw3p0n7AZ0igjGGZHvJctiOM8f2QitTbHN6+gyGVW9I1Bh+OYl0rmoPTxETSr726e09Ip46dbsDmWuY7ODLPFccy+HMecPtscy2x9JupNWp0ox1+fZdIdtzC2l16IFkZ5TzOO7z82Qxql0vfHy1pFk3cn6vS7b6PYjeIjbhL/uolnICJXC+KMVf3dDhl8E6F91aeEKqvGhCbDM1I2jomRlN2UjflKZEUiiG20TdHWuKi+mGDGhqBsb5uRyEJIqhtvvSeZphypQLd1PZrE16gDdLvNy16ljpPuXa/qr6yrXmPYglIQNYr6YbsoLsW268cv5n2mV5HEUT2R9Sl6j6lFYcJ6fC8j4JqSWtihyPz7fNGu12gCdNjXSyXTuBvva1VTTIniYiZC1zXM2lOXIjcvq0u1k7nzanhd/1yOGRYvay+4f/H9xdx5D/bkMSnEJiKRV1/4/b2SWifG4DTXHGthG0RuTlaLSOl+U12M1I9gI8ss7O/LBcAcMizT+qkX0bFfZFJcW2cZYVLnorLcIMJ9r6sqxOomkD4omT6diB6HATFcpxxFOi+8Wr4LiDnt+NIWdeOKXbsIRQ74b7sipGcFO/gkRDVDms6bK+E6U5itPuKXp4S+32ZSmO1TQxb1a+pivL4Soa7G0q/+gohMDjHuQLdzlJbrSuCWV5GUUC05L4EUk1kg4oyrEa0OCntlVwKzFEOlRH90OZejBi/N9fF0YwXxGBRqu2yIiOqCoJ5lIuKM7JVBIba0VGmpnkSx/6nXZTCIKZOy/6GrCpG68vTNpLf6uRwzHECpOIIZ6L3sf85sDPEpWeW+vZ5TMVK/AF2t8nFc5AmyCG0M9/lxCrLs5kldpU3i1vqh9CX67NoY5ksA+hdBXYrU1lSWo+nsRuOBQ1NRQ8vdLoHLFwTEY2Kobxl/Atahuhw1WN+COvU7n4Ds2lZ23q7cycytumVWiC2MRNHffIN0POZb1ccmqP4bTvfF6lzAerd6K6t7AhtRpnPLdXkvurhsrvtm1OqYbJ/cGH2TzqMad/eqay1N09hNQEPsrIReVLi1BYbdGpAr7hiQa9mN8+WYq0b11mM9TdXv/wDBLffQ" ================================================ FILE: docs/api/assets/search.js ================================================ window.searchData = "eJy8vVuT2ziWrv1f7Fvv2kmc2Xcu29Vd0eVyfWV39Z6omMhgSsxMjiVRTVE+9MT89y8ISEpw8SUIUMy5mIlqJ4H1klpYODwLwH+/aOqvhxd/+fO/X3yudusXf2GvXuyKbfniLy++HnfrsrkrjtsXr14cm82Lv7zY1uvjpjz836c//fDYbjcvXr1YbYrDoTy8+MuLF//z6lxXxsyltn8OazuV8Wv7Z6jiVy/2RVPu2r42z9wNExd7X8rmUNW7BGMvn4pMGfWKjtk/ttUmxfjp+QUsV6t6977YH1Ksd2W2rsw8BexGPP3WD2X7qSnLFAEPZdu6IsvY/7Vep9rfuSLL2H/3pdy1P+/u60QRZVeucuXmKZHZUxNe1btD2xxXbd2k6OgXW8Alm7puUwScnl+iMayTmsF6Gavlptx2xRJMPxVZwP5jWazL5l26CldwUS2b6tC+qXdtUe1mSerKr87lF1XWNfhfqkM7Q1RXtBO2qJ510RYpIk7PL2C53rdVvUvqMZ6KzAyYimXiSUGxaqsvZWrUdqWuC9x9Hff16nhIlWELXaWi92u03/dl0m9xLrCA7VW9OW7TPOGpyBI9RVmsvyd1FacCC9i+TR2z3S43aLvr3mPVHLd3KQJ6pRZQcV9t2rJ5n+r+ttR2Mf93rfpNvfl5/S09GqzqTWXLLeERq3Kz+bX4kvpFbLld8WW5b7IpDu3/d6xWnw9l0aweP1XbJEFd8X89FW9d8WfQVTYp87uBLld8kUh+mgbNmAUtMwm4XRWb1XFTtOXfy6SI9lTwczk/svW1NOVDN1ppUnu2S8HlJke3x91sNU9FF9SzKjab92X7WCdNFWyx7bnYUjrstDFZRvnlmjHoYOpe77f1of1jP2MO3bqyX/aLTqZ/qb+WMwVtbNEl9RTr9ZvHarNuyqTFpWK9Xj0VW0LHfr/5/qbebotdkuPacqtLuQWUrDZlkba+cCqwgO11eWibOim+PhVZxjs/2JlQol/W50ILaDjM0XBYVsNjcfipmwUlLTYUh/tTmWUU/M2uXiRKeDwXWkBDc9z9s2of35b3ZdOU63/s10WbFLGa4+5r1T6uTxUczxU8l7bXh++71dUCi1MtC6gsv+2L3fr1Jmke5goVm/lzMdKeNuWqTdTgCi2moa0fHjblR1tp0lKALXc4l1smwjkZ5brrf5Na+EPZHk5ld6eyyyj6vbz/e/k9VUtT3n92pZbo9epj2lDtXGAp2//YVf86JkUXW+x4LrbImPWxXH1OG6+eSixg/b5KjhRdkcXaaFfZj9+dK6aKuPvunHE5JT9VzSHJH7tC96dCC2mY8SEW/Qa/lt+SyV9Xbld+WxD/dTX+XnYT+PUcMY0ruqCeutkW7W3Vpo2N+sUW05EuYakVoVO5BPuXEovYX82YNi47Z3wo29fnxdWOeiX2n5cF1tKVXVLRjLn9EvSHaLFB1E7vE7XYQLo6FVxo1aOYJ6VbU11WyU9zoFj3Ua7mYmRM/LFtqt1D2nj4cC6zgILq8G5dtYkSqkN5KbSkhk9Vu0n6RS5C2lPJRdT8Uhfr5C+yuRRaQMOmTrLuHl/G7tvy7phqfH0qs4yCd02TltyzqR/KU5llFKRmOW3qh6sSnKj9ZBZWP1zFv5D9d2lrsCcJ5VIrsJv64Z9FkzSw2NQPX12RpVrCvilXictdtjU8lVtilas8lO2b9CQGW+7aTIb+KtKqqTebT0lNw5VpF2obly+RuC68OhdaYoXgMHeA15VceoQ3W8zzaEleMT+cxlTL/Tazhna3h8XHdt0qY5sYPbrVxXaxyHGYoeCwqIJjMi5Ykg8cyvbdrrjblEk92aFsy0upZUa36SKqw8Ia/tok5g0fHporcoeJ9Tcu6ShNwCnjaCkNv9dfkyU09dfFFHR9V/pn6DqvJb/DoWx/Lb5UD0UHSmcx1t2l+MLEt4tVyZ3H4VxoGQ2fUhNVD2V7Xa5qX0HdtHNSMbpyy66rdTX++P23pt6XTZu0BN6VvPu+fyq5kJpUDUut0LytVu3rpimSPkJbr6tVW5yKLTGycR2jG/3+s1q3j0lueiruxsFfz8UX6+F/K3fd4sf7el3dV6siOfXe1bJ3tWxJLYtQvaYsWjtG/nmVFvFORbtxYbVaKtp9qQ5VkkOfCyxl+/dui2Sq/cYVWmR2XSSuLhQLjUWaMtX2pcQC1t2gLj2JxpVbcmzsMtyT0x9csQUzHyzAf1+0q8c0Ibbc9lJusS/yY1PsUrW4kndPJZf4Ll1C4U+24uQ8xPtzsUXGzU6EW2RIGz07HcW55GK9TfpnceWW/C5/dhC5aOvmP5O+yanQfA1Pe9nvbnf+IsdlI7v79xm72PGayam6/mMTW9lP0kY2WBzKfx3L3SrJ1kuvUNAoKTxnF3FARdQ24ikh/Z1xcFd5QEJ4T3mS7bHsgYD1qfSBtHfHuC/08mHMl2T9M8rsCdgOpvUkWW5G8qsCxqdSq5Lsj2dthFx/cnqZqKFcfb6rwW64oIZLoUV+h2Jd1Q9NfQRbqkK/hV9skW9Rbw77IvHnuJRZQkEFJ0UB8+GpUJLtTfHvtLZwKrCEbZdmjZZmA/a9QktoOKcTJ3aHl0JLaDjuTsned4kBmRRcpFf4vk/sFFyBRWzX9aat0sLBU5mlWuKnGSq6cssqOdeRFJUuZZZQgE+PCJgPHx+R1irtunL3xKdUf3RFuz8v6Jm3gbSkgJbbiMSkNB1NNxA/tD+nxatzsRBSStNhM35wok5Ihy0WTtZJ07Evmm5HSJqKp0KLacDLWlMiwktbSSrsYkySgnOJRdrr8c6uIr3Be1hCzfV4Z4VMbGZJn9l0e9P+Vj08bqqHxzRJtni3Ke3RK76Ip7glkNfHtn43Z+hzKl8c23rhUdBtU3+Fh2MEg0r9NXgwRrICnEE+ISGcOj6lITIfODgAmUoITtIQ2rUf+hIR2/aTdAR3pQeERG1LT1WSujbWqQinEqUpCO6MD8mI2hqfpGVVbzbF/lB+rO421e4hcYB4Knx4KryEpi63oqsmcQ7Vrk6FFtHQFk3bJbKPZLGHR4xN22WyX7m+FbvveHJyG9xQmKRidEdjQMPklsZkBW+rplyNbT2ZULK2ZSe2nyQrGtnbOKFlYnNjsorgnr4JLVGb+tIUhXb1hdTEbOuboWSGiIXsP9g85+TRyIPNdF5wPNLp2NQ7e6ZkspKu4KZazF8n9rKFxcRsZktVE9jNFhYTsZ0tWUv5JXEO2uk4FVpIQ7dV+TQoSFXS7VY+XIoupOe3dLz1ULbXEi6sYk4DcjaXbUG/FW3aHN3qaK+YpQ8UNOWXmV6yb8ovy3rJY3GYNcd4LA4LzzE6Jckj2E7GgiPY8ROPwhomNnAkaagOY5kmocXvw1SSSaqG3ao8tHXz4T5Vx6lgfb+YFuugyUKsey6pouvNUzWcyiykYAYlrQ5Xc1Ki4m15WJW7dbFrk3+S9aXogr+LW8ZL5nbVofQLLqQluIs7rCZqG/esb5O4xHn+MtesbA4y6JrDrJFRl0LXHBYeGVWHbuA6T043cl1eTSL/72RckwEAdvsn+8jmXGg5DTN+jWvJHlHxW/FQ7R6SFwOqw94WXHAloNPSFUyOsc7egvH1tzlozeq4kq0BHamY0cm4ijPSPXRd6lOihuZUZiEFZfdccnttnootpKOuR87lCuuo64ljuRJ1fJyTPVQdrs8fojou+RKpSi7ZEotp+VTv09dDqkNb75dcEakO/9ht5vQtx93CvcsfVddnp/4wXy6lFqGK92W7euyQ+Iexm1mC3Lkr3RHxyUtaklQlR/YFY3pXVfLYpyu04OgHnjMUNL/UaGP8pKGw+YmjhlI1jJw1FNYwcdhQqobkJKbJ44ZSFeCTdsIKwkftJCnYFp/LOTGqK7dslNrWX0p0vk1IRP2lDB1vk2T/tLc+7TN4hZbQ0JTdKyXuTjgVWc7+rGVZV3ThlVlX6fui+Zy6T8AW3J4LLtKLunFsaurlLngcfOLXOHTkK7HbsqUW7LfcrvzELKl1FTpAPJVjjJ2dEUYYU0dmJKlwZ2v9vGvrP6rya1rGiS1a7dr6iyu6iJ7zWVSpaTiLrt53p/7MWfjrzv1ZduVv/DyssIwlecqhbPGJB2EJ1+3voQpSd7sdyvaq3W5RZ1AF7E9ttE+NFRO3DYRDRuR1A6maRu8bCKuZvHAgMY4/3QmR2Fz9ayEWa7G3xe77x3k7obqiS+yFIjla35ycqt7x1/dt2bzZVOh6goCw++rb4VJH0dWxOtWxuMKfmnr7bjfDzXsa75t6W+4W9ffDUxtMDUQLe1jgnKsJHRMHXaWqmJGlWi6an9ppmLHRrVNx7T63hDO3QkqiDt1KH13/Wtjz7C32b1JH+7aGna3BpQA0y439p84Em/hWMYeCJetJVrHcL5Vq/VJkkZ6zqR4e7MW21f339CzEU3F71Nb3JXMRe7rmS1pIzcg5WwEVEwdtpVt3IDMt3NuC+0vBxbTM2rJhSy68X6M6uCOfUtHI9lJqCRWhM3xCMiYP8ZlUcZHw9e6W3lTvHeRz+pM127fm1SXVpbYngxSyVLu2bO6LVb/ewfPAUv+9zmLHdmF+Ljdl6003U+y+9EpPCRhWMb6Ru+m5fJqiS+HlBFmC0eeTSZps+UlQmSxrW+3csskV0rbVzi2hLC+v3O5b1891qdQg6y1JqK3NdntdenVcKlyy5Kb++rey27z727d5Kpv666OtYB/c45osjN4jniRq8kbxGYK6/cRvTjsD56nqalg91bCgtPV/HQ+t+x1nSrM1PJZT27iTpa3q3a5cdZdb/9iUxXrVHLd38ySeamrr/Z1f03JSn86Mfl+vS7J0niT1qaZtvS6nl9OTpT72b9xNEjd18e6c7uux/vpxX+12czV1FRwuFSzbZnsLiMntdWodMb1B0DPF0hpBxOFis0Lt/Di7uBS64JKsKGLpJVkYPXQpSdRzCIKnYSWpij0WK30YRK6SSBv1TF0pMUPQffWtXL+pZw4YbelVvexQ8XTnkTsldv5veKrm3q9mcZHvy93xKnlbV8Hiwn4vD9W/r/54jVfL4hI/1s0VTfRUyeGpkgU77bppf6o367I59Pf5p/XcddPeu1om9/2nS7Tx6b2fr5smzpbfTqTtJsv617FafT6UReMdJZWkq1/Bgt+rl2vx4TRX7tO4tA/Yy8CoTxPnSTSXvtywm9lbrMMnxqR3XetqZkM4lVyy0+odIJ7YZYUPEZ8zlO7SX/wchbSh9FPp5STdlfd1U76+Spir4xnluTZ4jbjyXMPS0hxtvkba4VzDgtKObVvvrohZroLlY9Tqsdg9zO3Hz2UXlDP/Cz1D/L7bXKHHK73kCvZ1EWv9PEFhXR1WRTO3t7sUXnbG/WOxnuvZXfG7U/EFu+Hejoe0fnhq30O6mCsi+DPE7vtetmbasGAqZzPde3Zzx0unkstJ+Vx+X9dfZy5kPRVeTlCXSv6Lv3crSVFXemojV7qk+XKWlrIFSSFpHC8uLSSdiJWr0k8PT6Nhl8JLCtrNXuyf3lYxUw7YSjtDWNy+2pnrBtesGSy7VNtP7k5bpJ3M8o6R4+df/Lyr2tQcDFjm2jyM7gjAcjf1M42bfvlUweTHwS891qlNRaSApvDZ8bPknBYB52t6qmBZYW3vHtpkWRNX0s4UdaiPzWqqsQVUXcovLCsu6SgkDCUetYf1/6kO/6faPZZN5RLAr5calYwUUgoSkp5DaHSSUkDrSKLSc8hNSl4KSA4kMD2H7JlJTaG4HZPY9ByvEp/sFFA/lvD0HILjkqBC3RFIhHoOoQnJUQG1owlSzyI5PmkqJHksceo5JM9IpgoOCyYSqp7jFWYkWQVeYTLR6jleISr5KiAaJGA9SzcenZQV6spHErOeK4ZEJGtNxI9BwtazNMS4JK5Q40OJXM/VhVzXf/yvSIxL+ppQivKsnkNwXDJYaOrzvyQ0IUksoHY0UexZhptRyWOh0SVIIHsOoZFJZQGlMLHsecYNaclm02sJIwlnzyg+IgltWvYgEe0ZBccmp02rxglqzyg9MmltWjlMXHuWwU5aMltw/SeQ0PYs0mOT3EKicaLbc8iNT34L6B1LgHuW7zs7KS70wSMT455l2WsSRofWu3bP3zNGJNGF+m+aSPc8nXdEcl2w6x4k2D3LlCguhSU0JUJJLM8hNSkZLyA4kJD3fLKjkvQmRYNkj+eTHJW8NykZ0M5nkRyd1BdSPJLY9zwT/Ihkv+D0fpDw9ywyr/ui/0v9VVxyYKjTQgmCz0OUro+4I4mDzyI3KpkwpBUkFD7XCk9MkuHE+s4w0fBZhi8RyYeh8csgAfFZRF7ZY/0v9VUxyYqhYdYgYfFZvHM6iTHkmDSR8TkkxiU3BlSiBMfnEBqZ9BhQChMfn0XqdTL/NyTGJ0mG8gVGEiWfhbBHJU+G6DpIoHweoRFJlUGdg8TK55MZnWw5KXgk4fL51rWuXdN6fkQSk5wZgiPDBM2FZDJvWXa79Sje+fAt98/Bg7eUF0uKz2XnQvZMTXey2ZP/3x939lzTS6Xo6XDC50nktO2P3UXyaQL8IvNV7C7V1U0bsk0enG9xXa7q7b4pD4ePNq3wbdEW42bR04m2+ZPTvH330+t//PLp9u27H//x11/e/fHul4vlL0VTdcvyF8vDZ6+3+/uHf97+7d3Pf/3bp2m7T8/Ot/vzmw+/3v7z57ef/jZu7+mZ+XY+/fzpl3e3H397/evtb6/f3v7HuDX65Hybv7/79e2732/fv/5/t7/9/u6nd5/eBF4SPHy95Z9/TbDsPXy9H3W1vfnwy9RPCx+fb/3XD2/f3X76j9/e3f704Ze3734fN0yfvMKz3n38dPvTz7+8u/3tdehV+89dae9vn94HAsPlkflW1uV9cdzYA/7fF/vDuC3y4HyLf3/3H7e/fnj7+tPrcWNPz1wRcX797R+fbv/+7j8+BiLO5Zlr2uDHd7//8e7t7aff3727/fjhH7+/eTdhdrTIdV/104fbX1//8fNfX3/6+cOvt+9f/xb+wIPHr7b+5sP7969/fRtl2ns20e6T2f4Wh/PAy/5rcNzFbnKdSa+pNVU39C4/fd8/jTVtNafK/AfCap2kcVNvTpllJLfSN9Z/5Dpz3RDJngA++mq9J64z1sWGwHs9/fk6M6dktICl3hOJxry9WG6c9/q/im+9r+dNQs6f0H8uzV5/ttPAfKVRKy9dgYAlUmrM8L5oii1cCRy3fSlzvfm7ev09zfipxPWmz7vgkqw/FZolYNhIuwe7/RuBVuo/cq1Lf7j7ry5RPcKpn568wq1v7+tmW8DFhoCtl0/Fpr+y90ZjIr6UzWEknT2o4qncEjJGd+WFNIT34qUJCOxXDEmY2qWYKIJelhGrYuqujESf+Fx+H9nAE3SJz+X30PnFiSL29aHq4kkxGfwHQnpFl2klxeZYzvkmtuBVXwWHxYmQeP2o5bRw9abYbO4Kj4P7tsgz1xn8593PZ+AaNAqeu34wGLToP3C9qb+X3yetec9cZ/DHut648d7v5aHefPEWEH2bw8cWMNt8tFtqY83Dx6+T0S3Fvt6FPzd55nqD7jUmbfYfu97s+fKgv5ZtO9Fsxx+/XkZ323C4NQ2eWsjo7+VhX+8OOCoOnrre6Cd7U+rkq/YfW8C9LGGadq/eY9eZfVvum3Jldyx+IPMC3+zwsSsGx4dqt8I/5YiZl7e39u+ncqHOFtTgCo9unqy8Ez/itZyKXStl8HN83xXbavUUs7E0+tSVPuCq82N0yKz/3CKGh13E5Fs3i4uIWKWCTy5ifGIZafDUIkanF5XQg/Mn4v+864JXjzgORtpPz1wRX0KbYaGZ6VsMaLH5s7yh6aj5XYTpU+l4y5cCVxquN4d9kfLKlwLXGS7phdWTlievqo41PbaJGZsN3kqRYjKwKXnc8tRlD7ECPpeBFUtqOHQTdazBLvEu3uLp6etMNsW6qh+a+pjwjXtlrjRvr5dOMG3vlb7a7IHe8jtpePJ+32jTl0yy8NrsQMAlb6x15a6T0fbu9Z20Hr7RN9poamteqiW3SZ96kQ88tfEfm466ESZWwm3blOXPCU5uCwTOzwsY7p1v6PIHwv7tPXTFyGd0pwA2MnE28aBQKHG52j0kGH4qcaXpXR0eUA4s7+q1K3Cl4dNQonk3OQgZfvVT0cjRSIKY84lXc9SsvLILyfkl2IGPKonoySNE2DG9naslfQ5bbHUptoSIf+xW82Qcd0sL+bwb2bQRlnEudqUIO1JK/0lsscW+hK1tzk9iCy74k5yEpP4kJxkL/STuZIkE85cCixj+sA/NlUeM1/vJ6XK0gMQQ5QotE6DW9SrBsnv62ihQb0JHqYwFgXozeeFXqoTXh5SXPys4LPcN3pZzFKzLhST8NHrew7iAiUuU0s3bwxESPeF00NK55PVSxs5RGpcQvL0vZHqIpd8Uu1W56Tz73Zdy1xGk42Y8JSnw/DXLop+a0lU3NQXuPXjFNGFq/je0EzcJ7L/ImPGmTDbuiixg/NhWgZQTaPxUZAHjZfdAovVzmXnmh8vvUX7We/AKP9uNbBAct/MyePUCLBdy8p9393WigO5PlSu2kIgZAqJ376WJmWh5SAxteYuJmWqJQMygJS4mZrJlAjXDlnmNnH5LPZ9aFNVaBw9f0WL3TfllbFtv2N7LrmhU6x2+3NxfZURJXMyMljEVxUZUJO1pThYVE91GhMEIt7i4mcKeUdRE9BsTNScCRouaioIjomZFwpCofvh5Yw+Ligo+5NErQk814c3I0suonpq+zqiA/bF9tym3M1Tsj23pSi4n5Y8uvXieli+noouKwbcQRYmJgCiRYqaCMZQxKxRHCooJxFDU7DCcIGyWqGcTNBGAsaA54TdS0FTwhYJmhd5IQZMDHqho3kB0XBLpC7pz7uK6gt6T11DMyc8wtBQ56CNvMzfIAPtRQ98485M9ITAf1xFGmZ8MHMD8vLgRJ2cqbCA5s6JGlJzJoAHkzIsZY3L67fPt5XTHqEYKHr9mgaf81k5NF8csvuwKR7Ua9IpzQ8eomrj4kSBlKoiMf5c5o5UEYTEjllFxs0ctiQJni3tWYROhaFzYnHiUIGwqKI0KmxWZwsL64endumpf7/eb71HRafj0M04oR4zFdaXgva6ZVo5riZ1ZxguqN+vJqeWYnnqzjptbxsvZlV9ny9mVX5eWEzfbDf5ecRPehC800ZOMfp05HUm8rJh+ZEza7G4kTd5cac8pa6IPGZU1pwuJlzXVg4zJmtWBxMuaHM+N6Zo3KQ4KG3Zs9v9i+7Xew1d1a5H9ydBgYnfSf70rghNQMjs2RYmKDU1A2FWRKVrcTGHPKCoiLCFRc6NSlKiYoAREzY5JUaKiQhJQNT8ijckiAanL5Y+LRr0nn2+7AbQUtedg8DZzgw+wPy/yRMmJCjtA0vyYEy1rjqTnkjMVapCcWXEmSs5kkAFy5kWYKDnT4QW1qXmxZUwQCSx2W0ZcZOk/ekVoud8Ugf1E2NLLU6GJyEJeZ25oQQLmxZY4QVHBBYmaH13ihc0S9WyCpgIMFDQrwsQJmgwxSNC8GBMnaDrIIEUzo8yopH6Y+am7WicqyvSffE7QCCxFggLyNjOjHLIfFeTizE+1XGR+VsONlDPRbqGcOc02Ts5Uq0VyZjXaUTn9BnI5Gi+qkQyfvmbNYVXvPk6c8TFi0B5BEXXcB3i/mX3zmJRZ3XO8rJgeekza7E46Td5cac8pa6LVj8qa0/LjZU21/jFZsyJAvKzJLmtM17zOOygsFJ7ATqpJsXN3U/U+0N3YrY5RNl8Ob3Sc/izeu4ZEvemqvFLZ6lTHM8iLOFsoQmDsESUREol77SLX2nsPPufi1tBQ5NpW/1XmRuuh9XmBOkrMVIwGYmaF5xgxk5F5KGZeUI4RMx2PgZfMC8UjcvrN5O/uWs2olkKffc6ZFLQVOZcavNPMYSHWEJWCFithKucES4jKOImVMBU4sIRZsSNa0kT4GJE0J4LESpoKIljSrDgSkNRvu++fLleNar/o+SvacHdodNEG74sI2HzpFw97MnzPsQMCxq6anRY0vGD2WjFTAWZcy6yZZ4q0mLnnuLzZs89UifPlPa+0iQAVkDYnSKVImwpU49JmBasUaZOd/ri2eUOgCXH9aPq7u7g5KpLSZ6+Iog09qj7W3EuvZDhkDd5sZrjCMmaFqlhJMWEKy5odolKkzZP1fJImwtKIpDkhKVbSVDjCkmaFolhJk2EIa5oXggKi+uHHXdIQFX3Io89IzZGlOKBEX2dmyIECZkWcSEExAQeKmh1vEoTNEvVsgiaCDRY0J9ZECpoKNVDQrEgTKWgy0EBF8+LMuKR+mPnx2Lb1Ln6TLnr+GffYjJqLW/OAbzc2U6y322IXnCuOq3kqvJygqRY+rmZWM0+SNtHWA9LmNPgUaVOtflzarKafIm2y/Y9rmxcEJsTR+Y49jjpuutN79Jr2f/i1/Jpq6mV12NlSU9Oc/gsFhhxT2yGgiq5g1F6ISCHV4c30TSgj3yP2SpRIKcVm86bedOOew4/fw0feQ0HFZrOqN12IPtx9j9jLFimrKU+HlM9X5qp4DnHTs+URH5oxh4gSFDdXBqKumCpHC5sl6tkETc6TkaB50+QoQdOzZCBo5iQ5SlDEHBkomjtFHpPkd1jnK1/elvfVrgpjh+GzV3RZ07eDjZiLvSUMvNmYlFMt6UouBRcSMtVxjQqJ6rfihYTv1RpTEXG/VpqEyVyYkJK4FJigIHgnae9h/75qW+GosPfFPq21+K30jb0DPaaN0ievGVQGhgXQzMvJUcDgPcZ6kvA9UNh6zG1Q0QKmPG9EQpTPxYr4Wq3bx1QJ50JLCNhWu3/O0bCtdkvKaMpD9e/wZQlYh19wCSGr46Gtt/aT/BbouLAYV9h+l/1U3xUr6HQLwIxP0yu5nJSpqwyCYmIuM4iVc5i8XwNLibxhI0XGhyZ4tcu4jrqZvuIluikHr3cYacbTFzxEt5ypIdZIm4kaYMWKeCyLbt47T4orvLAg1zen6Zjsz6PN334tq4fHwHwEK3gqtoyIeeH0dtlIelvfH2aIqO8P10igA0z66C/VAd+Kjh68dnhpp4rhlQ7y4LMOLvtWYseWTy8xanjyVx5YjvuFJ02XweVRaDtiYTT2vZN/2Bg2NG4cu/blybGJ0/Cx+W79z7sIl/Yees7Ea/q7RmVcT/6oq2JX76pVsfm1+9/x5i/ldq7clTImd3r3rUfdIzRpdPJkgL7RqPzySaNN+TCRmUvMXgpc+0vXm7flfcpP3F2Rd7+I4Z9DIRMZno6YsYZT7S5idvJUr4HhuLO8IqKkuxDuPb2H24+Q/UcSoyMx59Ibgub6j1xn7tfiS/Vgk9NHzfUfuc6cPULujUspGDVIH7ryDet16X6g32kCKl0e7D+2lNk3xWZzV6w+T5g9P5Zm1jz15vbY+t5HLXfH7aUXv/w5sU996ryL3feYql+654JjlCet0NLav7k6ZCrixuopW92tl21zXLXHJurL2ZV9r8A1tu1qWJzVy6NX2au/xhmrp5IQpiwdkj7pUt/z0Bbt8RBp8vToVfZWTb3ZxNk7P5ps76mJO374k5/K6pt8+vPsJr7alEXzvmg+H/cxJl7a57fn50Ov5mmHlt1iTpTRy6PX2GvKdVPg1kDtXR69xl7AVai9KFeB9kyv+/loXXy0R+g/Mttl6s+xtb+0j06hP081tLepi3W1wy0AGX16/lrL/X3vU3ZjdrzHWN3Vb8d6Q2R2V0f0iTF298VDyme+PD7Lbt9xf+9P6ahp9+fZDnskF8sHqn8Zd5u8pxlHVZpYETKZkEsRtGlXbeIsnh+9xl5pDzMbieDUovfwNTZ7OREhe5FpECFb+6a8r+J+wcuj19jr8/aQuRjIDq0ZfxbYTSff7Y5bbPLp77Ob3aEtmvb3kQEoNfDSPj09DPWF42ZQjnS6A4unJ6+yZlW/iTZpH1/C7ti4fmBx7hels9tPTbE7bOyiwGF0Hk8fumIxl3b6k2Ze3t52f43r/Ac1uMIhMe96Y4EUOTHjgkRBZIgQqyZqqJAo5a4pi/WqOW7v3pabalu1ZfpXeqpj7dWxoMh/HcvmuztZKVmcLducyy76I74v2tXjjF9xeyq3oBhb5c+7dfktWY8tWp2KXifJJ0ev12u7vb0pdx/2tsz4evDw2StCz115XzcBrDFi7OWlXOgbgLca27jQLYq+r3b20Nhfyi9lIE1hTJKtY1vt7PBrc6pjGXm3m3mSbq+W0XMSb+V42kuGD88Hm+4s39ebzaRZ+uSVveIvxb+/JxqzU+KNKxf67IN3Gk8GLNs5OmzBBYXc180q0FKxiHOhJQSsy30oNRMLOBdaRkAZyJAds19OLd7Fml/Vm02xP5Qf2seySW0DL8+l63PpJSR9Lsu9S4Hs5kB/VIcqmIKIlXWVuFzIjll/uVQyU6AfOBz86bQdJkPH8NkrgkdxbGunMdngy65seS4b+gjg7UIjjh+bYrcKNKAxPbbw3bnwMoLuj/8ORbQxKediy4h4rNblu9NiSbpjvOyKl17xpUQ9PG7CaYrjgp6KLiNmUxZfysOH3WbGb+XK1q7sQm4cTIEZ9d/pTJh4CWQ2GC0iajoYlOHHtb+WbbfSW04GNfLgNRHNRui/lwFPQMZeunKfyykvoK80lnjmGtz67+X3xNc+r4uuP7uiC4g52FyRWWLORa8S47vEx1iX+LiYS7gPGh6bImunXyJibEpfCr/6p6a04CNqUyd4+OpPMOUBYzZTXBK95PymOqootrkmy3kzkVc3oSgqyy5BVFzTHRWV0HzDonw//uU044wbr6Knr/bkGeZeRo1V4bvNm2mOK4mZbaYI2dU2h3HOj/ByV5fnslfI8d3jffH5PMWadI7hs1e4xq5+vau2E2f0jlh8uasLr3DoU4D3G2u8Nofj511b/1GFzoEZ0+TKV7u2/lJNnggTL2vaXcY/UpSzBKX0TjlwWaXTfkIevHZ8WLSBdotsvfSKTYCz3gvN25YAFcRsTRg37391lzU0+c17j13xxVc2ny3FzstLkenkp6kV8/s2tMkT2D6XuNr0xOFPwHTMwU9RpveN9ZaP/XSzGAmnkgnZZ5NfoZuIvkn3gerQzUSX84S2DiyKAvvu+avNupTd7uirJOuu2MoVmyOiN+fq9UPTMy/0+HP3y+NGE7pm/KKzu8GgpqieMElQW+9/Da4cBfS09T5iJ1WSnPr+8B+ztNT3h8kJ84SQofd+qiP99vLgVR47/TP07cTsY6Mvcn1zGWjADWX6WKpoaXFNZqALNJbFREU2G6IJNZjFJMU0HaJn0GiuEUMW3Bz6illx6z15RQNqyrapHh5CQy9o7aVfcGLFrf9W8z0W6oiN75Ey7rs7TD8Ft+NiHbZgxMbcWCETMw8sImbqEStgat8qVhC1rBb9DdZV+idwZWaaJ+3RbaaPaY+9J69ZNZt65aGhyFfuv8uVCyFYSNIySEAS+RVOSHU6R2j47BW/xMSi4YitqCVD8E5jk7LttlxXwVWQMSF+0WXExI13RuQkzA0SBEX0GSNqYnuNSCkRWVVjWmLzquLFJLRiqCi1HY/L8lvyP/brmBXE3mNXtN+IpjM0Fd1q+i8zTlBPWCcKovaffdbQhWzFhi76Ttc0TqgjvnFGStk39b7ollvf+jvJovVciq+nN5fFi1rRgwai9XglZ0sBpP84jQjpk1f46LY8HIrQSiO09fKpWATmP07xuHXZFlVowQ+LeCo2U0R/kb9L8I1Y5Pceu+K7H+pjMDgM7by8FAkvb/qvMdpLTlMdIKBXbI6I/vc+lO5IyYhvTh69aqK9OjaH4Nme2NxLv2T45emLBcJh+LyrESVdwV0xeeBVSEgv7nRne06GnKeHrvj6069M7cS+rP8So0nNoVEhtTudKhNhcrUNMJSBSff0lSbr8CmtA6Mxh7NGmA3n0A+sRqTPx3ze4lD+vDuUu0MVPq53+KmLQ1n1Sl4pZVfYlYzuY/6W7uSutP0plvP3ox0bn65HSRDjyp3uRFngRwqffzb8aSLOP8NmB6eC1U374/fu5yib9jsNcbYmT8Tgyfn7mrpcjm7X9vRIjj55zURrt9oc1+XfqvW6DIyuocWXp8KP58Khjz94u7Cgj+UmcN5fUM7BFV1CTIfpm9A1uljIU7ElRNit66kSzoWWEPC1KQJRGts/lZlpfrhr502925WriUvFBo9e1TD2xzZ8FiI299KWjDgTcfhigY0O7ha2ZC1d0btz0UXE7Mpv7UwxXdFlxXR5NDPFdEWXFXPayH1fz/Ka017u+/pKz8Fnap6a2MSxmt5Tid0YMfpuXbVTJskzVzTVdXlXH3ereDOn7fNeueDuQlLDxHb+arcpdw8tPl0goMYvuKCc4ttMOV7B5eRQXh0pJo5WJ0vZztAx1TbTRLidA6kyLqUWFLKpv9o7Pt+Wm+J7sqBN/XXVlV6fSi8n7EuxqdZVmyzJK7ecGHfQxDsf80bKcSUjYG+aoHKGlMVF2LMuUlWcC10lg/Y73bS+aX8Nnejcf+S6bu5tU+/f3d/Tcalvrv/IUuZebzb113L8WGf45PXG3bFmQatPjyxp7mOJvXzw1JJGR68fGT42f9L/tikeIq5L7j12TYZKODFpaCYqK6n/ErOyPYHliFzPKMPt5BW2wHg77/raaEHJYp5FSDBVDglJv642Skj4qlogZMY1tWNC+q2x3ke1Ru+x52yN1Exka/RfYmZrHFiOao0Rhh2PDKcSA/Ou2FIiuj0v9ky0+xD1ADK6gu1TwauFxMSmgYjZsSlSULKYZxEyEZuGQubEpgghU7FpIGRWbMJCBgOUy/Fp44Ow3iPXbOG8HFr0/mOspfPY/nJm0XYqz4JUMrGYcdy01cd+DkScIlsyKhUiSVDpj7ETJbmyxaXsYqLWl8H/2/K+GDt4c1xYV96JW1/KLybu4VgeDk/Tk0RptvSTviWFnbZk/lQ3ZfWwsxvwE8Wdarh3NexONSwtsEtT/c1WM1Nfl6y6v1SwtLxf62u+3a5+tu/2u0vE8UhBkrLGK760tI/FtnQ/6Txth2Jb7s/llxb3R12t39df5v6iX+pqvT0VX0zaoWyqYtNt+N1U+7u6aNajhzKPC7xUsjpXEnEmV5rM/sbwSFkxe8JnyPh4yloZW0qdUnTolV9aXLcqOU/WqeRyguwg4U29//63usYXiQU02cKrev/9sZ68gSWxUy8ePvYSAWI78+IhJhcgWcwMHUtLeLdL9ZmuVDl5OtEMIWNnr09JmT4EI1nMhy+ztNRflpdCzjxN+DAxh0glD4pnjIOX/iK/dMdgzvggm1O5a8TQyeRfm2pyNkmeuW55/e/l913xZcrk4KnrjP5Sd+R4yujgqUSjeLVgW6+Pm/Lwf7t/BBU+lVfetPvQ8alLFffH3cqKcpW4v4bFWQmw7nW9Om7tWLhYfx8z0XtoEUu/NfW28rLoQgZPz861WxbemcbETve32fX27iChFXd/nF3zYVXsy7+5wrj6yxPX2fi9fPDumIBG7CPXWflU15u22oftnB6abelb2xSrtvskn8pvo42FPDbX2kPZ/lFsjuVPTb3tpXYRc/S5ufYOp3o+1SFr/aeusNUVf1sd9n7ax9CW99TsX21TbrtvE/R175lr7bj9c4FmS56bbc8u3NYf28a/Noka8x+6wu9Lbzw1dPdyt55bd3V43TTjLnD68/za32337fcPd//lL/oNbHgPzbf006nWcTPnJ+bb+G1TVLupt/EemmtpV9ejobT729x66927HnskVZ/+PLv2L2XTVOvyfdk+1qP+2n/qigj2qdqW9bGdGGYMHpxtcePvWqJWuj/OrbmtHx425Rt6hSUx0X/qil7NDXcD3Zl7YK6Fpm79vaKkevfX+V/KTwkafKDJTKBQzb9V38rRDur057m1H3eH4r58UxxGxT89Mf8NfqzrwAt0f50d+bpOLFT95YHZ6v0pGtU+OTEbr7dYF/tuu9ynx6Zu/fs+6QyLPDfX3vbYbI/N34rDIx8z5T2SZIU/TTfff/jHx3e3P/7j06cPvz5R2i9FUxV3l5ln76HZll7/v9uff/00asP9eW7t1eF9sRqr2/4xqWa6CPBrPegd3Ozf1v/016uMnEcVYJHBmvH/fpUh2ze/OZ0jMWZt8FCSyYw93Vj7R5dJbc/+6d/EearImSMPJRmT2dO7rerdoW2OvXH7pKGX/VLjRmnpkTd+W96XTeMRiZ6C81+f7x17FuJf7iLbdx3x9FpNeag33vJjwObTo1fZ642Pg+b+K4Tzo6y1j94G1oCt03NXWFr1Lg8N/XLB20KjbO1J2ApYe3o0zZ6/bEd3UtGlutPfZ3fuE51vm9Dp/uerF+7q07/894tuu203lP3LC/YD/yF/8erFfVVu1ocXf/nTmX71YlVvt27Gc15vfPGfp8f+sNP/7mH39P+9efHqz5tXMv9Bmvw///PVn+fC9g/2H851PP2LLZiNFcwGBbNeQfbi1Z/ZKyV+ULnpFWSDgqxXkL949Sd7xfUPmWG9gnxQkPcKihev/uTIohgUFL2C8sWrPwUqKAcFZa+gevHqT4kKqkFB1SuoX7z6U6GCelBQ9wqaF6/+1K8k/0Fn/Y9jBgVNr2D+4tWfBlnMBwXzvgN0/pAjk9nQdzLiPJ1LZNh9gP/0HSizHgRdKBv6UNZ3oqxzjYzBwkM/yvqOlHXukUFXyoa+lPWdKetcJBOvpPqBMdkvPPSnrO9QWecmmYQfbOhTWd+pss5VMuhW2dCvsr5jZZ27ZBoWHvpW1neurHOZzEAnGfpX1ncw1vlMlqMPxoYexvoexjqfYTcwzgw9jJEQ1fkMw0EKRKm+hzEbp6CHsaGHsb6Hsc5nGPQwNvQw1vcw1vkME8hJ2NDDWN/DWOczDAYtNvQw1vcw1vkMgx7Ghh7G+h7GOp9h0MPY0MNY38NY5zPMQCcZehjrexjvfIblyDIfehjvexjvfIZDD+NDD+N9D+Odz3DoYXzoYZx0hJ3PcOhhHPSFfQ/jtjfkyEn40MN438N45zMcdol86GG872G88xkOPYwPPYz3PYx3PsMVlD30MN73MN75DNew8NDDeN/DeOczHHaRfOhhvO9hovMZDj1MDD1M9D1MdD4jbpBvi6GHib6Hic5nBPQwMfQw0fcw0fmMYOiDiaGHCTLc6nxG4AEXGHH1PUzYMRf0MDH0MNH3MNH5jIAeJoYeJvoeJjqfEdDDxNDDRN/DROczAsYwMfQw0fcw0fmMgB4mhh4m+h4mO58ROZIthx4m+x4mO5+RcBwmhx4m+x4mO5+R0MPk0MNk38Nk5zMSxjA59DDZ9zDZ+YyEHiaHHibJoL7zGQl7SQnG9X0Pk3ZkDz1MDj1M9j1Mdj4joYfJoYfJvofJzmck9DA59DDZ9zDZ+YyEHiaHHib7HqY6n5HQw9TQw1Tfw1TnMwp6mBp6mOp7mOp8RmWw8NDDVN/DVOczCsYwNfQw1fcw1fmM4mjgqoYepvoepjqfUTCGqaGHKTJ17HxG4ckjmD32PUzZ+SP0MDX0MNX3MNX5jIK9pBp6mOp7mOp8RkEPU0MPU30P053PKOhheuhhuu9huvMZDcdheuhhuu9huvMZDT1MDz1M9z1Mdz6joYfpoYfpvofpzmc0HIfpoYfpvofpzmc0jGF66GG672G68xkN55J66GGaLFB0PqOhh2mwRtH3MG1XKWAM00MP030P053PaOhheuhhuu9hpvMZDT3MDD3M9D3MdD5joIeZoYeZvoeZzmcM7CXN0MNM38NM5zMG9pJm6GGm72Gm8xkDe0kz9DDT9zDT+YyBMcwMPcz0Pcx0PmNgDDNDDzN9DzOdzxjoYWboYYYsg3U+Y6CHGbAS1vcwY9fCoIeZoYeZvoflnc8YONLPhx6W9z0s73wmv0HdTT70sLzvYXnnMzn0sHzoYXnfw/LOZ3LoYfnQw/K+h+Wdz+TQw/Khh+V9D8s7n8mhh+VDD8v7HpZ3PpNDD8uHHpb3PSzvfCaHqxX50MPyvoflnc/k0MPyoYflZLG185ncIN/OwXorXXC1K67Qxdzf+sW9fzuVd6uueNn1Bqy73pCF1xu78noDu0v3R1oBWXy9sauvN7DLdH+kFZAF2Bu7AnuDl2BvwBrsDVmEvbGrsDfQ7dwfaQVkIfbGrsTeQNdzf6QVkMXYG7sae4OXY2/AeuwNWZC9sSuyN3Cg5v5IKyCLsjd2VfYGBjr3R1oBcUS7mp/d4MV/tPo/WP63npjBLjWDBIB44okBYAiAKADFAI4DjIAARAIoCnAsYAQGIBpAcYDjARn2REQEKBJwTCDDnoioAMUCjguMgAFEBigacGxgBA4gOkDxgOMDGQyIGSIEBBFkjhFkOCYCSpARTJDZlf8Mg4IMkIKMoILMrv5nGBZkgBZkBBdklgBAdJcBXpARYJBZBpAxCEYzwAwyAg0yywHwPDkD2CAj3CCzKCBjcK6cAXSQEXaQWRyQMUzGAD7ICD/ILBLIGKZjACFkhCFkFgvg2X4GKEJGMEJmyUDG1Ct588MNF/0KAEnICErIuPNCHNIBTcgITsi480LIQTJAFDKCFDLuuGiOXwGRUeKH3LFRPLgAZCEjaCGztCDjeHAB6EJG8EJmiUHGsScDwpARxJBZapBx7AeAMmQEM2SWHGRcvOLqB60UqQA4IkENmaUHGZevZPaDvqE/I/BEghsySxCyjpOACgBxyAhyyCxFyDiOR4A6ZAQ7ZJYkjGBuAB4yQh4yCxMyjrsEAB8yQh8y4RwRdwkAQGSEQGTCgXrcJQAIkREKkVmwkGH2kgEQkRESkVm4kAk8OAEwIiM0IrOAIcMMJgNAIiNEIrOQIcMcJgNQIiNUIrOgIcMsJgNgIiNkIrOwIRN4cALgREboRCadI+LBCQAUGSEUmYUOGeYyGYAUGaEUmQUPmcCeCEBFRkhFZuEDXhfNAKvICKzILH/ArDcDuCIjvCKT49A1A8QiI8gisxQik7glAWqREWyRWRKBwW0GwEVGyEVmYUSGGVUG4EVG6EVmgUSGOVUGAEZGCEZmoUSGWVUGIEZGKEZmwQTGkhngGBkBGZllExh3ZQBlZIRlZBZPZBJHAoAzMsIzMosoMPXKANHICNLILKXA+CkDUCMjVCNzWAOPSwDXyAjYyBzZwMMSgDYywjYy7XwQR0KANzLCNzLtfHAkfwv4IGEcmcUWmAplgHJkBHNkllxgMJQB0JER0pFZeJFhdpkB2JER2pFp54M4EgPgkRHikWmXPocjMYAeGaEemXYpdDiSAfCREfKRWZiRKRyJAPzICP3ILNDIFI5EAIBkhIBkFmpkCkciAEEyQkEyCzYyzDUzAEIyQkIyCzdwVkkGWEhGYEhm+UaG2WgGeEhGgEhmGUemcEsCTCQjUCSznCNTI9mQwBEJGMks68gwJ80AG8kIHMmMc0QcDgEfyQggyYzL58SeDBhJRiBJZrlHprEnA06SEVCSWfaBmWsGUElGWElm8UemcUsAuCQjvCSzCCTTuCUAZJIRZpJZDJJp3BIANskIN8ksCsk09mSATjLCTjKLQzKNPRngk4zwk8wiEcyuM0BQMoJQMktFMsxyM0BRMoJRstz5IW4JgKRkBKVkufNDHNMBTckITmEWj2SY6zLAUxjhKczxFMx2GeApjPAU5ngK5rsM8BRGeApzPAUzXgZ4CiM8hTmegjkvAzyFEZ7CHE/BrJcBnsIIT2GOpxicOwx4CiM8hTmegpkvAzyFEZ7CHE/B3JcBnsIIT2GOp2D2ywBPYYSnsBBPYYCnMMJTmOMpORzkMsBTGOEpzOIRnI3CAE5hBKcwS0dwQgoDNIURmsIsHME5KQzAFEZgCrNsBKelMMBSGGEpzKIRnB/CAEphBKUwh1IwhGcApTCCUphDKRjEM4BSGEEpzKEUDOMZQCmM7rY4bbfAoQBtuKA7LhxKwVCeoU0Xg10XNhxiMM/gxgvihZaNZBjOM7T5gu6+cDAlx9sJ0AYMugPDwpEMM3qGNmHQXRgWjuCcb4b2YdCNGG4nxg1MJWFoLwbdjOF2Y9yMbH4Bjkg3ZLgdGTfYk9GeDEJTmIUjOAGIAZjCCExh3G39wS0BwBRGYAqzbIThHAEGYAojMIVZNoLTeRhgKYywFGbRCM7pZgClMIJSmCUjOK2bAZLCCElhFowwnOPAAElhhKQw7rwQt2RAUhghKYw7L8QtGZAURkgK484LcacOSAojJIVZMMJucEsGJIURksIsGMG5PgyAFEZAChNuExoengKSwghJYRaMMJwjwQBJYYSkMEdScL4QAySFEZLCLBhhOMmCAZLCCElhjqTghCMGSAojJIU5koITjhggKYyQFGbBCMNpHgyQFEZICrNghOE0DwZICiMkhVkwwnCaBwMkhRGSwhxJwflCDJAURkgKk84TcWsGJIURksKk80TcmgFJYYSkMAtGWIZbMyApjJAUJuV4ogkDKIURlMIsGmE40YQBlsIIS2GWjbCRHakApjACU5hlI4zhtgBgCiMwhVk4whhuC4CmMEJTmIUjjOFZAqApjNAUZuEIG9mhCmgKIzSFWTjCcKYHAzSFEZrClNuciwcXAKcwglOYxSMj+00BTmEEpzBLRxjO9GAApzCCU5jFI4xhTwY8hRGewpRzRDy6AECFEaDCLCBhONODAaLCCFFhlpCwLtMDfUTgiASpMEtIGM70YACpMIJUmCUkDJNRBpAKI0iFaTae8cMAU2GEqTAdyLdhAKowAlWYgyo404MBqMIIVGEOquBcEwagCiNQhTmogtNlGIAqjEAVZhkJ4zgcAKjCCFRh2nkiDgcAqjACVZh2nojDAYAqjEAVZpwn4uYMoAojUIUZ54m4OQOowghUYRaSMI6bM6AqjFAVZiEJE7g5A6rCCFVhFpIwgTsmQFUYoSrMuHMLcHMGVIURqsIsJGECN2dAVRihKsxCEiawJwKqwghVYRaSMIEbE6AqjFAVZiEJw5yfAarCCFVhlpIwofFJCMATCVZhlpIwgT0RYBVGsAqzlISNTFsBVmEEqzBLSdjIvBVgFUawCrOUhEnsiQCrMIJVmKUkTDL8EYEnEqzCcneKBvZEwFUY4SrMYhKG8zUY4CqMcBVmMQmTOCYCrsIIV2EWkzCcsMEAV2GEq3CLSZjExy0ArsIJV+EWkzBpXgnzg7rhpAJwtgbhKtxiEibh0TEccBVOuAq3mIThpBEOuAonXIVbTMJw1ggHXIUTrsItJmE4LZsDrsIJV+EWkzCFD/oAXIUTrsJP+1QglOCAq3DCVbjFJExBV+aAq3DCVbjFJExBV+aAq3DCVbjFJAxnvnDAVTjhKtxiEoZTXzjgKpxwFW45CVNwvsABWOEErPBsfCmRA7DCCVjhmXNEfMYNICuckBXutqnglgDICidkhVtSgp0IgBVOwAo/gRX8/YATEq7CHVeBJ1BxgFU4wSrcUhKmR94f+CDBKpzdjE/bOcAqnGAV7k6ywtN2DrAKJ1iFu9Os8LSdA6zCCVbhpxOtcEMGWIUTrMLdqVZ42s4BVuEEq3B3shWetnOAVTjBKtydboWbIcAqnGAV7rAKnrZzgFU4wSrcYRU8becAq3CCVbjDKnjazgFW4fSsK3fYFZ62c3TcFT3vynEVPG3n6MgreuaV4yp42s7RsVeDc6/4+LSdw6OviCO6TSp42s7R8Vf0/Cu3SQVP2zk6AouegXXapAKn7Rwdg0XPwTptUoEzDY6OwqJnYZ02qcBpO0fHYdHzsBxawdN2jo7EImiFO7SCp+0coBVO0AoX2fi0nQO2wglb4Y6tjJwIBtgKJ2yFO7aCp+0csBVO2AoXYnzazgFb4YStcMdW8LSdA7bCCVvhQo1P2zlgK5ywFS70+LSdA7bCCVvhjq3gaTsHbIUTtsIdW8HTdg7YCidshTu2gqftHLAVTtgKl9n4tJ0DtsIJW+GOreBpOwdshRO2wh1bwdN2DtgKJ2yFO7aCp+0csBVO2AqXcnzazgFb4YStcMdW8LSdA7bCCVvhjq3gaTsHbIUTtsIdW8HTdg7YCidshTu2gqftHLAVTtgKd2wFT9s5YCucsBXu2AqetnPAVjhhK9yxlZFpO2ArnLAV7tjKyLQdsBVO2Ap3bGVk2g7gCidwhTu4MjJtB3CFE7jCHVwZmbYDuMIJXOEOroxM2wFc4QSucAdXRqbtAK5wAle4ygPTdgBXOIEr3MGVkWk7gCucwBXu4MrItB3AFU7gCtcsMG0HcIUTuMI1D0zbAVzhBK5wLQLTdgBXOIEr3LKSkWk7YCucsBVuUQnDSfYcsBVO2Ap3bAUnyXPAVjhhK9yxFZwkzwFb4YStcMdWcJI8B2yFE7bCHVvBSfIcsBVO2Ap3bAUnyXPAVjhhK9yxFZzkzgFb4YStcMdWcJI7B2yFE7bCHVvBSe4csBVO2Ap3bAUnuXPAVjhhK9yxFZzkzgFb4YStcMdWcJI7B2yFE7bCHVsx2BMBW+GErXDHVnCSOwdshRO2wh1bwUnuHLAVTtgKd2wFJ7lzwFY4YSvcsRWc5M4BW+GErXDHVnCSOwdshRO2wh1bwUnuHLAVTtgKd2wlx54I2AonbIU7toJzrDlgK5ywFe72rOC1QIBWOEEr3KEVnKPNAVrhBK1wh1ZwjjYHaIUTtCIcWsE52gKgFUHQinBoBedoC4BWBEErwqEVnKMtAFoRBK0Ih1ZwjrYAaEUQtCIcWsnxcckArQiCVoRDKzhHWwC0IghaEZaU8BvoyQKgFUHQinBoBQ6QBCArgpAVYUEJxznaApAVQciKsKCE4xxtAciKIGRFWFDCcY61AGRFELIiMnd6PvZkQFYEISvCnQAGG7MAYEUQsCLcAWBwriEAWBEErIhs/IRgAbiKIFxFWE7CcY61AGBFELAiMueFuCUCtCIIWhEWlXCcYy0AWxGErYjMuSFuiQCuCAJXRObcELdEAFcEgSvCshKOc6QFgCuCwBVhWQnHOdICwBVB4Ipg7iIH3JIAXBEErgjLSjhOMBYArggCV4RlJRwnGAsAVwSBK8KyEo4TjAWAK4LAFWFhCcf5wQLQFUHoirCwhOP8YAHoiiB0RVhYwnF+sAB0RRC6Iiws4Ti9VwC6IghdERaWcJzeKwBdEYSuCHedCD5HTgC6IghdEe5KEXzxjAB0RRC6Ik7XimBPBHRFELoi3NUiDHsioCuC0BXhrhfBl9AIQFcEoSvCXTGCL6IRgK4IQleEu2YEX0YjAF0RhK4Id9UIw54I6IogdEW460bwpTQC0BVBbxxxV47gi2kEunSE3jpiYQnHl9MIdPEIvXlEBJJjBbp8hN4+YmHJSN+K7h8ZXEAixg8xE/AOEuKI7ggwzIsFuoeEXkTiNq7gw+wEuouEXkbiNq5g0CjQfST0QhJ3BBgGjQLdSUIvJXFHgOFjGQW6l4TAFeGOAMPAWQC4IghcETJwKqIAcEUQuCLcEWA470AAuCIIXBHuCDBMrAWAK4LAFeGOAMOcUAC4IghcEW7jCj5dUwC4IghcEVKN82IB4IogcEVIPX6ooQBwRRC4ImQAOAsAVwSBK8KdAoYvvBIArggCV4RyIRH3bACuCAJXhApkJAoAVwSBK8KyEs5xYwJwRRC4Iiwr4d3PCD4igCuCwBVhWQnnuGsEcEUQuCIsK+Ecd40ArggCV4Ryp9HhrhHAFUHgirCshI/cpgXgiiBwRVhWwvHJjgLAFUHgirCshI/cqgXgiiBwRVhWwvHJjgLAFUHgirCshI/crgXgiiBwRVhWwkdu2AJwRRC4Iiwr4SO3bAG4IghcEZaVcHyyowBwRRC4Iiws4SO3bQG6IghdERaW8JEbtwBdEYSuCAtLOD7ZUQC6IghdERaWcHwyogB0RRC6Iiws4fhkQwHoiiB0RVhYwvHJhgLQFUHoirCwhOOTDQWgK4LQFWFhCccnCwpAVwShK8LCEo5P1hOArghCV4SFJRyfrCcAXRGErgh3HthI3wjoiiB0RbjzwPDp3QLQFUHoinB0BRcHfkjYijDODzV+AeCHhK0I4/wQNyXAVgRhKyJ3fpi/4vkPN4JUANiKIGxFWFTC1c0rkf3ABRkpA7YiCFsR7jiwkR8RsBVB2Ipwx4GN/IiArQjCVoRjK7g48EJCVoQFJWM/IiArgpAVYUEJx4cTCkBWBCErIncHxeJgAtCKIGhF5CE/BGhFELQi8jzkRsAPCVqRlpSMuJEEaEUStCLdaWDYjSRAK5KgFelOA8NuJAFakQStSIdWcPGhF0oCVqTlJBwfECkBWJEErEjLSXiXOTKcs0oAViQBK9KBFQUHuRKAFUnAirxxfggDugRkRRKyIh1ZwZkjEpAVSciKvAn4oQRkRRKyIrOQHwKyIglZkVnIDwFZkYSsyCzkhwCtSIJWZDbuh4CsSEJWZCbGY4EEaEUStCIdWsGHbEqAViRBK9KhlTEFwA8JWpEOrYx4AUArkqAV6dDKSFMCaEUStCLdcWBjXgD8kKAV6Y4DG/ECgFYkQSvS7VvBxYEXErAiHVjBG5ckACuSgBXpwAo+6FQCsCIJWJEs5IcArEgCVqQDKyNeAMCKJGBFOrAyEgsAWJEErEgWWLuRAKxIAlbk6WqVkZ8R+CEBK9JtW8HFgRcSrCIdVsF5cBJgFUmwinRYZaQhAawiCVaRDquM9EkAq0iCVaTDKjgTTwKsIglWkQ6r4Ew8CbCKJFhFOqwy0qkBrCIJVpGnm9uxIwOsIglWkafb27EjA6wiCVaRoZtVJMAqkmAV6W5WGXFkgFUkwSrSbVqBxQFUkQSqSAdVRoIJgCqSQBVpGQnH6ZQSQBVJoIq0kGTMkQFVkYSqSCECXgCoiiRURQoZ8AJAVSShKvJEVbAXAKoiCVWRJ6oy8jMCPyRURbotK7g48ELCVKTIQ14AvJBe9i5dPMQXkKP73umF7zKwki3Rne/00nfJAl6A7n2nF79LHvACdPc7vfz9xFSwF6D73wcXwMuAF8A74Ikfug0ruDjwQnoNvLtWZcQL0E3w9Cp4C0g4TmyW6DZ4eh386V4VuOtIohvhCVGRKrBqIwFRkYSoSBVYtZGAqEhCVKQKrNpIQFQkISpSBVZtJCAqkhAVqUZXbSTgKZLwFKkCqzYS8BRJeIp0PAUnl0vAUyThKVKF/BDwFEl4inQ8ZcSNAE+RhKdIFZqlAJ4iCU+ROjRLATxFEp4i9fgsBdAUSWiKdDQF5+dLQFMkoSnS0RScny8BTZGEpkgd6pUBTZGEpkgd6pUBTZGEpkgd6pUBTZGEpkgd6pUBTZGEpkg93isDliIJS5E61CsDliIJS5GOpeA9EhKwFElYinQsBe+RkIClSMJSpGMpBs8RAEuRhKVIx1LwHgkJWIokLEU6loL3SEjAUiRhKdLtVBkZmQCWIglLkSbkh4ClSMJSpAn5IaApktAUacb9ELAUSViKNCE/BCxFEpYiHUvB20wkYCmSsBSZh2YpgKVIwlJkHhodApYiCUuROQ/M1wFLkYSlyDw0OgQ0RRKaIvPQ6BDQFEloiszHR4eApUjCUqRjKXirjwQsRRKWIh1LwVt9JGApkrAU6VgK3uojAUuRhKWom8DoUAGWoghLUTeB0aECLEURlqJuAqNDBViKIixF3QRGhwrQFEVoiroZHR0qwFIUYSnqJjA6VIClKMJSlGMpeLuVAixFEZaiHEvB260UYCmKsBTlWMqYFwz9UBGWohxLGfOCoR8qwlKUu1dlxAsAS1GEpSjHUka8ALAURViKcud/4eLACwlJURkPeAFgKYqwFOVYCt6ypgBLUYSlKMdS8BBfAZaiCEtRWWDtUAGWoghLUVlg7VABlqIIS1FZYO1QAZaiCEtRWWDtUAGWoghLUWx07VABkqIISVEs0CsrwFIUYSnKsRS871ABlqIIS1Es0CsrwFIUYSnKsRS8cVEBlqIIS1EskGmjAEtRhKUoFsi0UYClKMJSFBvNtFGApChCUhQLZDgoQFIUISnKbVHBWzcVYCmKsBTlWEoOj1VQgKUowlKUYyl466YCLEURlqLcLfX48kIFWIoiLEU5loL3firAUhRhKcqxFLz3UwGWoghLUTzA9BRgKYqwFMUDTE8BlqIIS1E8wPQUYCmKsBTFA0xPAZaiCEtRfJTpKUBSFCEpym1QGWkJgKUowlKURSMC779VgKUowlJUaIOKAixFEZaiTpfUww0mCrAURViKcjtU8AYTBViKIixFicBBdAqwFEVYihKBg+gUYCmKsBRl0YjAm5AVYCmKsBRl4YjAm5AVoCmK0BRl4Qg86EYBlqIIS1FufwreWqEAS1GEpSi3PwVvrVCApSjCUpRFIwLvolaApSjCUpRFIwLvolaApSjCUpRFIwLvQlaApSjCUpRFIwLvQlaApSjCUpSFIwLvQlaApihCU5R0foj7BEBTFKEpSjo/xH0CoCmK0BRl4YjAu5AVoCmK0BRl4YjAu5AVoCmK0BTlrqkfiUeApihCU5SFIwJvY1aApihCU5S7ph5vY1aApihCU5TFIwJvY1aApyjCU5TFIwJvY1aApyjCU5TFIwJvY1aApyjCU5TFIwJvY1aApyjCU5TFIwJvY1aApyjCU5Qaj4iApihCU5R2fohbAqApitAUZfGIwLugFeApivAU5XjKyEQD8BRFeIqyeETgbdQK8BRFeIqyeETgbdQK8BRFeIrSoZUbwFMU4SnK4hGB92ErwFMU4SkqxFMU4CmK8BSlQys3gKgoQlSUBSQC7wRXgKgoQlSUBSQC7wRXgKgoQlSUcZ6IJzuAqChCVNTpsnqYNKUAUVGEqCjjPBE3ZkBUFCEqyjhPxK0REBVFiIqygETgneAKEBVFiIqygETgneAKEBVFiIqygETgnZsKEBVFiIqyiETgnZsKMBVFmIqyiERw7ImAqSjCVJRFJIJjTwRMRRGmoiwiEXjjpQJMRRGmotz+FLh1VAGkoghSUZaQCLxxUwGkoghSUZaQCLxxUwGkoghSUblzRDzvB0hFEaSiLCMReOOmAlBFEaiiLCMReOOmAlBFEaiiLCMRAh4xqwBUUQSqKMtIBN64qQBUUQSqaMtIBN64qQFU0QSq6JvA+e0aQBVNoIq2jETgnZ8aQBVNoIq2jGTkI2oAVTSBKtpSEoG3jmqAVTTBKtpSEoG3jmqAVTTBKtphFbyRWwOsoglW0Td6HHNrgFU0wSraYRW8E1wDrKIJVtE3zhNhc9YAq2iCVXTmPBF2TBpgFU2wiraURODdrxpgFU2wis4CCzgagBVNwIq2nETg7bMagBVNwIq2nETg7bMagBVNwIrOAgs4GoAVTcCKtpxE4P23GoAVTcCKdler4PUPDcCKJmBFW04i8AZeDcCKJmBFW04i8AZeDcCKJmBFW1Ii8AZeDdCKJmhFM+eJOB4AtKIJWtGWlIx0bRqgFU3QimbOE3FrBGhFE7SiHVoZiUgArWiCVrQlJULi5gzQiiZoRbttKiMRCaAVTdCKtqxEKBwPAFzRBK5oy0oE3sCqAVzRBK5oy0oE3sCqAVzRBK5oy0oE3jypAVzRBK5oy0qEwo0JwBVN4Iq2rEQo3JgAXNEErmjLSgTePKkBXNEErmh3bb3CjQnAFU3girasROCNexrAFU3girasROBNYxrAFU3ginZwJYdnFmkAVzSBK9rBlRwu7GsAVzSBK9rSEqHh0eEa4BVN8Iq2tETgLVMa4BVN8Ip2eEXDQ480wCua4BVtaYnAG5Y0wCua4BVtaYnAe100wCua4BVtaYnA2yQ0wCua4BVtaYnAGfYa4BVN8Iq2tETgo8M1wCua4BXt8ArO7tYAr2iCV7TDKzg1WQO8ogle0ZaXCJyarAFg0QSwaMtLBE6L1QCwaAJYtOUlAqfFagBYNAEs2m1WGemZAGDRBLBoB1gMdmUAWDQBLNoBFgNvU9AAsGgCWLQDLAaeYaYBYNEEsGgHWAxcCdMAsGgCWLQDLDghUQPAoglg0Q6w4IREDQCLJoBFO8CCExI1ACyaABbtAMvIRwSARRPAolUW+BkBYNEEsGh3u8pIeeCIhK9ox1dGfkXAVzThK9rxFZwPqAFf0YSvaHe5Cr6dRQO+oglf0Y6vjPRsgK9owle04yt5BqfOgK9owle04ys4G04DvqIJX9EWmIgcT5kAYdGEsGh3ucoN/gaAsGhCWLQjLCPfABAWTQiLtsBE4EwsDQiLJoRFu8tV8I1VGhAWTQiL1oF99RoQFk0Ii7bAROBMKA0IiyaERbvbVfB9URoQFk0Ii9bOE/HwABAWTQiLdoQF52JpQFg0ISzaERaci6UBYdGEsGhHWHAqlQaERRPCoh1hwalUGhAWTQiLtsBE4jQeDQiLJoRFW2AicQaKBoRFE8KiHWEZac6AsGhCWLQjLCOtERAWTQiLtsBE3uCxNiAsmhAWbYGJvIH3RWlAWDQhLNoCE4lTSDQgLJoQFm0CJ95oQFg0ISzaEZaRXwEQFk0Ii85DMREQFk0Ii85Z4FcAiEUTxKJzHvgVAGLRBLFoS0wkzsPRALFogli0JSYS5+FogFg0QSzaIZaxjwg8kSAWnTtPxBEJIBZNEIsOnQGmAWLRBLFoh1gw89UAsWiCWIy7XgVfB2kAYjEEsRhLTCTORTIAsRiCWIy7XmVMwdATDUEsxp0BhgcYBiAWQxCLuXGeCMO6AYjFEMRiHGLBwzwDEIshiMVYYiLxleMGIBZDEIu50eMBxQDEYghiMTdmvC0YgFgMQSzGEpORVSADEIshiMVkgXGiAYjFEMRiLDGROKXMAMRiCGIxlpiM/QoAsRiCWIwlJhKnlBmAWAxBLMYhlhFHAojFEMRiskDvbABiMQSxmMx5Ipz9G4BYDEEsxhITiZPaDEAshiAWY4mJxEltBiAWQxCLscRE4qQ2AxCLIYjFWGIicVKbAYjFEMRimPNEHBMBYjEEsRh3f73GnggQiyGIxTDniTikAcRiCGIxlphInNZmAGIxBLEYS0wkzkozALEYgliMJSaSwRGKAYjFEMRiLDGROKnMAMRiCGIxlpiMKgCeSBCLYYH7BAxALIYgFmOJicRJZQYgFkMQi7HEROKkMgMQiyGIxTjEglfGDUAshiAWY4mJxPeTGIBYDEEshjtPxK0RIBZDEIvhzhNxawSIxRDEYhxiwXlxBiAWQxCL4c4TcWsEiMUQxGIsMZE4rc0AxGIIYjGWmEic1mYAYjEEsRhLTCROazMAsRiCWIwlJhKntRmAWAxBLMYSE4nT2gxALIYgFmOJicRpbQYgFkMQi7HEROK0NgMQiyGIxZxOA4Nr8wYgFkMQi7HEROK8NgMQiyGIxVhiInFemwGIxRDEYoTzROzKALEYgliMJSYS57UZgFgMQSzGEhOJ89oMQCyGIBZjiYnEFxIYgFgMQSzGEhMp4NTXAMRiCGIxlphInNdmAGIxBLEYS0wkTkszALEYgliMJSYSZ5UZgFgMQSzGEhOJs8oMQCyGIBbjLrDH900bgFgMQSzGEhOJc7oMQCyGIBYjnSdiVwaIxRDEYiwxkTinywDEYghiMe6OlZGpL0AshiAWY5GJxDldBjAWQxiLschE4pwuAxiLIYzFWGQicUqWAYzFEMZiLDKROKPKAMZiCGMxFplInFFlAGMxhLEYi0wkzqgygLEYwliMRSYSZ1QZwFgMYSzGIhOJE6IMYCyGMBajA/tLDWAshjAW4+5YwSuaBjAWQxiLOe1igXt8DWAshjAWo/n4Hl8DGIshjMVoMb4FxADGYghjMRaZjBQHfkgIi9HOD3E4AYTFEMJidOBsOgMIiyGExYT2sBhAWAwhLMYCE4mT0gwgLIYQFmOBicRJaQYQFkMIizGBU3AMICyGEBZjAqfgGEBYDCEsxgROwTGAsBhCWIwZPQXHAL5iCF8xFpdInJZnAF8xhK8Yx1dwWp4BfMUQvmIcX8FpeQbwFUP4inF8BaflGcBXDOErxjg/xBEZ8BVD+IrJnR/iiAz4iiF8xVhcInFangF8xRC+YhxfwWl5BvAVQ/iKcXwFn+VuAF8xhK8Yx1dwUpwBfMUQvmIcX8GneBvAVwzhK8biEomT4gzgK4bwFeP4Ck6KM4CvGMJXTGgLiwF8xRC+YiwukTirzgC+YghfyS0ukTirLgd8JSd8JXd8BWfV5YCv5ISv5BaXSJxVlwO+khO+kt84T4SunAO+khO+kju+grPqcsBXcsJXcotLJM6qywFfyQlfyR1fwVl1OeArOeErucUl0kBXzgFfyQlfyS0ukfiwyRzwlZzwldziEokPm8wBX8kJX8ktLpH4pMQc8JWc8JXc8RWcFJcDvpITvpI7voKT4nLAV3LCV/LxW1ZyQFdyQlfyzPkhdmRAV3JCV3ILSyROKMsBXckJXckdXcEHzOWAruSEruSOruB0rhzQlZzQldzCkpG9WDmgKzmhK7mjK/hgrRzQlZzQldzRFZyNlQO6khO6kju6gnOhckBXckJXcgtLJE5lygFdyQldyR1dwZlIOaArOaEruaMrOBMpB3QlJ3Qld3QFZyLlgK7khK7kFpYonImUA7qSE7qSW1iicCZSDuhKTuhKbmGJwmfh5ICu5ISu5BaWKHyUTA7oSk7oSm5hicJ5QDmgKzmhK7mFJQqnsOSAruSEruQWliicwpIDupITupJbWKJwBkoO6EpO6EpuYYnC6Rs5oCs5oSu5hSUKJ0/kgK7khK7kFpYofJRMDuhKTuhKbmGJwtw/B3QlJ3Qlt7BEYWyfA7qSE7qSW1iiMDTPAV3JCV3J3QaWGzRMzQFcyQlcyS0rURi65wCu5ASu5JaVKAzdcwBXcgJXcnd/PYbuOYArOYEruWUlCkP3HMCVnMCV3LIShaF7DuBKTuBKLpwjYk8GcCUncCUXgWPqcgBXcgJXcstKFIbuOYArOYEruWUlCkP3HMCVnMCV3LIShY+CyQFcyQlcyS0rURi65wCu5ASu5JaVKIy8cwBXcgJXcstKFEbeOYArOYEruWUlChPrHMCVnMCV3LIShYl1DuBKTuBKblmJwsQ6B3AlJ3Alt6wEHo2VA7SSE7SSS+eHuCUAtJITtJJbUjJiH3ghASu55SQK4+4cgJWcgJXcchKFcXcOwEpOwEpuOYnCuDsHYCUnYCW3nERh3J0DsJITsJJbTqIw7s4BWMkJWMkdWMGUMgdgJSdgJbecRGFengOwcv63/3z1otp9KZu2XP+8W5ffXvzlzz9f3Ba774dyU67a4m5Tvnj13y9uK/dHdqNeWYsv/vLfLxjXL/7y3//zP6/Olrr/9epiw/6tM3q7Kjar46Zoy8/l915t+VNl/Ca6rk35pfsnryLOvYqEK5pJFV/jtmwf63WvSuZVySNrKjebXfFlW6/7H0163yxW1GO5+uxXIr1XlCKykqYs2nJXr8tqVe/82jL/d8xuIn/Hsmnqptrd172qROZVJSLf775sV49fq/ax3rdVvTv0ajSeX7CbPLLGatOWTXFs6/Lbvtity3VfpfZUShZZZ91si76n3Xi/g8ginbY6bOpiXe0eepJ47n+4yN/0c/l9W+z7kqQvScbV0zWijV9NtzH+Uo3mkXLq+8P+W6+azPOs7gjCqGr2RdN2n6j/k3H/+5j4mg79N8sE8yuKdPZ9fag61yw2/Y+t/I8d+XpN+VAd2rLZkcjAb7wgk8XWtVuXTe/9cu+XYywyWDXlv47loa3IN7/xP1WkLzX113JTbvsV+c4dG7Ca+mu1/tavx/j1RL7boWyLVVt9KekHN+KpNhP5doeyva9Xx8OgLu+rm0jvbJuy7H9x7nc0QkYGu+Nu1KW8aMwjw9xxvy7aclVvjtvd12rdPvbD8Y3w+4rIr3Zsq17DYZnvo3F1fCk2x3IQ7bTfACMb85eyOVT9LpD7ryWyyNb3taweHtt+wBN+wIv0dPuZaeSUfkVx39m6edH2xxt+X8fyG1eUu07v1Ytuw2t01Z1XkCbJvPdl8lSnivtJXZ1kGCj8VqmlOFcZ95Pghu73QpFOUqyLfVdV+9jUbdsf93bz/Et9eWRfVKzXq8dqs27KvuN50twIuhusxmq8VAkGTt3xPF4XHvn91uvBx5N+LyBj3/a/jof2cdA+GPMiOHMDglcvOIus9b7td3bdruand9SRn22z6fx4d18f7r6T+Jt78lTkSL/Y9R3Y/0m78zGj6tjvN71autTApw4ltkF1tXSPFLv+a3khgJuzk8X1Ln6d0MuM72WRccqrs/2+L/ufz+sCuyNWoyvcVjs72AeDWd9LYn/Vw6Fser4r/ZlIRxijqjm29arebIr9oR+WmR+WlTw3hMj3vUxseu+p/feMHK89VbXt/bBdHpI3ookb23aVfS6/9ypi/nyc6ez0ppE/w12xfugPaYznH93VktG1rDbF4dCvyp9Gm7g2Zqtq63rTVv2hiN9vdRdORlVW3tdN3/2V9H01UpOtBvf+frzNzx8/cujs6i3XVb8d+B1ClxmdUNXAZ5k/NWA5OwuMa12uVrc61V9kufFr5eda45rEXb3uxWJ+49UmbuKawl1db1y8bMpDvfnS77h45q8cRM7SbJXNoW2q3UOoan8oyyI/ZFMW61Vz3N71g5S/vpdY0brcVNuKdtjK+5Q6MtrdHdu23q02VX8RjPldJMvF+UeO+326hb67giyr+Z/ORI4UV8Wu3lWrYmML+u/KvN+4O/c+rrZDWe0O5a5bb/jSq6/bivekLjJ+rop29dgbvPrhuFu7jqql3PR7VOn3NCwuBKwei90DWQv1HSw/d4LyNA7Wsd/MVjwcSPgzqDxWY7n6fFf3Vx38757x0wyK6XOsOq8xC3H6F3X+j+4k3xSrrlH3F8X9xew8bshmq7P/rySrC16sFSrSvbt6jjtUnb88JFTcEP5U3edd/bU/D/IXjoWKfdFq05fkD9Fk9OcaTsv8wY86MwSWn37o7PKLRzafbuRR9tdR/DF5JrJzheLsQqc+S0VOfFebsuh3A8Z3nPg63Mp5T6q/FJFFrnLaurZF8/nYGyV1R8I/BY6byEY5jPz+kDI/zyIj56ar7b4fVv2gH0meVvVmXd73o70fbkx0NXStVPgzZBU5jlzVm+Hvxv3oKuIVndjJivY/3P/qInLkbVeM+u+n/fc7Rcpu43V0df2FMj86K3NqSDq27Z+mZnX7WDb9ia323laLyEB+qu5Q3W2q3UO/xfvDlSxy2LyqN9tyd+z/DP74LnLWsqo3h7oZEFzuj75F5PrJua7Dqi/L7w10ZFN0Va1LWpffGejon/KwL/pg029LGWfnICvPQTZucNzV3DbHVXskczU/hqs8uqket7v+YMp72+7s/vhq1uV9tavooKG7ptDrvaJ/il6Fm+pAVrb9l40dPNhKLaGnpLi7OcyrL0XkpT5CA4SPFVTkwrarst9Se8T51Llk+tz5J9V7CqY0c8JfI2T6NK7gIjqmdlXTwMB8d2TmLDw+dHWVNuWh+vdQrr+KcI6xPHb042pG8Yf5I1J27gx4dLscrnTmPn+JnPx2fyXDbu2Ds9hpSL3blau2rfdjs2n/ZdXJnziLDW87F4TqXifvY8fzMPU0dMzP86g8cql3VR/7mTR+YIrMkrB1bLtJJxnv+gseWeTiqa3suKv+dSQTYU9WpA8eD229RZzNR/gqcoFoXbRFn0v7hOT0M1xmg3ncl+sqbZtid7jvD+O6TbneQmzcuGF9twGjZp/V5Wf/i3SONV5flH46U35yQR45ulmXd/Vxt+r/uH6ihzljyjySgK7Lu+PDAAAwfyWGqXNkjCTP63JVb/dNeTgc6mOzKumvz7T3DXjsD1SW/b5Le06tz/NBE7kAvC7vi+OmvR15fZ8L8MiB+7nKbbW7XdUbx6h7tfoL8TwS1Jxrbeqvt4AO6h6oSnr3LqltW+z76MH4Lx6J4tblfdk0/aUO5YfuPHKeuS73TbkqusEUAGfcX1wV0b/zvv8jCL+j6i7HiKvl0DZ1f4nbnwfE1tIW1abPjPwxoolcFlxXh1XR9KGAv7zI8vPwKHIpZE0Jgx/2zDnsiciwd6GedH2x2+nvxeXI+FT3Jzo+nhGR44zzP3SDjD4zzv28hMjcv15t+6beVn1Q2e159CqNlNgUvezGbg/yUyU3cX12V0nZ/y27rchePXGjCFcPWRzp9iR7NUV+q66mbvIx8AXp/Y4mcvXS1jYAYtKHknlkMm5X1aYsCC8w0q8pXlT9hX4r5lcU7wKHtiAIXRu/psig19T9BTuftHa7rWMrKe/vuyz2zab+Wg7yDqQ/OjKxw41LradeqB8gfF5jYr//ucahQH9gFJlaZqvDHuu1SBMbeZp636Ua1rthddIXN6c6uuDQHcvkVRn/Mz9VeShJjf4PErmEsf6+K7bV6gnn9jtxP9s6MvvFr7FHc/s1+5mNkXsrTjWP4yTuL4gJHvlNXa3dAAvV6I8cIkHDqcbxd/fTqiNXhk91njIzQKV+erWIHOOURR+edvv3vc4w7leh+RPdsXHegOQ8Z5KnaYm5ufzHeXE8cuDTGRobrPhZ792xiFH1+fGyH9n81h6Z7jwEHd4PonSkpE1p/7c/+fbTE89LU5GDn666+6bePrbb3oxJ+RkfeeQ2nXNlLh2lv0zTbbf3Kox0nO2+/W7pZBcb3VBhsILW3090XkGLXPkqd119/eEy94fL+jxcjmzZrkKXQt5f/vHhRyQWtzuM+vMCH2renH9sdVrwEur0L1Ke/8OcU5JvzmthWaS3HlbFvqR+IX0c352PEV9VUz6U3/p1+cOYyMmSqwukn3Wnani1RbamwdY5v88V59AkzpkZ4rzAI88EXGYn+CrZ+V/OaxdS5Odf4/Tt5TnNUZ4XP+V56Vua83+cc9TUzZnSZGcUzs5/YicZSpxCpdLn31mfE8bPUrsDE0//Ednquo9yzg6icVT53XIeuVzoBmC16+76UcHvTiKToEEOnextnTzHAHXJnY+U6QIMyfTxu+Jzfnpk1tClQpg37GnWkX083Mvn73HJ+NlTI7cenmukSavCnztpednYENlKXa39zDd/1VpHLoCfKzpzZZLO4zfWpB+5bNCn5D6VEpEzjHOFm+Lf/RUlnxmJyBmBq4xW1d3G6P0Ysc2kJYsHyt+tmUdil/Jb26VCdB1BW37rTyf8zQd55GLyME2D+U2MneMgjxyjYb4o/H1RKjIjcizvQ9z4pDZyb4qr7K4pdkMS5NOSyIGaq+5M2Ojg1vdbEwkPXY2D3dk+KEitaJCB6M+NlEmprtuEQ76av201kkF7daEQ7HMLnfTZRmcZPqqSccHyvhp0Ov5SzDn2ZpHxqKvu7ntT3pN9Zf4CRXSD2K3XVVOu2kGiYeZLzCJjb1fffdWQZQ7Pd+Wlg40XSF/Ta/iRkbKrZVd+a+nGL3+wHQkJu6qasjvcYbCNzB+jnAlcFrkGc199K9erug+1/PReps+z6MiJ/X31zU3TqnrH7bayIS71YyiLXCvp1dtNBsvdetCWmR9QWeTM6n7TX1LnPiuT8jx8VuehcWxg7TYz98T5Gfr85rx1ItbF0d5onwpFTr9sPd3+6J4L+b+IziN9p8vU7OdF+usvIjJNyFUzGOT4m2uFjv1GXVX1nmwE7Y+0I4Nd3fTpeXcDmTeszs/j1tMszZwnVSZyADo888LPRjgvN2SRxMPVdks3hfged57CZ5ELvvfHnW1sw4maPziL3E50f/w3GXn6vEJHHhbxcD5vAGT3+ntyI5vUw9jxBaq3Qzy2rtWm3g3W2jPZ2yEdN/TsKhu+YeZ3tJmOC8cPZQuTBP1dJrEV2R520GH7LOa8+pDpuDH7w8iZD/7W18gW8FC2mwLJ6yVFn+XFhaauTprykfnjgCwymnQV1V/LQ/tlPzhIwk9xifa1blRxSoXui/PTciOjZlcdHVJ43yy2jiEY6O1iOUUfxk4BM48cTzyU7b5oyGpa5o95skjGdqlp0Eb9MJlFLmbZ2vopI5kfwbPIRO2unqb8gn5MPw0ii8zVfihbN0Lvp494L3hewWWRUO2hbN3AqxyOt/wk9zNlYZFnGnX1tmQx21+OiMS652rQNMwH/joyn/Gh7PJMtzVsq14fE//16GDLn4LF1vGlO6LFjnpJpyD9Q4nyyK2kD001mnvjJ3blkQdvPRzLw+GJr/cnYT5rjtwr+Fgc0NaxzN87lkWmA3Z10e3pmT9YyCLT9h6Lw2BIL3qZZqfOJTK77rE4PJYFXcv0NwLFtaNhJcxfuGXqvO7Fzzt4biJf2FYMdtgJf8VERZ6g4WpDlNEPmpF1VevyvDpKFl78te/I/RmP1cPjZnDckJ8PpSNT2h4rAn38DXAiMi5SMib8/G4VCQgGBxH6C2YZP7kq0+dJ6NPq/mkEp85beXTkdLezCFJZ/WE+j9xz11U1OJ2C+eMufib5PNL7TjmsvQp75zecPsiZ7PBIMo1yY/0mnFIJDcXc308uIgEJziVhPsMUke2/q4ru++J+s5CRw+iuIgBWM39xOTsDR3bmjJyrs09eoGTkj9KbAPiOf9lypM41nn54FTnLoFsk/Q1NKnJKVm235boiIx/pJ1uYm/OqQmRArHarzXFdPlbrdb/LlH5XYCIXxE61HcrNfb8uP9MlcvhO553c73rledFEZmf8fe6kZH5h0+f/iJxTdrvs+stu/o6jm7N3Rc7ru9q+HruO5q44bsEgk/m4lUcu/lS7/bG9HZznY3o7yyMbV1cVHRVyfwVDnr1JnvdpyvNYxUQeZWSNfCnIhmPuj+3kzeUnTKuzvwOJ+8sJMjJJtzvW4/8v7VqWG8eR4L/MeQ9N8Kn9lY0JBWXBMsYyqSYos32Yf98AqWJnAdBsOvYwEX2YrqZIPKqyMrOmtYhN2ZO4ZtnLM3AKAolQo3NItGZPIp82/Qr8NgV7Jfp+eLF+HqfxVQfDM5RshDnfT1Ov4LgGXRoPpFTf+YcTszpuMRC5ErZsP/5lCtumIwUQTsdBSIR023I+p3nGnV6I3KEhBSbOB3VT6tRSoTFeSxY1zgdhtx3O/TBHrw1h6oIkEDofCI4R/KDwbjJjkDizi4wdlZhGsG2yGnR+peuNp7+i2rJBR+YD6cPofIaap8in7I7M0wYL/KkFCWJIsIiAg8q1ghRZOf+MbVAos06SYhvCTT6LNaKNQ0FKMZ2XtoL+kAg60BsqwBjqKyILhT0KA2ic/XUIEdBrIm5jFcgEKPiHGvtkLeDHow/WjB+6kkMKZnFgz8WPXsmsGqOOe/bLbTJmTbVD1wJDtpWcH+yi0gdkDjXiKNWSWKzzt/7ihkvaZsW7iGyPhmABbY7PZ7yODvxzZYzbEewoSFuWLVTs3I5VnSG9g5y/XXs35I5lhCRJNzXnp/7s9MQDFJIZtmj1m3F7tHtQZ2VIdZTz0zgmrZECQWVDCu5CqCVOlPDyYrefIPH6iZBXTkr3nA+oeYZRgPRMQ6r3nJ/HW9opQyjZkOI25+9D5vRDE3jDVgX+04WTPfp9eMyQErfQVu8j3UGB51VRCIBEEpffrb397j1nnrPC+6cl/UAixlKBh3NhxGZFMI+OJGq+26/jMCbafPwkJXn9h1DzeHzZ7EWOMSKHgtiS3BOPkEP/6S6rIjyNirQb8uZ8t1/nyEfPYDuuFOSiJIlW7/Zr6D+fNl3wxCQ98kPi8vPuXt697aeXt8CU1MgrrHNumccRnXbdNMpZh4wYpUMoFSt2e0eSzxiixZegwZ5QKUhHSXJcVmWvH4drxIrGaoV9OOfngHP2bsi3OTCX57ZzcuH/wAtfwEyzo5qixClJMHM9Z2MlUIWt9dZw2yWTZpbYLK2Ez9TKF2pJDnCInKwi3NFGlGktKeWUiE/Juxi9Jbmx11Hn2MrhTr4Td0Zex8tqNqLiqckNEo9dRRfxy9C9dgScySNnvCTrRVGdG3k0+q1dnlOe8aY+8C8vRpyVdXsrD8iu60t8CmIpQJYVjyiRaEIR4NgNe1n6Sd1KWAN0+4nAXZ0f/fuafqxozVaS6T4yloolyYtRQVebgmxkPLXJ1xgi5zI6zBoMCWtArOwRgDUyeZd8xH7QBdqjFGQLeY2yiTt0OojLjkSfHy8+58dd4lVSkXLLNd4WAB8Nb+CWhI4foV7HhM+CpLGOfmm/jlHrvUG9yYGU/Hz0v652uOgWdo2k5o4EJmIHwQpZDg3Zy/2w3veXqE2Ighz2uz3Gd5wz9l1YOgpDuCRpRB9uyL0uNfuLjpRQB6ofitFLBorK2QoL/5ZdBmO4gIMzvm5/IpOJ3YLj2b1+JaRUg13QUjpnJdlA/hjv3h63J/R60SOOTSauH+OnnTXwgsefIXUFH8EpbLNx0/wvXA6kl9nHffq4T4EgVeqfh1J/8mwY+lBlj9PZTrdpvK1/HZ8P66SOpOg8is3oGkJciNyZEih7B2HOR/a+f1fBYQ1neCBYfxmhQ5fkof07ejoRAI83ssn2GOFqh7t2r0ABWEuuvsEuaSsZk4ya/bT215zb+0hMJdUKOeVXiV21mjxnh7Ef3Eef0KtxxbVC82u7Xae/u52Qz5tgPCWS0ysxg2jlvGqFy92SwH/KfccsRJ63Elu6+of8QSyCaxmEVws5qBZf/1oMLWvRwNVSedUy/KYR2ljzYxdzCRNIGHCNCN5aEQx3MlmpIxPV8DOPYYscU4mUQfi4JPuzIWA/fGUTORwhUJHleoiXUEaQSNyQzdQQ6CEgzjxbhRBAQ7ZCf4ecrL+Ng48OGiRDfuP1rT4vGVRGzb6lo4WbxE7zV2B3P0mwUZpXkXBZCL15bKnfjCz7lqRkr+jG2izIPh3epNU3PvbWNEiOf1TYtWQHYgsXjDvy7w9FeyQb83fdOU6xFwBCKCVZb6/xxrN79hJR38LeoQ+S0jN/eRTskablacjYvR0tkhr29nr0JfI/HZsfFQkv7xFzG7tEelVFdiaGcdW2RXkTgrciSGvlDG933yKxw2kPu4RTjG1IdGgYk5q/wiZBS/8MbcPYoIr5wCYu45zxGG3QxOpAFlHjq9fKd7T3aIX41JKkiFHfCajrbcn+4ng9Z7I75VVOPsuQGDM1iIQcSO5iOP2TfKzslKyf2xSZzF8NBRIONJnDrXWOrm8QvSBP+mBTOrmz/bDz26jVlEhTPpB6xY1UoVcBgp+kgeqtn3o9SdMclLky9+1SNSHuWUlyCzKVuk321Wk8DN1EWxKKDRLAXNmB6C4pZwmh7DCH8Y3uMiSSvbpRGjtuQTxihrbJ9vqikPiYZCv4EXIYM4+IRVJHHjOPeJN9uSczyGvk5HUkC+sR0PcfNl0yNZ4+HdlUlogv0xhZgWHu3ZLJySPa5+jOAcKJ3iA62pKd9BAwqdBwb1Qk8yl3D2EpcCCbaiHL7gM8Erfia0zbO3qL5dAf/IxSznZkQvPzbqevyfrInbhqFNmVjbX33dUJhwpDI7lLSeZwK6UqO18QvyqJOq7BLtOox84ViJ8Vot6oSKR1jflkBCKKn0j+8SNcZgQi9qJJLC0/9rfGPLUjAZzET96oBjYZ48VGhF6DSoJSwJKSfVnbSRlPQsWTkrwDJ3ue+kXf79gVJvl2qbNUgclLId6UFYk2ZSppbHc05Dk72YTgiGjngbQQnmxC59D+ut8Jk8kaayzJOnpRhatDw9jYESK9U7cwWck2shzNt54qHXFZILnXkLaAGx1UbxucXbKzZ+hjIcQ7hpZjyP1sVAAapFWUpGxFYrrhSUzkxZHo+hYzTHUcXkd/+oqkW5g5N2SJuYWM7bIqPKkbkoa8hcq2PfDkIB1Mtmh5QqtBHVG521iT0tUwAn36tOdjMHE4bu2tjG4PJYBkehkiz5kReZ06gPlQKUkKia7iIdyWgnGQRdcae+uhbSKtuMA0SGwzpJ5wjbqGyp1keDt2JE8sO12vQoFQQ7bu10gR6KzsiL4TJsjINJSC5DL++4ZZ9PoKwjqc5BNMKy6qP55y5eRS1Sx8h6hbQ4IVk50nFxhg+g0hlkxWVqEUmqLpOtgp6EgoNRD+1WaEl0wGiC1tGjRlPdDPodMpxAEa8Qxv2RtwXLaBYHo+n1F8XsE9Sjbnuw+Lm99kpFfqS69S7v8jZu+/BiU5Uh6D3F2YqbixAmrE0bwVU8aORO+2wG6Yx0+nhUgGN7oxOwy992TlXyL7OfpfGh+EntRaEy9j0wkdmtzZ0T+SOZqxk9CS2uUtqreDd7P7dHPkqor4JJk9PCLebDQ8A6meB1LGtMXSBBgkDX3rR85j9qUhoYHsWW69O53IoDtAIV700uTuSPvoLXDsRa+GE3wjTuwYr0jugl2TbQeJmDrG441QC+eAFPNvURM3avRTNkKZKNnzZI+ZEmGwgUem/97+vNtoWGdhlC0Sd9J7O7n+GtKOq7udxn46x4yOGtugHYnnefHJ1AccvkES5fLPHDdRdUYWOHus7JbDKoLM2EI+nvEzwDOFfl+p5RgaURQkZrBXCOpVqbzme3GyrwpxA1Jl6e1KpjivGtBoUiK208hkIoTLiPLVYB86UEZIb3AaiCG7shAr+9oU8Z7+DomFHB5u0lo1JJnA2zk2+jLYqzVkv8/bOULeDN5ZhlTkr5WiUART8qFyKqTPi4ytKHJX6CfL3VcGP6IhHY0gVhaGw9yBlOj4nBWmmuv5rTBZK0zsMNGpwrxhKuorYk0kqleyrNkDZl8cXgEk7dPbOShrxnuW+4Cc+wOpDlsjRuYlBoFawy+5jOeZwWcypKw3hAozF9VnULZm9PpYaRRzqrpQX5WEXPzbuPibG4bYfRIPWyl4SvYMcVEiVKKQoyJ5Hv7dXm3UTDeYUZlauM8kxO2v0VjxplCeMeR6vY7LWrOdbXxvIiOiI+dLxHBOgfdcISWmIbuFIVqKoCHy8I3HOn0JR1I/IGa1Mk/LsGm3iptFDvFSZnf8OD3huKl7hjzhQrBcHwThgELm6xnS4jVE3ajEPplyYlAwZTqBcUjIJUR+t0+YxdgkZiuQcZqzHwaTVpINu8aK4eYKz+KGHJ6yR4rLNYPdroo9RhORi0HCSlns/grsOwvx+r/6X+njYV1Oeo1s4V7G29fbqLl3NYoID2yqv4YL9OnEnxQ3c0VaaWzhEr95dGDvyMbsFmrzn0mfDRcv2evYAqahkOvKJibJfG4kwHRsDhGihJpU45+YyrHnUogUzNmS5KZAulpB2umu4WKQGsdTtKQAKs0vK0zvG7bi3lt/8dcrsHdTiO9lRfaCNn66bvYhZcjIdKCS/g7T/WW+TxFUjmcaydby99PKPn4Z75GkAQHyghQBztbPx1d3tcd4qIFBd6GSFHGs4WIja4MgT0nWNPObvkgbzC4P5HKd36ZxjtZ9Uyr/Je6lp7sHL7bC7IiniIlkKEJL6tLXf+AYnCePt/581DU5av1LEvpZA4auStblvEDhRkGStnLeqFg+kKnHJqzQLWssx0nsaAuTGJoWiIMUJD4zj5fL1cqQd/0LUU5O+ihv4VIkH1N9UsWKoWIMAxk2JCM768aNx4bMtzLiNlKWYh8lAHxDL5cnc+MNQqsVSRKbtVSiwiXTkjYm83hzv2y0ghHmJevyeUwmhNSYd7TSf2tJ+HIevY1GguEhRWYN2XnI+JpkRiDpVTZP/eCvfdakpcKBQy1Jh4wHohR4pRZCVank7VViBV5LwVhL96s2ovUUcmAtK7eWSQa1HMa19LjrdleIyv8jyGstzfVGqudGKEON5PWN2TWjoiKVON3ejSPbhuFVrJjdEz0YooAtWe3kSBV4znf0d1/jbBYGGsTCI4fNMDBaaoiALAnDHoqTi7xDUMtPjiKdJ7d+gLT2QvMbEq9Os0+0gCl3AqucpULorkShXAs0UBf7H2RBC4mgljh1JSLoWpTOzS6Clr8lnky1bKf6UMkSF1m0cCAbseFvRGfdlPvKFsJC9/hbBxJWCu8knQKA0klR4tUiz66LfVvv1vXy42UuSV3Lbhbbkrrd9+4+6V62rAjIG2lKd+JC3bGXRozAKv9HKQPkBZYH+cSkcVWGtV7VyoGbe8z74PtX+9L76B7Byp9Ene7DgysQGy4hs6qQD2NkoZW/lzm3aVLuEDqwyeIwMrSmLOQPJDt0i/9gwmpUCjFXUvq3RUuHjRdqFisJE2zBst0QpIGy336NdrND8OJbD1r30qfyQkwYC9KW9T7pUg77BRWZXd9np4PA+SrnoewaOaNquYZr+eq1ke1u9pteTgsZFFNLjlrLPJ+63Q8J+X86+YPofxuZDdJIetFIetGY/fzYTSFk3quQRw5ks28do7F+lsRLr8EFeSAvsDVezKpCy4COzBsChTLKP7BjwsUI+m692nBbiMLakLXDGi6jODSYrhtSD7Jpz8cl2g14jJNA4R4pt3ORAdSRfaE14MOFPvqhaONKEqmW0zGxdUbZUkH2vJbTMTfwBkMZcpEuJxFTraLFBGNFyVxFImDLaVPMrg21fFh0yGlIOH45vYRZK9dw7T0kpUHhl8RGuLMi0/3ltA0AefIS0K+IPFSX0z/8fGQM1qSpwXI62//xrXDoJ/1SA/jc327Xr3xQVOjWJP1lC7r+l4+Jo6bI4nk5rafyk4A4VYmse5ZTdv5yhTVvQ44RXk4PRlL+6XByEpl7LaeVkPQkIC4fEiFdTvv0vnxTERN/+hzaY/7ThkTlTE0O0olDp+8AFzspEVtOoZTPx0MMvCYx3OX0sCN/EhK/OwmrLycoxJ+ExeFeZB9mOYWLJ7URw9Od5AptofKPhqBTRbYkltNDQfvkpsAWDJkUh5DrrIl8RNQekZzD5fSAePMR8cgg3aqWU4CYnrxGxF/ZtZjadOIdS6Ziy6QdimoEnzsSfv09lg9DwXcUnPOb0ZLcCfVoBdno++eZgUr/zbDx/vzXHzd3s1c32D/+/Z8///77v/s6Aas="; ================================================ FILE: docs/api/assets/style.css ================================================ @layer typedoc { :root { /* Light */ --light-color-background: #f2f4f8; --light-color-background-secondary: #eff0f1; --light-color-warning-text: #222; --light-color-background-warning: #e6e600; --light-color-accent: #c5c7c9; --light-color-active-menu-item: var(--light-color-accent); --light-color-text: #222; --light-color-text-aside: #6e6e6e; --light-color-icon-background: var(--light-color-background); --light-color-icon-text: var(--light-color-text); --light-color-comment-tag-text: var(--light-color-text); --light-color-comment-tag: var(--light-color-background); --light-color-link: #1f70c2; --light-color-focus-outline: #3584e4; --light-color-ts-keyword: #056bd6; --light-color-ts-project: #b111c9; --light-color-ts-module: var(--light-color-ts-project); --light-color-ts-namespace: var(--light-color-ts-project); --light-color-ts-enum: #7e6f15; --light-color-ts-enum-member: var(--light-color-ts-enum); --light-color-ts-variable: #4760ec; --light-color-ts-function: #572be7; --light-color-ts-class: #1f70c2; --light-color-ts-interface: #108024; --light-color-ts-constructor: var(--light-color-ts-class); --light-color-ts-property: #9f5f30; --light-color-ts-method: #be3989; --light-color-ts-reference: #ff4d82; --light-color-ts-call-signature: var(--light-color-ts-method); --light-color-ts-index-signature: var(--light-color-ts-property); --light-color-ts-constructor-signature: var( --light-color-ts-constructor ); --light-color-ts-parameter: var(--light-color-ts-variable); /* type literal not included as links will never be generated to it */ --light-color-ts-type-parameter: #a55c0e; --light-color-ts-accessor: #c73c3c; --light-color-ts-get-signature: var(--light-color-ts-accessor); --light-color-ts-set-signature: var(--light-color-ts-accessor); --light-color-ts-type-alias: #d51270; /* reference not included as links will be colored with the kind that it points to */ --light-color-document: #000000; --light-color-alert-note: #0969d9; --light-color-alert-tip: #1a7f37; --light-color-alert-important: #8250df; --light-color-alert-warning: #9a6700; --light-color-alert-caution: #cf222e; --light-external-icon: url("data:image/svg+xml;utf8,"); --light-color-scheme: light; /* Dark */ --dark-color-background: #2b2e33; --dark-color-background-secondary: #1e2024; --dark-color-background-warning: #bebe00; --dark-color-warning-text: #222; --dark-color-accent: #9096a2; --dark-color-active-menu-item: #5d5d6a; --dark-color-text: #f5f5f5; --dark-color-text-aside: #dddddd; --dark-color-icon-background: var(--dark-color-background-secondary); --dark-color-icon-text: var(--dark-color-text); --dark-color-comment-tag-text: var(--dark-color-text); --dark-color-comment-tag: var(--dark-color-background); --dark-color-link: #00aff4; --dark-color-focus-outline: #4c97f2; --dark-color-ts-keyword: #3399ff; --dark-color-ts-project: #e358ff; --dark-color-ts-module: var(--dark-color-ts-project); --dark-color-ts-namespace: var(--dark-color-ts-project); --dark-color-ts-enum: #f4d93e; --dark-color-ts-enum-member: var(--dark-color-ts-enum); --dark-color-ts-variable: #798dff; --dark-color-ts-function: #a280ff; --dark-color-ts-class: #8ac4ff; --dark-color-ts-interface: #6cff87; --dark-color-ts-constructor: var(--dark-color-ts-class); --dark-color-ts-property: #ff984d; --dark-color-ts-method: #ff4db8; --dark-color-ts-reference: #ff4d82; --dark-color-ts-call-signature: var(--dark-color-ts-method); --dark-color-ts-index-signature: var(--dark-color-ts-property); --dark-color-ts-constructor-signature: var(--dark-color-ts-constructor); --dark-color-ts-parameter: var(--dark-color-ts-variable); /* type literal not included as links will never be generated to it */ --dark-color-ts-type-parameter: #e07d13; --dark-color-ts-accessor: #ff6060; --dark-color-ts-get-signature: var(--dark-color-ts-accessor); --dark-color-ts-set-signature: var(--dark-color-ts-accessor); --dark-color-ts-type-alias: #ff6492; /* reference not included as links will be colored with the kind that it points to */ --dark-color-document: #ffffff; --dark-color-alert-note: #0969d9; --dark-color-alert-tip: #1a7f37; --dark-color-alert-important: #8250df; --dark-color-alert-warning: #9a6700; --dark-color-alert-caution: #cf222e; --dark-external-icon: url("data:image/svg+xml;utf8,"); --dark-color-scheme: dark; } @media (prefers-color-scheme: light) { :root { --color-background: var(--light-color-background); --color-background-secondary: var( --light-color-background-secondary ); --color-background-warning: var(--light-color-background-warning); --color-warning-text: var(--light-color-warning-text); --color-accent: var(--light-color-accent); --color-active-menu-item: var(--light-color-active-menu-item); --color-text: var(--light-color-text); --color-text-aside: var(--light-color-text-aside); --color-icon-background: var(--light-color-icon-background); --color-icon-text: var(--light-color-icon-text); --color-comment-tag-text: var(--light-color-text); --color-comment-tag: var(--light-color-background); --color-link: var(--light-color-link); --color-focus-outline: var(--light-color-focus-outline); --color-ts-keyword: var(--light-color-ts-keyword); --color-ts-project: var(--light-color-ts-project); --color-ts-module: var(--light-color-ts-module); --color-ts-namespace: var(--light-color-ts-namespace); --color-ts-enum: var(--light-color-ts-enum); --color-ts-enum-member: var(--light-color-ts-enum-member); --color-ts-variable: var(--light-color-ts-variable); --color-ts-function: var(--light-color-ts-function); --color-ts-class: var(--light-color-ts-class); --color-ts-interface: var(--light-color-ts-interface); --color-ts-constructor: var(--light-color-ts-constructor); --color-ts-property: var(--light-color-ts-property); --color-ts-method: var(--light-color-ts-method); --color-ts-reference: var(--light-color-ts-reference); --color-ts-call-signature: var(--light-color-ts-call-signature); --color-ts-index-signature: var(--light-color-ts-index-signature); --color-ts-constructor-signature: var( --light-color-ts-constructor-signature ); --color-ts-parameter: var(--light-color-ts-parameter); --color-ts-type-parameter: var(--light-color-ts-type-parameter); --color-ts-accessor: var(--light-color-ts-accessor); --color-ts-get-signature: var(--light-color-ts-get-signature); --color-ts-set-signature: var(--light-color-ts-set-signature); --color-ts-type-alias: var(--light-color-ts-type-alias); --color-document: var(--light-color-document); --color-alert-note: var(--light-color-alert-note); --color-alert-tip: var(--light-color-alert-tip); --color-alert-important: var(--light-color-alert-important); --color-alert-warning: var(--light-color-alert-warning); --color-alert-caution: var(--light-color-alert-caution); --external-icon: var(--light-external-icon); --color-scheme: var(--light-color-scheme); } } @media (prefers-color-scheme: dark) { :root { --color-background: var(--dark-color-background); --color-background-secondary: var( --dark-color-background-secondary ); --color-background-warning: var(--dark-color-background-warning); --color-warning-text: var(--dark-color-warning-text); --color-accent: var(--dark-color-accent); --color-active-menu-item: var(--dark-color-active-menu-item); --color-text: var(--dark-color-text); --color-text-aside: var(--dark-color-text-aside); --color-icon-background: var(--dark-color-icon-background); --color-icon-text: var(--dark-color-icon-text); --color-comment-tag-text: var(--dark-color-text); --color-comment-tag: var(--dark-color-background); --color-link: var(--dark-color-link); --color-focus-outline: var(--dark-color-focus-outline); --color-ts-keyword: var(--dark-color-ts-keyword); --color-ts-project: var(--dark-color-ts-project); --color-ts-module: var(--dark-color-ts-module); --color-ts-namespace: var(--dark-color-ts-namespace); --color-ts-enum: var(--dark-color-ts-enum); --color-ts-enum-member: var(--dark-color-ts-enum-member); --color-ts-variable: var(--dark-color-ts-variable); --color-ts-function: var(--dark-color-ts-function); --color-ts-class: var(--dark-color-ts-class); --color-ts-interface: var(--dark-color-ts-interface); --color-ts-constructor: var(--dark-color-ts-constructor); --color-ts-property: var(--dark-color-ts-property); --color-ts-method: var(--dark-color-ts-method); --color-ts-reference: var(--dark-color-ts-reference); --color-ts-call-signature: var(--dark-color-ts-call-signature); --color-ts-index-signature: var(--dark-color-ts-index-signature); --color-ts-constructor-signature: var( --dark-color-ts-constructor-signature ); --color-ts-parameter: var(--dark-color-ts-parameter); --color-ts-type-parameter: var(--dark-color-ts-type-parameter); --color-ts-accessor: var(--dark-color-ts-accessor); --color-ts-get-signature: var(--dark-color-ts-get-signature); --color-ts-set-signature: var(--dark-color-ts-set-signature); --color-ts-type-alias: var(--dark-color-ts-type-alias); --color-document: var(--dark-color-document); --color-alert-note: var(--dark-color-alert-note); --color-alert-tip: var(--dark-color-alert-tip); --color-alert-important: var(--dark-color-alert-important); --color-alert-warning: var(--dark-color-alert-warning); --color-alert-caution: var(--dark-color-alert-caution); --external-icon: var(--dark-external-icon); --color-scheme: var(--dark-color-scheme); } } html { color-scheme: var(--color-scheme); } body { margin: 0; } :root[data-theme="light"] { --color-background: var(--light-color-background); --color-background-secondary: var(--light-color-background-secondary); --color-background-warning: var(--light-color-background-warning); --color-warning-text: var(--light-color-warning-text); --color-icon-background: var(--light-color-icon-background); --color-accent: var(--light-color-accent); --color-active-menu-item: var(--light-color-active-menu-item); --color-text: var(--light-color-text); --color-text-aside: var(--light-color-text-aside); --color-icon-text: var(--light-color-icon-text); --color-comment-tag-text: var(--light-color-text); --color-comment-tag: var(--light-color-background); --color-link: var(--light-color-link); --color-focus-outline: var(--light-color-focus-outline); --color-ts-keyword: var(--light-color-ts-keyword); --color-ts-project: var(--light-color-ts-project); --color-ts-module: var(--light-color-ts-module); --color-ts-namespace: var(--light-color-ts-namespace); --color-ts-enum: var(--light-color-ts-enum); --color-ts-enum-member: var(--light-color-ts-enum-member); --color-ts-variable: var(--light-color-ts-variable); --color-ts-function: var(--light-color-ts-function); --color-ts-class: var(--light-color-ts-class); --color-ts-interface: var(--light-color-ts-interface); --color-ts-constructor: var(--light-color-ts-constructor); --color-ts-property: var(--light-color-ts-property); --color-ts-method: var(--light-color-ts-method); --color-ts-reference: var(--light-color-ts-reference); --color-ts-call-signature: var(--light-color-ts-call-signature); --color-ts-index-signature: var(--light-color-ts-index-signature); --color-ts-constructor-signature: var( --light-color-ts-constructor-signature ); --color-ts-parameter: var(--light-color-ts-parameter); --color-ts-type-parameter: var(--light-color-ts-type-parameter); --color-ts-accessor: var(--light-color-ts-accessor); --color-ts-get-signature: var(--light-color-ts-get-signature); --color-ts-set-signature: var(--light-color-ts-set-signature); --color-ts-type-alias: var(--light-color-ts-type-alias); --color-document: var(--light-color-document); --color-note: var(--light-color-note); --color-tip: var(--light-color-tip); --color-important: var(--light-color-important); --color-warning: var(--light-color-warning); --color-caution: var(--light-color-caution); --external-icon: var(--light-external-icon); --color-scheme: var(--light-color-scheme); } :root[data-theme="dark"] { --color-background: var(--dark-color-background); --color-background-secondary: var(--dark-color-background-secondary); --color-background-warning: var(--dark-color-background-warning); --color-warning-text: var(--dark-color-warning-text); --color-icon-background: var(--dark-color-icon-background); --color-accent: var(--dark-color-accent); --color-active-menu-item: var(--dark-color-active-menu-item); --color-text: var(--dark-color-text); --color-text-aside: var(--dark-color-text-aside); --color-icon-text: var(--dark-color-icon-text); --color-comment-tag-text: var(--dark-color-text); --color-comment-tag: var(--dark-color-background); --color-link: var(--dark-color-link); --color-focus-outline: var(--dark-color-focus-outline); --color-ts-keyword: var(--dark-color-ts-keyword); --color-ts-project: var(--dark-color-ts-project); --color-ts-module: var(--dark-color-ts-module); --color-ts-namespace: var(--dark-color-ts-namespace); --color-ts-enum: var(--dark-color-ts-enum); --color-ts-enum-member: var(--dark-color-ts-enum-member); --color-ts-variable: var(--dark-color-ts-variable); --color-ts-function: var(--dark-color-ts-function); --color-ts-class: var(--dark-color-ts-class); --color-ts-interface: var(--dark-color-ts-interface); --color-ts-constructor: var(--dark-color-ts-constructor); --color-ts-property: var(--dark-color-ts-property); --color-ts-method: var(--dark-color-ts-method); --color-ts-reference: var(--dark-color-ts-reference); --color-ts-call-signature: var(--dark-color-ts-call-signature); --color-ts-index-signature: var(--dark-color-ts-index-signature); --color-ts-constructor-signature: var( --dark-color-ts-constructor-signature ); --color-ts-parameter: var(--dark-color-ts-parameter); --color-ts-type-parameter: var(--dark-color-ts-type-parameter); --color-ts-accessor: var(--dark-color-ts-accessor); --color-ts-get-signature: var(--dark-color-ts-get-signature); --color-ts-set-signature: var(--dark-color-ts-set-signature); --color-ts-type-alias: var(--dark-color-ts-type-alias); --color-document: var(--dark-color-document); --color-note: var(--dark-color-note); --color-tip: var(--dark-color-tip); --color-important: var(--dark-color-important); --color-warning: var(--dark-color-warning); --color-caution: var(--dark-color-caution); --external-icon: var(--dark-external-icon); --color-scheme: var(--dark-color-scheme); } *:focus-visible, .tsd-accordion-summary:focus-visible svg { outline: 2px solid var(--color-focus-outline); } .always-visible, .always-visible .tsd-signatures { display: inherit !important; } h1, h2, h3, h4, h5, h6 { line-height: 1.2; } h1 { font-size: 1.875rem; margin: 0.67rem 0; } h2 { font-size: 1.5rem; margin: 0.83rem 0; } h3 { font-size: 1.25rem; margin: 1rem 0; } h4 { font-size: 1.05rem; margin: 1.33rem 0; } h5 { font-size: 1rem; margin: 1.5rem 0; } h6 { font-size: 0.875rem; margin: 2.33rem 0; } dl, menu, ol, ul { margin: 1em 0; } dd { margin: 0 0 0 34px; } .container { max-width: 1700px; padding: 0 2rem; } /* Footer */ footer { border-top: 1px solid var(--color-accent); padding-top: 1rem; padding-bottom: 1rem; max-height: 3.5rem; } footer > p { margin: 0 1em; } .container-main { margin: 0 auto; /* toolbar, footer, margin */ min-height: calc(100vh - 41px - 56px - 4rem); } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes fade-out { from { opacity: 1; visibility: visible; } to { opacity: 0; } } @keyframes fade-in-delayed { 0% { opacity: 0; } 33% { opacity: 0; } 100% { opacity: 1; } } @keyframes fade-out-delayed { 0% { opacity: 1; visibility: visible; } 66% { opacity: 0; } 100% { opacity: 0; } } @keyframes pop-in-from-right { from { transform: translate(100%, 0); } to { transform: translate(0, 0); } } @keyframes pop-out-to-right { from { transform: translate(0, 0); visibility: visible; } to { transform: translate(100%, 0); } } body { background: var(--color-background); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; font-size: 16px; color: var(--color-text); } a { color: var(--color-link); text-decoration: none; } a:hover { text-decoration: underline; } a.external[target="_blank"] { background-image: var(--external-icon); background-position: top 3px right; background-repeat: no-repeat; padding-right: 13px; } a.tsd-anchor-link { color: var(--color-text); } code, pre { font-family: Menlo, Monaco, Consolas, "Courier New", monospace; padding: 0.2em; margin: 0; font-size: 0.875rem; border-radius: 0.8em; } pre { position: relative; white-space: pre-wrap; word-wrap: break-word; padding: 10px; border: 1px solid var(--color-accent); margin-bottom: 8px; } pre code { padding: 0; font-size: 100%; } pre > button { position: absolute; top: 10px; right: 10px; opacity: 0; transition: opacity 0.1s; box-sizing: border-box; } pre:hover > button, pre > button.visible { opacity: 1; } blockquote { margin: 1em 0; padding-left: 1em; border-left: 4px solid gray; } .tsd-typography { line-height: 1.333em; } .tsd-typography ul { list-style: square; padding: 0 0 0 20px; margin: 0; } .tsd-typography .tsd-index-panel h3, .tsd-index-panel .tsd-typography h3, .tsd-typography h4, .tsd-typography h5, .tsd-typography h6 { font-size: 1em; } .tsd-typography h5, .tsd-typography h6 { font-weight: normal; } .tsd-typography p, .tsd-typography ul, .tsd-typography ol { margin: 1em 0; } .tsd-typography table { border-collapse: collapse; border: none; } .tsd-typography td, .tsd-typography th { padding: 6px 13px; border: 1px solid var(--color-accent); } .tsd-typography thead, .tsd-typography tr:nth-child(even) { background-color: var(--color-background-secondary); } .tsd-alert { padding: 8px 16px; margin-bottom: 16px; border-left: 0.25em solid var(--alert-color); } .tsd-alert blockquote > :last-child, .tsd-alert > :last-child { margin-bottom: 0; } .tsd-alert-title { color: var(--alert-color); display: inline-flex; align-items: center; } .tsd-alert-title span { margin-left: 4px; } .tsd-alert-note { --alert-color: var(--color-alert-note); } .tsd-alert-tip { --alert-color: var(--color-alert-tip); } .tsd-alert-important { --alert-color: var(--color-alert-important); } .tsd-alert-warning { --alert-color: var(--color-alert-warning); } .tsd-alert-caution { --alert-color: var(--color-alert-caution); } .tsd-breadcrumb { margin: 0; padding: 0; color: var(--color-text-aside); } .tsd-breadcrumb a { color: var(--color-text-aside); text-decoration: none; } .tsd-breadcrumb a:hover { text-decoration: underline; } .tsd-breadcrumb li { display: inline; } .tsd-breadcrumb li:after { content: " / "; } .tsd-comment-tags { display: flex; flex-direction: column; } dl.tsd-comment-tag-group { display: flex; align-items: center; overflow: hidden; margin: 0.5em 0; } dl.tsd-comment-tag-group dt { display: flex; margin-right: 0.5em; font-size: 0.875em; font-weight: normal; } dl.tsd-comment-tag-group dd { margin: 0; } code.tsd-tag { padding: 0.25em 0.4em; border: 0.1em solid var(--color-accent); margin-right: 0.25em; font-size: 70%; } h1 code.tsd-tag:first-of-type { margin-left: 0.25em; } dl.tsd-comment-tag-group dd:before, dl.tsd-comment-tag-group dd:after { content: " "; } dl.tsd-comment-tag-group dd pre, dl.tsd-comment-tag-group dd:after { clear: both; } dl.tsd-comment-tag-group p { margin: 0; } .tsd-panel.tsd-comment .lead { font-size: 1.1em; line-height: 1.333em; margin-bottom: 2em; } .tsd-panel.tsd-comment .lead:last-child { margin-bottom: 0; } .tsd-filter-visibility h4 { font-size: 1rem; padding-top: 0.75rem; padding-bottom: 0.5rem; margin: 0; } .tsd-filter-item:not(:last-child) { margin-bottom: 0.5rem; } .tsd-filter-input { display: flex; width: -moz-fit-content; width: fit-content; align-items: center; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; } .tsd-filter-input input[type="checkbox"] { cursor: pointer; position: absolute; width: 1.5em; height: 1.5em; opacity: 0; } .tsd-filter-input input[type="checkbox"]:disabled { pointer-events: none; } .tsd-filter-input svg { cursor: pointer; width: 1.5em; height: 1.5em; margin-right: 0.5em; border-radius: 0.33em; /* Leaving this at full opacity breaks event listeners on Firefox. Don't remove unless you know what you're doing. */ opacity: 0.99; } .tsd-filter-input input[type="checkbox"]:focus-visible + svg { outline: 2px solid var(--color-focus-outline); } .tsd-checkbox-background { fill: var(--color-accent); } input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark { stroke: var(--color-text); } .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-background { fill: var(--color-background); stroke: var(--color-accent); stroke-width: 0.25rem; } .tsd-filter-input input:disabled ~ svg > .tsd-checkbox-checkmark { stroke: var(--color-accent); } .settings-label { font-weight: bold; text-transform: uppercase; display: inline-block; } .tsd-filter-visibility .settings-label { margin: 0.75rem 0 0.5rem 0; } .tsd-theme-toggle .settings-label { margin: 0.75rem 0.75rem 0 0; } .tsd-hierarchy h4 label:hover span { text-decoration: underline; } .tsd-hierarchy { list-style: square; margin: 0; } .tsd-hierarchy-target { font-weight: bold; } .tsd-hierarchy-toggle { color: var(--color-link); cursor: pointer; } .tsd-full-hierarchy:not(:last-child) { margin-bottom: 1em; padding-bottom: 1em; border-bottom: 1px solid var(--color-accent); } .tsd-full-hierarchy, .tsd-full-hierarchy ul { list-style: none; margin: 0; padding: 0; } .tsd-full-hierarchy ul { padding-left: 1.5rem; } .tsd-full-hierarchy a { padding: 0.25rem 0 !important; font-size: 1rem; display: inline-flex; align-items: center; color: var(--color-text); } .tsd-full-hierarchy svg[data-dropdown] { cursor: pointer; } .tsd-full-hierarchy svg[data-dropdown="false"] { transform: rotate(-90deg); } .tsd-full-hierarchy svg[data-dropdown="false"] ~ ul { display: none; } .tsd-panel-group.tsd-index-group { margin-bottom: 0; } .tsd-index-panel .tsd-index-list { list-style: none; line-height: 1.333em; margin: 0; padding: 0.25rem 0 0 0; overflow: hidden; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 1rem; grid-template-rows: auto; } @media (max-width: 1024px) { .tsd-index-panel .tsd-index-list { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 768px) { .tsd-index-panel .tsd-index-list { grid-template-columns: repeat(1, 1fr); } } .tsd-index-panel .tsd-index-list li { -webkit-page-break-inside: avoid; -moz-page-break-inside: avoid; -ms-page-break-inside: avoid; -o-page-break-inside: avoid; page-break-inside: avoid; } .tsd-flag { display: inline-block; padding: 0.25em 0.4em; border-radius: 4px; color: var(--color-comment-tag-text); background-color: var(--color-comment-tag); text-indent: 0; font-size: 75%; line-height: 1; font-weight: normal; } .tsd-anchor { position: relative; top: -100px; } .tsd-member { position: relative; } .tsd-member .tsd-anchor + h3 { display: flex; align-items: center; margin-top: 0; margin-bottom: 0; border-bottom: none; } .tsd-navigation.settings { margin: 1rem 0; } .tsd-navigation > a, .tsd-navigation .tsd-accordion-summary { width: calc(100% - 0.25rem); display: flex; align-items: center; } .tsd-navigation a, .tsd-navigation summary > span, .tsd-page-navigation a { display: flex; width: calc(100% - 0.25rem); align-items: center; padding: 0.25rem; color: var(--color-text); text-decoration: none; box-sizing: border-box; } .tsd-navigation a.current, .tsd-page-navigation a.current { background: var(--color-active-menu-item); } .tsd-navigation a:hover, .tsd-page-navigation a:hover { text-decoration: underline; } .tsd-navigation ul, .tsd-page-navigation ul { margin-top: 0; margin-bottom: 0; padding: 0; list-style: none; } .tsd-navigation li, .tsd-page-navigation li { padding: 0; max-width: 100%; } .tsd-navigation .tsd-nav-link { display: none; } .tsd-nested-navigation { margin-left: 3rem; } .tsd-nested-navigation > li > details { margin-left: -1.5rem; } .tsd-small-nested-navigation { margin-left: 1.5rem; } .tsd-small-nested-navigation > li > details { margin-left: -1.5rem; } .tsd-page-navigation-section { margin-left: 10px; } .tsd-page-navigation-section > summary { padding: 0.25rem; } .tsd-page-navigation-section > div { margin-left: 20px; } .tsd-page-navigation ul { padding-left: 1.75rem; } #tsd-sidebar-links a { margin-top: 0; margin-bottom: 0.5rem; line-height: 1.25rem; } #tsd-sidebar-links a:last-of-type { margin-bottom: 0; } a.tsd-index-link { padding: 0.25rem 0 !important; font-size: 1rem; line-height: 1.25rem; display: inline-flex; align-items: center; color: var(--color-text); } .tsd-accordion-summary { list-style-type: none; /* hide marker on non-safari */ outline: none; /* broken on safari, so just hide it */ } .tsd-accordion-summary::-webkit-details-marker { display: none; /* hide marker on safari */ } .tsd-accordion-summary, .tsd-accordion-summary a { -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; } .tsd-accordion-summary a { width: calc(100% - 1.5rem); } .tsd-accordion-summary > * { margin-top: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; } .tsd-accordion .tsd-accordion-summary > svg { margin-left: 0.25rem; vertical-align: text-top; } /* * We need to be careful to target the arrow indicating whether the accordion * is open, but not any other SVGs included in the details element. */ .tsd-accordion:not([open]) > .tsd-accordion-summary > svg:first-child, .tsd-accordion:not([open]) > .tsd-accordion-summary > h1 > svg:first-child, .tsd-accordion:not([open]) > .tsd-accordion-summary > h2 > svg:first-child, .tsd-accordion:not([open]) > .tsd-accordion-summary > h3 > svg:first-child, .tsd-accordion:not([open]) > .tsd-accordion-summary > h4 > svg:first-child, .tsd-accordion:not([open]) > .tsd-accordion-summary > h5 > svg:first-child { transform: rotate(-90deg); } .tsd-index-content > :not(:first-child) { margin-top: 0.75rem; } .tsd-index-heading { margin-top: 1.5rem; margin-bottom: 0.75rem; } .tsd-no-select { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .tsd-kind-icon { margin-right: 0.5rem; width: 1.25rem; height: 1.25rem; min-width: 1.25rem; min-height: 1.25rem; } .tsd-signature > .tsd-kind-icon { margin-right: 0.8rem; } .tsd-panel { margin-bottom: 2.5rem; } .tsd-panel.tsd-member { margin-bottom: 4rem; } .tsd-panel:empty { display: none; } .tsd-panel > h1, .tsd-panel > h2, .tsd-panel > h3 { margin: 1.5rem -1.5rem 0.75rem -1.5rem; padding: 0 1.5rem 0.75rem 1.5rem; } .tsd-panel > h1.tsd-before-signature, .tsd-panel > h2.tsd-before-signature, .tsd-panel > h3.tsd-before-signature { margin-bottom: 0; border-bottom: none; } .tsd-panel-group { margin: 2rem 0; } .tsd-panel-group.tsd-index-group { margin: 2rem 0; } .tsd-panel-group.tsd-index-group details { margin: 2rem 0; } .tsd-panel-group > .tsd-accordion-summary { margin-bottom: 1rem; } #tsd-search { transition: background-color 0.2s; } #tsd-search .title { position: relative; z-index: 2; } #tsd-search .field { position: absolute; left: 0; top: 0; right: 2.5rem; height: 100%; } #tsd-search .field input { box-sizing: border-box; position: relative; top: -50px; z-index: 1; width: 100%; padding: 0 10px; opacity: 0; outline: 0; border: 0; background: transparent; color: var(--color-text); } #tsd-search .field label { position: absolute; overflow: hidden; right: -40px; } #tsd-search .field input, #tsd-search .title, #tsd-toolbar-links a { transition: opacity 0.2s; } #tsd-search .results { position: absolute; visibility: hidden; top: 40px; width: 100%; margin: 0; padding: 0; list-style: none; box-shadow: 0 0 4px rgba(0, 0, 0, 0.25); } #tsd-search .results li { background-color: var(--color-background); line-height: initial; padding: 4px; } #tsd-search .results li:nth-child(even) { background-color: var(--color-background-secondary); } #tsd-search .results li.state { display: none; } #tsd-search .results li.current:not(.no-results), #tsd-search .results li:hover:not(.no-results) { background-color: var(--color-accent); } #tsd-search .results a { display: flex; align-items: center; padding: 0.25rem; box-sizing: border-box; } #tsd-search .results a:before { top: 10px; } #tsd-search .results span.parent { color: var(--color-text-aside); font-weight: normal; } #tsd-search.has-focus { background-color: var(--color-accent); } #tsd-search.has-focus .field input { top: 0; opacity: 1; } #tsd-search.has-focus .title, #tsd-search.has-focus #tsd-toolbar-links a { z-index: 0; opacity: 0; } #tsd-search.has-focus .results { visibility: visible; } #tsd-search.loading .results li.state.loading { display: block; } #tsd-search.failure .results li.state.failure { display: block; } #tsd-toolbar-links { position: absolute; top: 0; right: 2rem; height: 100%; display: flex; align-items: center; justify-content: flex-end; } #tsd-toolbar-links a { margin-left: 1.5rem; } #tsd-toolbar-links a:hover { text-decoration: underline; } .tsd-signature { margin: 0 0 1rem 0; padding: 1rem 0.5rem; border: 1px solid var(--color-accent); font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 14px; overflow-x: auto; } .tsd-signature-keyword { color: var(--color-ts-keyword); font-weight: normal; } .tsd-signature-symbol { color: var(--color-text-aside); font-weight: normal; } .tsd-signature-type { font-style: italic; font-weight: normal; } .tsd-signatures { padding: 0; margin: 0 0 1em 0; list-style-type: none; } .tsd-signatures .tsd-signature { margin: 0; border-color: var(--color-accent); border-width: 1px 0; transition: background-color 0.1s; } .tsd-signatures .tsd-index-signature:not(:last-child) { margin-bottom: 1em; } .tsd-signatures .tsd-index-signature .tsd-signature { border-width: 1px; } .tsd-description .tsd-signatures .tsd-signature { border-width: 1px; } ul.tsd-parameter-list, ul.tsd-type-parameter-list { list-style: square; margin: 0; padding-left: 20px; } ul.tsd-parameter-list > li.tsd-parameter-signature, ul.tsd-type-parameter-list > li.tsd-parameter-signature { list-style: none; margin-left: -20px; } ul.tsd-parameter-list h5, ul.tsd-type-parameter-list h5 { font-size: 16px; margin: 1em 0 0.5em 0; } .tsd-sources { margin-top: 1rem; font-size: 0.875em; } .tsd-sources a { color: var(--color-text-aside); text-decoration: underline; } .tsd-sources ul { list-style: none; padding: 0; } .tsd-page-toolbar { position: sticky; z-index: 1; top: 0; left: 0; width: 100%; color: var(--color-text); background: var(--color-background-secondary); border-bottom: 1px var(--color-accent) solid; transition: transform 0.3s ease-in-out; } .tsd-page-toolbar a { color: var(--color-text); text-decoration: none; } .tsd-page-toolbar a.title { font-weight: bold; } .tsd-page-toolbar a.title:hover { text-decoration: underline; } .tsd-page-toolbar .tsd-toolbar-contents { display: flex; justify-content: space-between; height: 2.5rem; margin: 0 auto; } .tsd-page-toolbar .table-cell { position: relative; white-space: nowrap; line-height: 40px; } .tsd-page-toolbar .table-cell:first-child { width: 100%; } .tsd-page-toolbar .tsd-toolbar-icon { box-sizing: border-box; line-height: 0; padding: 12px 0; } .tsd-widget { display: inline-block; overflow: hidden; opacity: 0.8; height: 40px; transition: opacity 0.1s, background-color 0.2s; vertical-align: bottom; cursor: pointer; } .tsd-widget:hover { opacity: 0.9; } .tsd-widget.active { opacity: 1; background-color: var(--color-accent); } .tsd-widget.no-caption { width: 40px; } .tsd-widget.no-caption:before { margin: 0; } .tsd-widget.options, .tsd-widget.menu { display: none; } input[type="checkbox"] + .tsd-widget:before { background-position: -120px 0; } input[type="checkbox"]:checked + .tsd-widget:before { background-position: -160px 0; } img { max-width: 100%; } .tsd-member-summary-name { display: inline-flex; align-items: center; padding: 0.25rem; text-decoration: none; } .tsd-anchor-icon { display: inline-flex; align-items: center; margin-left: 0.5rem; color: var(--color-text); } .tsd-anchor-icon svg { width: 1em; height: 1em; visibility: hidden; } .tsd-member-summary-name:hover > .tsd-anchor-icon svg, .tsd-anchor-link:hover > .tsd-anchor-icon svg { visibility: visible; } .deprecated { text-decoration: line-through !important; } .warning { padding: 1rem; color: var(--color-warning-text); background: var(--color-background-warning); } .tsd-kind-project { color: var(--color-ts-project); } .tsd-kind-module { color: var(--color-ts-module); } .tsd-kind-namespace { color: var(--color-ts-namespace); } .tsd-kind-enum { color: var(--color-ts-enum); } .tsd-kind-enum-member { color: var(--color-ts-enum-member); } .tsd-kind-variable { color: var(--color-ts-variable); } .tsd-kind-function { color: var(--color-ts-function); } .tsd-kind-class { color: var(--color-ts-class); } .tsd-kind-interface { color: var(--color-ts-interface); } .tsd-kind-constructor { color: var(--color-ts-constructor); } .tsd-kind-property { color: var(--color-ts-property); } .tsd-kind-method { color: var(--color-ts-method); } .tsd-kind-reference { color: var(--color-ts-reference); } .tsd-kind-call-signature { color: var(--color-ts-call-signature); } .tsd-kind-index-signature { color: var(--color-ts-index-signature); } .tsd-kind-constructor-signature { color: var(--color-ts-constructor-signature); } .tsd-kind-parameter { color: var(--color-ts-parameter); } .tsd-kind-type-parameter { color: var(--color-ts-type-parameter); } .tsd-kind-accessor { color: var(--color-ts-accessor); } .tsd-kind-get-signature { color: var(--color-ts-get-signature); } .tsd-kind-set-signature { color: var(--color-ts-set-signature); } .tsd-kind-type-alias { color: var(--color-ts-type-alias); } /* if we have a kind icon, don't color the text by kind */ .tsd-kind-icon ~ span { color: var(--color-text); } * { scrollbar-width: thin; scrollbar-color: var(--color-accent) var(--color-icon-background); } *::-webkit-scrollbar { width: 0.75rem; } *::-webkit-scrollbar-track { background: var(--color-icon-background); } *::-webkit-scrollbar-thumb { background-color: var(--color-accent); border-radius: 999rem; border: 0.25rem solid var(--color-icon-background); } /* mobile */ @media (max-width: 769px) { .tsd-widget.options, .tsd-widget.menu { display: inline-block; } .container-main { display: flex; } html .col-content { float: none; max-width: 100%; width: 100%; } html .col-sidebar { position: fixed !important; overflow-y: auto; -webkit-overflow-scrolling: touch; z-index: 1024; top: 0 !important; bottom: 0 !important; left: auto !important; right: 0 !important; padding: 1.5rem 1.5rem 0 0; width: 75vw; visibility: hidden; background-color: var(--color-background); transform: translate(100%, 0); } html .col-sidebar > *:last-child { padding-bottom: 20px; } html .overlay { content: ""; display: block; position: fixed; z-index: 1023; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.75); visibility: hidden; } .to-has-menu .overlay { animation: fade-in 0.4s; } .to-has-menu .col-sidebar { animation: pop-in-from-right 0.4s; } .from-has-menu .overlay { animation: fade-out 0.4s; } .from-has-menu .col-sidebar { animation: pop-out-to-right 0.4s; } .has-menu body { overflow: hidden; } .has-menu .overlay { visibility: visible; } .has-menu .col-sidebar { visibility: visible; transform: translate(0, 0); display: flex; flex-direction: column; gap: 1.5rem; max-height: 100vh; padding: 1rem 2rem; } .has-menu .tsd-navigation { max-height: 100%; } #tsd-toolbar-links { display: none; } .tsd-navigation .tsd-nav-link { display: flex; } } /* one sidebar */ @media (min-width: 770px) { .container-main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); grid-template-areas: "sidebar content"; margin: 2rem auto; } .col-sidebar { grid-area: sidebar; } .col-content { grid-area: content; padding: 0 1rem; } } @media (min-width: 770px) and (max-width: 1399px) { .col-sidebar { max-height: calc(100vh - 2rem - 42px); overflow: auto; position: sticky; top: 42px; padding-top: 1rem; } .site-menu { margin-top: 1rem; } } /* two sidebars */ @media (min-width: 1200px) { .container-main { grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr) minmax( 0, 20rem ); grid-template-areas: "sidebar content toc"; } .col-sidebar { display: contents; } .page-menu { grid-area: toc; padding-left: 1rem; } .site-menu { grid-area: sidebar; } .site-menu { margin-top: 1rem; } .page-menu, .site-menu { max-height: calc(100vh - 2rem - 42px); overflow: auto; position: sticky; top: 42px; } } } ================================================ FILE: docs/api/classes/util.Deferred.html ================================================ Deferred | wunderbaum - v0.14.1

A ES6 Promise, that exposes the resolve()/reject() methods.

TODO: See Promise.withResolvers() , a proposed standard, but not yet implemented in any browser.

Constructors

Methods

  • Parameters

    • cb: any

    Returns void

  • Returns { catch: (cb: any) => void; then: (cb: any) => void }

  • Parameters

    • Optionalerror: any

    Returns void

  • Parameters

    • Optionalvalue: any

    Returns void

  • Parameters

    • cb: any

    Returns void

================================================ FILE: docs/api/classes/util.ValidationError.html ================================================ ValidationError | wunderbaum - v0.14.1

Class ValidationError

A generic error that can be thrown to indicate a validation error when handling the apply event for a node title or the change event for a grid cell.

Hierarchy

  • Error
    • ValidationError

Constructors

Properties

message: string
name: string
stack?: string
stackTraceLimit: number

The Error.stackTraceLimit property specifies the number of stack frames collected by a stack trace (whether generated by new Error().stack or Error.captureStackTrace(obj)).

The default value is 10 but may be set to any valid JavaScript number. Changes will affect any stack trace captured after the value has been changed.

If set to a non-number value, or set to a negative number, stack traces will not capture any frames.

Methods

  • Creates a .stack property on targetObject, which when accessed returns a string representing the location in the code at which Error.captureStackTrace() was called.

    const myObject = {};
    Error.captureStackTrace(myObject);
    myObject.stack; // Similar to `new Error().stack`

    The first line of the trace will be prefixed with ${myObject.name}: ${myObject.message}.

    The optional constructorOpt argument accepts a function. If given, all frames above constructorOpt, including constructorOpt, will be omitted from the generated stack trace.

    The constructorOpt argument is useful for hiding implementation details of error generation from the user. For instance:

    function a() {
    b();
    }

    function b() {
    c();
    }

    function c() {
    // Create an error without stack trace to avoid calculating the stack trace twice.
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = 0;
    const error = new Error();
    Error.stackTraceLimit = stackTraceLimit;

    // Capture the stack trace above function b
    Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
    throw error;
    }

    a();

    Parameters

    • targetObject: object
    • OptionalconstructorOpt: Function

    Returns void

================================================ FILE: docs/api/classes/wb_node.WunderbaumNode.html ================================================ WunderbaumNode | wunderbaum - v0.14.1

A single tree node.

NOTE:
Generally you should not modify properties directly, since this may break the internal bookkeeping.

Constructors

Properties

Methods

Constructors

Properties

_errorInfo: any = null
_filterAutoExpanded?: boolean
_isLoading: boolean = false
_partload: boolean = false
_partsel: boolean = false
_requestId: number = 0
_rowElem: undefined | HTMLDivElement = undefined
_rowIdx: undefined | number = 0
checkbox?: CheckboxOption

Render a checkbox or radio button

children: null | WunderbaumNode[] = null

Array of child nodes (null for leaf nodes). For lazy nodes, this is null or ùndefineduntil the children are loaded and leaf nodes may be[]` (empty array).

classes: null | Set<string> = null

Additional classes added to div.wb-row.

colspan?: boolean

If true, (in grid mode) no cells are rendered, except for the node title.

data: any = {}

Custom data that was passed to the constructor

expanded?: boolean

Expansion state.

icon?: IconOption

Icon definition.

iconTooltip?: TooltipOption

Icon tooltip definition (true: use node's title).

key: string

Unique key. Passed with constructor or defaults to SEQUENCE.

Use setKey to modify.

lazy?: boolean

Lazy loading flag.

match?: number

0 if matched (-1 to keep system nodes visible); Added and removed by filter code.

Parent node (null for the invisible root node tree.root).

radiogroup?: boolean

If true, this node's children are considerd radio buttons.

isRadio.

refKey: undefined | string = undefined

Reference key. Unlike key, a refKey may occur multiple times within a tree (in this case we have 'clone nodes').

Use setKey to modify.

selected?: boolean

Selection state.

statusNodeType?: NodeStatusType
subMatchCount?: number = 0
title: string

Name of the node.

Use setTitle to modify.

titleWithHighlight?: string
tooltip?: TooltipOption

Tooltip definition (true: use node's title).

Reference to owning tree.

type?: string

Node type (used for styling).

unselectable?: boolean
sequence: number = 0

Methods

  • Internal

    Return true if at least on selectable descendant end-node is unselected.

    Returns boolean

  • Call event handler if defined in tree.options. Example:

    node._callEvent("edit.beforeEdit", {foo: 42})
    

    Parameters

    • type: string
    • Optionalextra: any

    Returns any

  • Parameters

    • source: any

    Returns Promise<any>

  • Find all descendant nodes that match condition (excluding self).

    If match is a string, search for exact node title. If match is a RegExp expression, apply it to node.title, using RegExp.test(). If match is a callback, match all nodes for that the callback(node) returns true.

    Returns an empty array if no nodes were found.

    Examples:

    // Match all node titles that match exactly 'Joe':
    nodeList = node.findAll("Joe")
    // Match all node titles that start with 'Joe' case sensitive:
    nodeList = node.findAll(/^Joe/)
    // Match all node titles that contain 'oe', case insensitive:
    nodeList = node.findAll(/oe/i)
    // Match all nodes with `data.price` >= 99:
    nodeList = node.findAll((n) => {
    return n.data.price >= 99;
    })

    Parameters

    Returns WunderbaumNode[]

  • Fix selection status for multi-hier mode. Only end-nodes are considered to update the descendants branch and parents. Should be called after this node has loaded new children or after children have been modified using the API.

    Parameters

    Returns void

  • Return a multiline string representation of a node/subnode hierarchy. Mostly useful for debugging.

    Example:

    console.info(tree.getActiveNode().format((n)=>n.title));
    

    logs

    Books
    ├─ Art of War
    ╰─ Don Quixote

    Parameters

    Returns string

  • Return the <span class='wb-col'> element with a given index or id.

    Parameters

    • colIdx: string | number

    Returns null | HTMLSpanElement

  • Return node depth (starting with 1 for top level nodes).

    Returns number

  • Return an option value that has a default, but may be overridden by a callback or a node instance attribute.

    Evaluation sequence:

    • If tree.options.<name> is a callback that returns something, use that.
    • Else if node.<name> is defined, use that.
    • Else if tree.types[<node.type>] is a value, use that.
    • Else if tree.options.<name> is a value, use that.
    • Else use defaultValue.

    Parameters

    • name: string

      name of the option property (on node and tree)

    • OptionaldefaultValue: any

      return this if nothing else matched Wunderbaum.getOption

    Returns any

  • Return an array of all parent nodes (top-down).

    Parameters

    • includeRoot: boolean = false

      Include the invisible system root node.

    • includeSelf: boolean = false

      Include the node itself.

    Returns WunderbaumNode[]

  • Return a string representing the hierarchical node path, e.g. "a/b/c".

    Parameters

    • includeSelf: boolean = true
    • part: (keyof WunderbaumNode) | NodeAnyCallback = "title"

      property name or callback

    • separator: string = "/"

    Returns string

  • Return an array of refKey values.

    RefKeys are unique identifiers for a node data, and are used to identify clones. If more than one node has the same refKey, it is only returned once.

    Parameters

    • selected: boolean = false

      if true, only return refKeys of selected nodes.

    Returns string[]

  • Return an array of selected nodes.

    Parameters

    • stopOnParents: boolean = false

      only return the topmost selected node (useful with selectMode 'hier')

    Returns WunderbaumNode[]

  • Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded.

    Returns undefined | boolean

  • Return true if node has className set.

    Parameters

    • className: string

    Returns boolean

  • Return true if node is the currently focused node.

    Returns boolean

    0.9.0

  • Return true if this node is the currently active tree node.

    Returns boolean

  • Return true if this node's refKey is used by at least one other node.

    Returns boolean

  • Return true if this node's title spans all columns, i.e. the node has no grid cells.

    Returns boolean

  • Return true if this node has children, i.e. the node is generally expandable. If andCollapsed is set, we also check if this node is collapsed, i.e. an expand operation is currently possible.

    Parameters

    • andCollapsed: boolean = false

    Returns boolean

  • Return true if this node is currently expanded.

    Returns boolean

  • Return true if this node is the first node of its parent's children.

    Returns boolean

  • Return true if this node is the last node of its parent's children.

    Returns boolean

  • Return true if this node is lazy (even if data was already loaded)

    Returns boolean

  • Return true if node is lazy and loaded. For non-lazy nodes always return true.

    Returns boolean

  • Return true if node is currently loading, i.e. a GET request is pending.

    Returns boolean

  • [ext-filter] Return true if this node is matched by current filter (or no filter is active).

    Returns boolean

  • Return true if this node is a temporarily generated status node of type 'paging'.

    Returns boolean

  • Experimental

    Return true if this node is partially loaded.

    Returns boolean

  • Return true if this node is partially selected (tri-state).

    Returns boolean

  • Return true if this node has DOM representation, i.e. is displayed in the viewport.

    Returns boolean

  • Return true if this node has DOM representation, i.e. is displayed in the viewport.

    Returns boolean

  • Return true if this node is a temporarily generated system node like 'loading', 'paging', or 'error' (node.statusNodeType contains the type).

    Returns boolean

  • Return true if this a top level node, i.e. a direct child of the (invisible) system root node.

    Returns boolean

  • Return true if node is marked lazy but not yet loaded. For non-lazy nodes always return false.

    Returns boolean

  • Return true if all parent nodes are expanded. Note: this does not check whether the node is scrolled into the visible part of the screen or viewport.

    Returns boolean

  • Load content of a lazy node. If the node is already loaded, nothing happens.

    Parameters

    • OptionalforceReload: boolean = false

      If true, reload even if already loaded.

    Returns Promise<void>

  • Write to console.debug with node name as prefix if opts.debugLevel >= 4 and browser console level includes debug/verbose messages.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.error with node name as prefix if opts.debugLevel >= 1.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.info with node name as prefix if opts.debugLevel >= 3.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.warn with node name as prefix if opts.debugLevel >= 2.

    Parameters

    • ...args: any[]

    Returns void

  • Expand all parents and optionally scroll into visible area as neccessary. Promise is resolved, when lazy loading and animations are done.

    Parameters

    • Optionaloptions: MakeVisibleOptions

      passed to setExpanded(). Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}

    Returns Promise<unknown>

  • Set focus relative to this node and optionally activate.

    'left' collapses the node if it is expanded, or move to the parent otherwise. 'right' expands the node if it is collapsed, or move to the first child otherwise.

    Parameters

    • where: string

      'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. (Alternatively the event.key that would normally trigger this move, e.g. ArrowLeft = 'left'.

    • Optionaloptions: NavigateOptions

    Returns Promise<void | WunderbaumNode>

  • Remove all descendants of this node.

    Returns void

  • Remove all HTML markup from the DOM.

    Returns void

  • Remove all children, collapse, and set the lazy-flag, so that the lazyLoad event is triggered on next expand.

    Returns void

  • Renumber nodes _nativeIndex. This is useful to allow to restore the order after sorting a column. This method is automatically called after loading new child nodes.

    Parameters

    Returns void

    0.11.0

  • Re-apply current sorting if any (use after lazy load). Example:

    load: function (e) {
    // Whe loading a lazy branch, apply current sort order if any
    e.node.resort();
    },

    Parameters

    Returns void

    0.14.0

  • Activate this node, deactivate previous, send events, activate column and scroll into viewport.

    Parameters

    Returns Promise<void>

  • Add/remove one or more classes to <div class='wb-row'>.

    This also maintains node.classes, so the class will survive a re-render.

    Parameters

    • className: string | string[] | Set<string>

      one or more class names. Multiple classes can be passed as space-separated string, array of strings, or set of strings.

    • flag: boolean = true

    Returns void

  • Set keyboard focus here.

    Parameters

    • flag: boolean = true

    Returns void

  • Set a new icon path or class.

    Parameters

    • icon: string

    Returns void

  • Change node's key and/or refKey.

    Parameters

    • key: null | string
    • refKey: null | string

    Returns void

  • Rename this node.

    Parameters

    • title: string

    Returns void

  • Sort child list by title or custom criteria.

    Parameters

    • cmp: null | SortCallback = nodeTitleSorter

      custom compare function(a, b) that returns -1, 0, or 1 (defaults to sorting by title).

    • deep: boolean = false

      pass true to sort all descendant nodes recursively

    Returns void

    use sort

  • Start editing this node's title.

    Returns void

  • Internal

    Return readable string representation for this instance.

    Returns string

  • Trigger modifyChild event on node.parent(!).

    Parameters

    • operation: string

      Type of change: 'add', 'remove', 'rename', 'move', 'data', ...

    • Optionalextra: any

    Returns void

  • Trigger modifyChild event on a parent to signal that a child was modified.

    Parameters

    • operation: string

      Type of change: 'add', 'remove', 'rename', 'move', 'data', ...

    • child: null | WunderbaumNode
    • Optionalextra: any

    Returns void

  • Trigger a repaint, typically after a status or data change.

    change defaults to 'data', which handles modifcations of title, icon, and column content. It can be reduced to 'ChangeType.status' if only active/focus/selected state has changed.

    This method will eventually call WunderbaumNode._render with default options, but may be more consistent with the tree's Wunderbaum.update API.

    Parameters

    Returns void

  • Call callback(node) for all descendant nodes in hierarchical order (depth-first, pre-order).

    Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".
    Return false if iteration was stopped.

    Parameters

    • callback: NodeVisitCallback

      the callback function. Return false to stop iteration, return "skip" to skip this node and its children only.

    • includeSelf: boolean = false

    Returns NodeVisitResponse

  • Call fn(node) for all parent nodes, bottom-up, including invisible system root.
    Stop iteration, if callback() returns false.
    Return false if iteration was stopped.

    Parameters

    • callback: (node: WunderbaumNode) => boolean | void

      the callback function. Return false to stop iteration

    • includeSelf: boolean = false

    Returns boolean

  • Call fn(node) for all sibling nodes.
    Stop iteration, if fn() returns false.
    Return false if iteration was stopped.

    Parameters

    • callback: (node: WunderbaumNode) => boolean | void

      the callback function. Return false to stop iteration.

    • includeSelf: boolean = false

      include this node in the iteration.

    Returns boolean

================================================ FILE: docs/api/classes/wunderbaum.Wunderbaum.html ================================================ Wunderbaum | wunderbaum - v0.14.1

A persistent plain object or array.

See also WunderbaumOptions.

Constructors

Properties

_cellNavMode: boolean = false
_util: util = util

Expose some useful methods of the util.ts module as tree._util.

activeColIdx: number = 0

Use setColumn()/getActiveColElem() to access.

breadcrumb: null | HTMLElement = null

Filter options (used as defaults for calls to Wunderbaum.filterNodes )

columns: ColumnDefinitionList = []

List of column definitions.

data: { [key: string]: any } = {}

Contains additional data that was sent as response to an Ajax source load request.

element: HTMLDivElement

The div container element that was passed to the constructor.

filterMode: FilterModeType = null

Filter options (used as defaults for calls to Wunderbaum.filterNodes )

headerElement: HTMLDivElement

The div.wb-header element if any.

id: string

Unique tree ID as passed to constructor. Defaults to "wb_SEQUENCE".

lastQuicksearchTerm: string = ""
lastQuicksearchTime: number = 0
listContainerElement: HTMLDivElement

The div.wb-list-container element that contains the nodeListElement.

nodeListElement: HTMLDivElement

The div.wb-node-list element that contains all visible div.wb-row child elements.

Merged options from constructor args and tree- and extension defaults.

ready: Promise<any>

A Promise that is resolved when the tree was initialized (similar to init(e) event).

The invisible root node, that holds all visible top level nodes.

Shared properties, referenced by node.type.

iconMaps: { bootstrap: IconMapType; fontawesome6: IconMapType } = defaultIconMaps

A map of default iconMaps. May be used as default, when passing partial icon definition maps:

const tree = new mar10.Wunderbaum({
...
iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
folder: "bi bi-archive",
}),
});
util: util = util

Expose some useful methods of the util.ts module as Wunderbaum.util.

version: string = "@VERSION"

Wunderbaum release version number "MAJOR.MINOR.PATCH".

Accessors

  • get parent(): null

    Always returns null (so a tree instance behaves as tree.root).

    Returns null

Methods

  • Internal

    Calculate a stable, unique key for a node from its refKey (or title). We also add information from the parent, because a refKey may occur multiple times in a tree (but not as child of the same parent).

    Parameters

    Returns string

  • Call event handler if defined in tree or tree.EXTENSION options.

    Example:

    tree._callEvent("edit.beforeEdit", {foo: 42})
    

    Parameters

    • type: string
    • Optionalextra: any

    Returns any

  • Call tree method or extension method if defined.

    Example:

    tree._callMethod("edit.startEdit", "arg1", "arg2")
    

    Parameters

    • name: string
    • ...args: any[]

    Returns any

  • Internal

    Parameters

    Returns null | HTMLElement

  • Update column headers and column width. Return true if at least one column width changed.

    Returns boolean

  • Return the number of nodes in the data model.

    Parameters

    • visible: boolean = false

      if true, nodes that are hidden due to collapsed parents are ignored.

    Returns number

  • Return the number of unique nodes in the data model, i.e. unique node.refKey.

    Returns number

  • Clear nodes and markup and detach events and observers.

    This method may be useful to free up resources before re-creating a tree on an existing div, for example in unittest suites. Note that this Wunderbaum instance becomes unusable afterwards.

    Returns void

  • Disable render requests during operations that would trigger many updates.

    try {
    tree.enableUpdate(false);
    // ... (long running operation that would trigger many updates)
    foo();
    // ... NOTE: make sure that async operations have finished, e.g.
    await foo();
    } finally {
    tree.enableUpdate(true);
    }

    Parameters

    • flag: boolean

    Returns void

  • Dim or hide unmatched nodes.

    Parameters

    • filter: string | RegExp | NodeFilterCallback

      a string to match against node titles, or a callback function.

    • options: FilterNodesOptions

      filter options. Defaults to the tree.options.filter settings.

    Returns number

    the number of nodes that match the filter.

    tree.filterNodes("foo", {mode: 'dim', fuzzy: true});
    // or pass a callback
    tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'});
  • Return the active cell (span.wb-col) of the currently active node or null.

    Returns null | HTMLSpanElement

  • Return tree.option.NAME (also resolving if this is a callback).

    See also WunderbaumNode.getOption() to evaluate node.NAME setting and tree.types[node.type].NAME.

    Parameters

    • name: string

      option name (use dot notation to access extension option, e.g. filter.mode)

    • OptionaldefaultValue: any

    Returns any

  • Return an array of refKey values.

    RefKeys are unique identifiers for a node data, and are used to identify clones. If more than one node has the same refKey, it is only returned once.

    Parameters

    • selected: boolean = false

      if true, only return refKeys of selected nodes.

    Returns string[]

  • Return an array of selected nodes.

    Parameters

    • stopOnParents: boolean = false

      only return the topmost selected node (useful with selectMode 'hier')

    Returns WunderbaumNode[]

  • Return the topmost visible node in the viewport.

    Parameters

    • complete: boolean = true

      If false, the node is considered visible if at least one pixel is visible.

    Returns WunderbaumNode

  • Return true if the tree (or one of its nodes) has the input focus.

    Returns boolean

  • Return true if the tree displays a header. Grids have a header unless the header option is set to false. Plain trees have a header if the header option is a string or true.

    Returns boolean

  • Return true if cell-navigation mode is active.

    Returns boolean

  • Return true if a filter is currently applied.

    Returns boolean

  • Return true if tree has more than one column, i.e. has additional data columns.

    Returns boolean

  • Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.

    Returns boolean

  • Return true if row-navigation mode is active.

    Returns boolean

  • Reload the tree with a new source.

    Previous data is cleared. Note that also column- and type defintions may be passed with the source object.

    Parameters

    Returns Promise<void>

    Wunderbaum.reload for a shortcut to reload the last ajax request and restore the previous state.

  • Write to console.log with tree name as prefix if opts.debugLevel >= 4.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.debug with tree name as prefix if opts.debugLevel >= 4. and browser console level includes debug/verbose messages.

    Parameters

    • ...args: any[]

    Returns void

    log

  • Write to console.error with tree name as prefix.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.info with tree name as prefix if opts.debugLevel >= 3.

    Parameters

    • ...args: any[]

    Returns void

  • Write to console.warn with tree name as prefix with if opts.debugLevel >= 2.

    Parameters

    • ...args: any[]

    Returns void

  • Run code, but defer rendering of viewport until done.

    const res = tree.runWithDeferredUpdate(() => {
    return someFunctionThatWouldUpdateManyNodes();
    });

    Type Parameters

    • T

    Parameters

    Returns T

  • Run code, but defer rendering of viewport until done.

    const res = await tree.runWithDeferredUpdate(async () => {
    return someAsyncFunctionThatWouldUpdateManyNodes();
    });

    Type Parameters

    • T

    Parameters

    • func: () => Promise<T>

    Returns Promise<T>

  • Set the tree's navigation mode.

    Parameters

    • flag: boolean = true

    Returns void

  • Set column #colIdx to 'active'.

    This higlights the column header and -cells by adding the wb-active class to all grid cells of the active column.
    Available in cell-nav mode only.

    If options.edit is true, the embedded input element is focused, or if colIdx is 0, the node title is put into edit mode.

    Parameters

    Returns void

  • Disable mouse and keyboard interaction (return prev. state).

    Parameters

    • flag: boolean = true

    Returns boolean

  • Set or remove keyboard focus to the tree container.

    Parameters

    • flag: boolean = true

    Returns void

  • Set tree option. Use dot notation to set plugin option, e.g. "filter.mode".

    Parameters

    • name: string
    • value: any

    Returns void

  • Add or redefine node type definitions.

    Parameters

    • types: any
    • replace: boolean = true

    Returns void

  • Sort nodes list by title or custom criteria.

    Parameters

    • cmp: null | SortCallback = nodeTitleSorter

      custom compare function(a, b) that returns -1, 0, or 1 (defaults to sorting by title).

    • deep: boolean = false

      pass true to sort all descendant nodes recursively

    Returns void

    use sort

  • Internal

    Return readable string representation for this instance.

    Returns string

  • Render pending changes that were scheduled using WunderbaumNode.update if any.

    This is hardly ever neccessary, since we normally either

    • call update(ChangeType.TYPE) (async, throttled), or
    • call update(ChangeType.TYPE, {immediate: true}) (synchronous)

    updatePendingModifications() will only force immediate execution of pending async changes if any.

    Returns void

  • Call callback(node) for all nodes in vertical order, top down (or bottom up).

    Note that this considers expansion state, i.e. filtered nodes and children of collapsed nodes are skipped, unless includeHidden is set.

    Stop iteration if callback() returns false.
    Return false if iteration was stopped.

    Parameters

    Returns boolean

    false if iteration was canceled

  • Return a {node: WunderbaumNode, region: TYPE} object for a mouse event.

    Parameters

    • event: Event

      Mouse event, e.g. click, ...

    Returns WbEventInfo

    Return a {node: WunderbaumNode, region: TYPE} object TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined

  • Return a Wunderbaum instance, from element, id, index, or event.

    getTree();         // Get first Wunderbaum instance on page
    getTree(1); // Get second Wunderbaum instance on page
    getTree(event); // Get tree for this mouse- or keyboard event
    getTree("foo"); // Get tree for this `tree.options.id`
    getTree("#tree"); // Get tree for first matching element selector

    Parameters

    Returns null | Wunderbaum

================================================ FILE: docs/api/enums/types.ChangeType.html ================================================ ChangeType | wunderbaum - v0.14.1

Enumeration ChangeType

Possible values for WunderbaumNode.update and Wunderbaum.update.

Enumeration Members

any: "any"

Re-render the whole viewport, headers, and all rows.

colStructure: "colStructure"

The tree.columns definition has changed beyond simple width adjustments.

data: "data"

A node's title, icon, columns, or status have changed. Update the existing row markup.

resize: "resize"

The viewport/window was resized. Adjust layout attributes for all elements.

row: "row"

A node's definition has changed beyond status and data. Re-render the whole row's markup.

scroll: "scroll"

Vertical scroll event. Update the 'top' property of all rows.

status: "status"

A node's status has changed. Update current row's classes, to reflect active, selected, ...

structure: "structure"

Nodes have been added, removed, etc. Update markup.

================================================ FILE: docs/api/enums/types.NavModeEnum.html ================================================ NavModeEnum | wunderbaum - v0.14.1

Enumeration NavModeEnum

Initial navigation mode and possible transition.

Enumeration Members

Enumeration Members

cell: "cell"

Cell-nav mode only

row: "row"

Row mode only

startCell: "startCell"

Start in cell-nav mode, but allow row mode

startRow: "startRow"

Start with row mode, but allow cell-nav mode

================================================ FILE: docs/api/enums/types.NodeRegion.html ================================================ NodeRegion | wunderbaum - v0.14.1

Enumeration NodeRegion

Define the subregion of a node, where an event occurred.

Enumeration Members

checkbox: "checkbox"
column: "column"
expander: "expander"
icon: "icon"
prefix: "prefix"
title: "title"
unknown: ""
================================================ FILE: docs/api/enums/types.NodeStatusType.html ================================================ NodeStatusType | wunderbaum - v0.14.1

Enumeration NodeStatusType

Possible values for WunderbaumNode.setStatus.

Enumeration Members

Enumeration Members

error: "error"
loading: "loading"
noData: "noData"
ok: "ok"
paging: "paging"
================================================ FILE: docs/api/enums/types.RenderFlag.html ================================================ RenderFlag | wunderbaum - v0.14.1

Enumeration RenderFlagInternal

Enumeration Members

Enumeration Members

clearMarkup: "clearMarkup"
header: "header"
redraw: "redraw"
scroll: "scroll"
================================================ FILE: docs/api/functions/common.decompressSourceData.html ================================================ decompressSourceData | wunderbaum - v0.14.1

Function decompressSourceData

  • Decompresses the source data by

    • converting from 'flat' to 'nested' format
    • expanding short alias names to long names (if defined in _keyMap)
    • resolving value indexes to value strings (if defined in _valueMap)

    Parameters

    Returns void

    void

================================================ FILE: docs/api/functions/common.makeNodeTitleMatcher.html ================================================ makeNodeTitleMatcher | wunderbaum - v0.14.1

Function makeNodeTitleMatcher

================================================ FILE: docs/api/functions/common.makeNodeTitleStartMatcher.html ================================================ makeNodeTitleStartMatcher | wunderbaum - v0.14.1

Function makeNodeTitleStartMatcher

================================================ FILE: docs/api/functions/common.nodeTitleSorter.html ================================================ nodeTitleSorter | wunderbaum - v0.14.1

Function nodeTitleSorter

================================================ FILE: docs/api/functions/util.adaptiveThrottle.html ================================================ adaptiveThrottle | wunderbaum - v0.14.1

Function adaptiveThrottle

  • Return a function that can be called instead of callback, but guarantees a limited execution rate. The execution rate is calculated based on the runtime duration of the previous call. Example:

    throttledFoo = util.adaptiveThrottle(foo.bind(this), {});
    throttledFoo();
    throttledFoo();

    Parameters

    • this: unknown
    • callback: (...args: any[]) => void
    • options: object

    Returns DebouncedFunction<(...args: any[]) => void>

================================================ FILE: docs/api/functions/util.assert.html ================================================ assert | wunderbaum - v0.14.1

Function assert

  • Throw an Error if cond is falsey.

    Parameters

    • cond: any
    • msg: string

    Returns void

================================================ FILE: docs/api/functions/util.debounce.html ================================================ debounce | wunderbaum - v0.14.1

Function debounce

  • Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked, or until the next browser frame is drawn. The debounced function comes with a cancel method to cancel delayed func invocations and a flush method to immediately invoke them. Provide options to indicate whether func should be invoked on the leading and/or trailing edge of the wait timeout. The func is invoked with the last arguments provided to the debounced function. Subsequent calls to the debounced function return the result of the last func invocation.

    Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only if the debounced function is invoked more than once during the wait timeout.

    If wait is 0 and leading is false, func invocation is deferred until the next tick, similar to setTimeout with a timeout of 0.

    If wait is omitted in an environment with requestAnimationFrame, func invocation will be deferred until the next frame is drawn (typically about 16ms).

    See David Corbacho's article for details over the differences between debounce and throttle.

    Type Parameters

    • F extends Procedure

    Parameters

    • func: F

      The function to debounce.

    • Optionalwait: number = 0

      The number of milliseconds to delay; if omitted, requestAnimationFrame is used (if available).

    • Optionaloptions: DebounceOptions = {}

      The options object.

    Returns DebouncedFunction<F>

    Returns the new debounced function.

    0.1.0

    // Avoid costly calculations while the window size is in flux.
    jQuery(window).on('resize', debounce(calculateLayout, 150))

    // Invoke `sendMail` when clicked, debouncing subsequent calls.
    jQuery(element).on('click', debounce(sendMail, 300, {
    'leading': true,
    'trailing': false
    }))

    // Ensure `batchLog` is invoked once after 1 second of debounced calls.
    const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
    const source = new EventSource('/stream')
    jQuery(source).on('message', debounced)

    // Cancel the trailing debounced invocation.
    jQuery(window).on('popstate', debounced.cancel)

    // Check for pending invocations.
    const status = debounced.pending() ? "Pending..." : "Ready"
================================================ FILE: docs/api/functions/util.documentReady.html ================================================ documentReady | wunderbaum - v0.14.1

Function documentReady

  • Run callback when document was loaded.

    Parameters

    • callback: () => void

    Returns void

================================================ FILE: docs/api/functions/util.documentReadyPromise.html ================================================ documentReadyPromise | wunderbaum - v0.14.1

Function documentReadyPromise

  • Resolve when document was loaded.

    Returns Promise<void>

================================================ FILE: docs/api/functions/util.each.html ================================================ each | wunderbaum - v0.14.1

Function each

  • Iterate over Object properties or array elements.

    Parameters

    • obj: any

      Object, Array or null

    • callback: (index: string | number, item: any) => boolean | void

      called for every item. this also contains the item. Return false to stop the iteration.

    Returns any

================================================ FILE: docs/api/functions/util.elemFromHtml.html ================================================ elemFromHtml | wunderbaum - v0.14.1

Function elemFromHtml

  • Create and return an unconnected HTMLElement from a HTML string.

    Type Parameters

    • T = HTMLElement

    Parameters

    • html: string

    Returns T

================================================ FILE: docs/api/functions/util.elemFromSelector.html ================================================ elemFromSelector | wunderbaum - v0.14.1

Function elemFromSelector

  • Return a HtmlElement from selector or cast an existing element.

    Type Parameters

    • T = HTMLElement

    Parameters

    • obj: string | T

    Returns null | T

================================================ FILE: docs/api/functions/util.error.html ================================================ error | wunderbaum - v0.14.1

Function error

  • Shortcut for throw new Error(msg).

    Parameters

    • msg: string

    Returns void

================================================ FILE: docs/api/functions/util.escapeHtml.html ================================================ escapeHtml | wunderbaum - v0.14.1

Function escapeHtml

  • Convert <, >, &, ", ', and / to the equivalent entities.

    Parameters

    • s: string

    Returns string

================================================ FILE: docs/api/functions/util.escapeRegex.html ================================================ escapeRegex | wunderbaum - v0.14.1

Function escapeRegex

  • Convert a regular expression string by escaping special characters (e.g. "$" -> "\$")

    Parameters

    • s: string

    Returns string

================================================ FILE: docs/api/functions/util.escapeTooltip.html ================================================ escapeTooltip | wunderbaum - v0.14.1

Function escapeTooltip

  • Convert <, >, ", ', and / (but not &) to the equivalent entities.

    Parameters

    • s: string

    Returns string

================================================ FILE: docs/api/functions/util.eventToString.html ================================================ eventToString | wunderbaum - v0.14.1

Function eventToString

  • Return a canonical descriptive string for a keyboard or mouse event.

    The result also contains a prefix for modifiers if any, for example "x", "F2", "Control+Home", or "Shift+clickright". This is especially useful in switch statements, to make sure that modifier keys are considered and handled correctly:

      const eventName = util.eventToString(e);
    switch (eventName) {
    case "+":
    case "Add":
    ...
    break;
    case "Enter":
    case "End":
    case "Control+End":
    case "Meta+ArrowDown":
    case "PageDown":
    ...
    break;
    }

    Parameters

    • event: Event

    Returns string

================================================ FILE: docs/api/functions/util.extend.html ================================================ extend | wunderbaum - v0.14.1

Function extend

  • Copy allproperties from one or more source objects to a target object.

    Parameters

    • ...args: any[]

    Returns any

    the modified target object.

================================================ FILE: docs/api/functions/util.extractHtmlText.html ================================================ extractHtmlText | wunderbaum - v0.14.1

Function extractHtmlText

  • TODO

    Parameters

    • s: string

    Returns string

================================================ FILE: docs/api/functions/util.getOption.html ================================================ getOption | wunderbaum - v0.14.1

Function getOption

  • Return opts.NAME if opts is valid and

    Parameters

    • opts: any

      dict, object, or null

    • name: string

      option name (use dot notation to access extension option, e.g. filter.mode)

    • defaultValue: any = undefined

      returned when opts is not an object, or does not have a NAME property

    Returns any

================================================ FILE: docs/api/functions/util.getValueFromElem.html ================================================ getValueFromElem | wunderbaum - v0.14.1

Function getValueFromElem

  • Read the value from an HTML input element.

    If a <span class="wb-col"> is passed, the first child input is used. Depending on the target element type, value is interpreted accordingly. For example for a checkbox, a value of true, false, or null is returned if the element is checked, unchecked, or indeterminate. For datetime input control a numerical value is assumed, etc.

    Common use case: store the new user input in a change event handler:

      change: (e) => {
    const tree = e.tree;
    const node = e.node;
    // Read the value from the input control that triggered the change event:
    let value = tree.getValueFromElem(e.element);
    // and store it to the node model (assuming the column id matches the property name)
    node.data[e.info.colId] = value;
    },

    Parameters

    • elem: HTMLElement

      <input> or <select> element. Also a parent span.wb-col is accepted.

    • coerce: boolean = false

      pass true to convert date/time inputs to Date.

    Returns any

    the value

================================================ FILE: docs/api/functions/util.intToBool.html ================================================ intToBool | wunderbaum - v0.14.1

Function intToBool

  • Return val unless val is a number in which case we convert to boolean. This is useful when a boolean value is stored as a 0/1 (e.g. in JSON) and we still want to maintain string values. null and undefined are returned as is. E.g. checkbox may be boolean or 'radio'.

    Parameters

    • val: undefined | string | number | boolean

    Returns undefined | string | boolean

================================================ FILE: docs/api/functions/util.isArray.html ================================================ isArray | wunderbaum - v0.14.1

Function isArray

  • Return true if obj is of type array.

    Parameters

    • obj: any

    Returns obj is any[]

================================================ FILE: docs/api/functions/util.isEmptyObject.html ================================================ isEmptyObject | wunderbaum - v0.14.1

Function isEmptyObject

  • Return true if obj is of type Object and has no properties.

    Parameters

    • obj: any

    Returns boolean

================================================ FILE: docs/api/functions/util.isFunction.html ================================================ isFunction | wunderbaum - v0.14.1

Function isFunction

  • Return true if obj is of type function.

    Parameters

    • obj: any

    Returns boolean

================================================ FILE: docs/api/functions/util.isPlainObject.html ================================================ isPlainObject | wunderbaum - v0.14.1

Function isPlainObject

  • Return true if obj is of type Object.

    Parameters

    • obj: any

    Returns boolean

================================================ FILE: docs/api/functions/util.murmurHash3.html ================================================ murmurHash3 | wunderbaum - v0.14.1

Function murmurHash3

  • MurmurHash3 implementation for strings.

    Parameters

    • key: string

      The input string to hash.

    • asString: boolean = true

      Optional convert result to zero-padded string of 8 characters.

    • seed: number = 0

      Optional seed value.

    Returns string | number

    A 32-bit hash as a number or string.

================================================ FILE: docs/api/functions/util.noop.html ================================================ noop | wunderbaum - v0.14.1

Function noop

  • A dummy function that does nothing ('no operation').

    Parameters

    • ...args: any[]

    Returns any

================================================ FILE: docs/api/functions/util.onEvent.html ================================================ onEvent | wunderbaum - v0.14.1

Function onEvent

  • Bind one or more event handlers directly to an EventTarget.

    Parameters

    • rootTarget: string | EventTarget

      EventTarget or selector

    • eventNames: string
    • handler: EventCallbackType

    Returns void

  • Bind one or more event handlers using event delegation.

    E.g. handle all 'input' events for input and textarea elements of a given form:

    onEvent("#form_1", "input", "input,textarea", function (e: Event) {
    console.log(e.type, e.target);
    });

    Parameters

    • rootTarget: string | EventTarget

      EventTarget or selector

    • eventNames: string
    • selector: string
    • handler: EventCallbackType

    Returns void

================================================ FILE: docs/api/functions/util.overrideMethod.html ================================================ overrideMethod | wunderbaum - v0.14.1

Function overrideMethod

  • Return a wrapped handler method, that provides this._super and this._superApply.

     // Implement `opts.createNode` event to add the 'draggable' attribute
    overrideMethod(ctx.options, "createNode", (event, data) => {
    // Default processing if any
    this._super.apply(this, event, data);
    // Add 'draggable' attribute
    data.node.span.draggable = true;
    });

    Parameters

    • instance: any
    • methodName: string
    • handler: FunctionType
    • Optionalctx: any

    Returns void

================================================ FILE: docs/api/functions/util.rotate.html ================================================ rotate | wunderbaum - v0.14.1

Function rotate

  • Return the next value from a list of values (rotating).

    Parameters

    • value: any
    • values: any[]

    Returns any

    0.11

================================================ FILE: docs/api/functions/util.setElemDisplay.html ================================================ setElemDisplay | wunderbaum - v0.14.1

Function setElemDisplay

  • Show/hide element by setting the display style to 'none'.

    Parameters

    • elem: string | HTMLElement
    • flag: boolean

    Returns void

================================================ FILE: docs/api/functions/util.setTimeoutPromise.html ================================================ setTimeoutPromise | wunderbaum - v0.14.1

Function setTimeoutPromise

  • Run function after ms milliseconds and return a promise that resolves when done.

    Type Parameters

    • T = unknown

    Parameters

    • this: unknown
    • callback: (...args: any[]) => T
    • ms: number

    Returns Promise<T>

================================================ FILE: docs/api/functions/util.setValueToElem.html ================================================ setValueToElem | wunderbaum - v0.14.1

Function setValueToElem

  • Set the value of an HTML input element.

    If a <span class="wb-col"> is passed, the first child input is used. Depending on the target element type, value is interpreted accordingly. For example a checkbox is set to checked, unchecked, or indeterminate if the value is truethy, falsy, or null. For datetime input control a numerical value is assumed, etc.

    Common use case: update embedded input controls in a render event handler:

      render: (e) => {
    // e.node.log(e.type, e, e.node.data);

    for (const col of Object.values(e.renderColInfosById)) {
    switch (col.id) {
    default:
    // Assumption: we named column.id === node.data.NAME
    util.setValueToElem(col.elem, e.node.data[col.id]);
    break;
    }
    }
    },

    Parameters

    • elem: HTMLElement

      <input> or <select> element Also a parent span.wb-col is accepted.

    • value: any

      a value that matches the target element.

    Returns void

================================================ FILE: docs/api/functions/util.sleep.html ================================================ sleep | wunderbaum - v0.14.1

Function sleep

  • Wait ms microseconds.

    Example:

    await sleep(1000);
    

    Parameters

    • ms: number

      duration

    Returns Promise<unknown>

================================================ FILE: docs/api/functions/util.throttle.html ================================================ throttle | wunderbaum - v0.14.1

Function throttle

  • Creates a throttled function that only invokes func at most once per every wait milliseconds (or once per browser frame). The throttled function comes with a cancel method to cancel delayed func invocations and a flush method to immediately invoke them. Provide options to indicate whether func should be invoked on the leading and/or trailing edge of the wait timeout. The func is invoked with the last arguments provided to the throttled function. Subsequent calls to the throttled function return the result of the last func invocation.

    Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only if the throttled function is invoked more than once during the wait timeout.

    If wait is 0 and leading is false, func invocation is deferred until the next tick, similar to setTimeout with a timeout of 0.

    If wait is omitted in an environment with requestAnimationFrame, func invocation will be deferred until the next frame is drawn (typically about 16ms).

    See David Corbacho's article for details over the differences between throttle and debounce.

    Type Parameters

    • F extends Procedure

    Parameters

    • func: F

      The function to throttle.

    • Optionalwait: number = 0

      The number of milliseconds to throttle invocations to; if omitted, requestAnimationFrame is used (if available).

    • Optionaloptions: ThrottleOptions = {}

      The options object.

    Returns DebouncedFunction<F>

    Returns the new throttled function.

    0.1.0

    // Avoid excessively updating the position while scrolling.
    jQuery(window).on('scroll', throttle(updatePosition, 100))

    // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
    const throttled = throttle(renewToken, 300000, { 'trailing': false })
    jQuery(element).on('click', throttled)

    // Cancel the trailing throttled invocation.
    jQuery(window).on('popstate', throttled.cancel)
================================================ FILE: docs/api/functions/util.toBool.html ================================================ toBool | wunderbaum - v0.14.1

Function toBool

  • Return the the boolean value of the first non-null element. Example:

    const opts = { flag: true };
    const value = util.toBool(opts.foo, opts.flag, false); // returns true

    Parameters

    • ...boolDefaults: (undefined | null | boolean)[]

    Returns boolean

================================================ FILE: docs/api/functions/util.toPixel.html ================================================ toPixel | wunderbaum - v0.14.1

Function toPixel

  • Convert a pixel string to number. We accept a number or a string like '123px'. If undefined, the first default value that is a number or a string ending with 'px' is returned.

    Example:

    let x = undefined;
    let y = "123px";
    const width = util.toPixel(x, y, 100); // returns 123

    Parameters

    • ...defaults: (undefined | null | string | number)[]

    Returns number

================================================ FILE: docs/api/functions/util.toSet.html ================================================ toSet | wunderbaum - v0.14.1

Function toSet

  • Convert an Array or space-separated string to a Set.

    Parameters

    • val: any

    Returns Set<string>

================================================ FILE: docs/api/functions/util.toggleCheckbox.html ================================================ toggleCheckbox | wunderbaum - v0.14.1

Function toggleCheckbox

  • Set or rotate checkbox status with support for tri-state.

    An initial 'indeterminate' state becomes 'checked' on the first call.

    If the input element has the class 'wb-tristate' assigned, the sequence is:
    'indeterminate' -> 'checked' -> 'unchecked' -> 'indeterminate' -> ...
    Otherwise we toggle like
    'checked' -> 'unchecked' -> 'checked' -> ...

    Parameters

    • element: string | HTMLElement
    • Optionalvalue: null | boolean
    • Optionaltristate: boolean

    Returns void

================================================ FILE: docs/api/functions/util.type.html ================================================ type | wunderbaum - v0.14.1

Function type

  • Return a canonical string representation for an object's type (e.g. 'array', 'number', ...).

    Parameters

    • obj: any

    Returns string

================================================ FILE: docs/api/functions/util.unsafeCast.html ================================================ unsafeCast | wunderbaum - v0.14.1

Function unsafeCast

  • Cast any value to .

    Type Parameters

    • T

    Parameters

    • value: any

    Returns T

================================================ FILE: docs/api/hierarchy.html ================================================ wunderbaum - v0.14.1
================================================ FILE: docs/api/index.html ================================================ wunderbaum - v0.14.1

wunderbaum - v0.14.1

wunderbaum

GitHub version Node.js CI npm jsDelivr Released with: grunt-yabs StackOverflow: wunderbaum

A modern tree/treegrid control for the web.

Designated successor of Fancytree.
See the upgrade guide for details.

Demo

  • Supports drag and drop, editing, filtering, sorting, and multi-selection.
  • Written in TypeScript, transpiled to ES6 (esm & umd).
  • Performant handling of big data structures.
  • Provide an object oriented API.
  • Framework agnostic.
  • Zero dependencies.
  • Keyboard support.
================================================ FILE: docs/api/interfaces/types.AddChildrenOptions.html ================================================ AddChildrenOptions | wunderbaum - v0.14.1

Interface AddChildrenOptions

Possible values for WunderbaumNode.addChildren.

interface AddChildrenOptions {
    _level?: number;
    applyMinExpanLevel?: boolean;
    before?: null | number | WunderbaumNode;
}

Properties

_level?: number

(@internal Internal use, do not set! )

applyMinExpanLevel?: boolean

Set node.expanded = true according to tree.options.minExpandLevel. This does not load lazy nodes.

true
before?: null | number | WunderbaumNode

Insert children before this node (or index)

undefined or null:  append as last child
================================================ FILE: docs/api/interfaces/types.ApplyCommandOptions.html ================================================ ApplyCommandOptions | wunderbaum - v0.14.1

Interface ApplyCommandOptions

Indexable

  • [key: string]: unknown
================================================ FILE: docs/api/interfaces/types.ColumnDefinition.html ================================================ ColumnDefinition | wunderbaum - v0.14.1

Interface ColumnDefinition

Column type definitions.

interface ColumnDefinition {
    _ofsPx?: number;
    _weight?: number;
    _widthPx?: number;
    classes?: string;
    customWidthPx?: number;
    filterable?: boolean;
    filterActive?: boolean;
    headerClasses?: string;
    html?: string;
    id: string;
    menu?: boolean;
    minWidth?: string | number;
    resizable?: boolean;
    sortable?: boolean;
    sortOrder?: SortOrderType;
    title: string;
    tooltip?: string;
    width?: string | number;
    [key: string]: unknown;
}

Indexable

  • [key: string]: unknown

Properties

_ofsPx?: number
_weight?: number
_widthPx?: number
classes?: string

Optional class names that are added to all span.wb-col header AND data elements of that column. Separate multiple classes with space.

customWidthPx?: number

Optional custom column width when user resized by mouse drag. Default: unset.

filterable?: boolean

Display a 'filter' button in the column header. Default: false.
Note: The actual filtering must be implemented in the buttonClick() event.

false (or global tree option columnsFilterable)

0.11.0

filterActive?: boolean

. Default: inactive.
Note: The actual filtering must be implemented in the buttonClick() event.

headerClasses?: string

If headerClasses is a set, it will be used for the header element only (unlike classes, which is used for body and header cells). Separate multiple classes with space.

html?: string

Optional HTML content that is rendered into all span.wb-col elements of that column.

id: string

Column ID as defined in tree.columns definition ("*" for title column).

menu?: boolean

Display a menu icon that may open a context menu for this column. Note: The actual functionality must be implemented in the buttonClick() event.

false (or global tree option columnsMenu)

0.11.0

minWidth?: string | number

Only used for columns with a relative weight. Default: 4px.

resizable?: boolean

Allow user to resize the column.

false (or global tree option columnsSortable)

0.10.0

sortable?: boolean

Display a 'sort' button in the column header. Default: false.
Note: The actual sorting must be implemented in the buttonClick() event.

false (or global tree option columnsSortable)

0.11.0

sortOrder?: SortOrderType

Optional custom column sort orde when user clicked the sort icon. Default: unset, e.g. not sorted.
Note: The actual sorting must be implemented in the buttonClick() event.

0.11.0

title: string

Column header (defaults to id)

tooltip?: string

Column header tooltip (optional)

width?: string | number

Column width or weight. Either an absolute pixel value (e.g. "50px") or a relative weight (e.g. 1) that is used to calculate the width inside the remaining available space. Default: "*", which is interpreted as 1.

================================================ FILE: docs/api/interfaces/types.ColumnEventInfo.html ================================================ ColumnEventInfo | wunderbaum - v0.14.1

Interface ColumnEventInfo

Column information (passed to the render event).

interface ColumnEventInfo {
    elem: null | HTMLSpanElement;
    id: string;
    idx: number;
    info: ColumnDefinition;
}

Properties

Properties

elem: null | HTMLSpanElement

The cell's <span class='wb-col'> element (null for plain trees).

id: string

Column ID as defined in tree.columns definition ("*" for title column).

idx: number

Column index (0: leftmost title column).

The value of tree.columns[] for the current index.

================================================ FILE: docs/api/interfaces/types.DragEventType.html ================================================ DragEventType | wunderbaum - v0.14.1

Interface DragEventType

interface DragEventType {
    event: DragEvent;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event: DragEvent

The original event.

The source node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.DropEventType.html ================================================ DropEventType | wunderbaum - v0.14.1

Interface DropEventType

interface DropEventType {
    dataTransfer: DataTransfer;
    event: DragEvent;
    node: WunderbaumNode;
    sourceNode: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

dataTransfer: DataTransfer

The DataTransfer object.

event: DragEvent

The original event.

The target node.

sourceNode: WunderbaumNode

The source node if any.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.ExpandAllOptions.html ================================================ ExpandAllOptions | wunderbaum - v0.14.1

Interface ExpandAllOptions

interface ExpandAllOptions {
    collapseOthers?: boolean;
    deep?: boolean;
    depth?: number;
    force?: boolean;
    keepActiveNodeVisible?: boolean;
    loadLazy?: boolean;
    resetLazy?: boolean;
}

Properties

collapseOthers?: boolean

Expand up to level=depth and collapse all other branches. Only in combination with flag == true, depth > 0.

false
deep?: boolean

Also collapse child nodes beyond the depth level. Otherwise only the depth level is collapsed and the expand state of the descendants is retained. Only in combination with collapse and depth. Expanding with deep option is not supported as recursion depth implied by the depth option. However a deep option will be considered if collapseOthers is set.

false
depth?: number

Restrict expand level. Pass 0 to make only toplevel nodes visible, 1 to expand one level deeper, etc.

unset (unlimited)
force?: boolean

Ignore tree's minExpandLevel option

false
keepActiveNodeVisible?: boolean

Keep active node visible

true
loadLazy?: boolean

Expand and load lazy nodes

false
resetLazy?: boolean

Unload lazily loaded children if any (if collapsing).

false
================================================ FILE: docs/api/interfaces/types.FilterConnectType.html ================================================ FilterConnectType | wunderbaum - v0.14.1

Interface FilterConnectTypeExperimental

Passed as tree option.filer.connect to configure automatic integration of filter UI controls.

interface FilterConnectType {
    inputElem: null | string | HTMLInputElement;
    matchInfoElem?: null | string | HTMLElement;
    modeButton?: null | string | HTMLButtonElement;
    nextButton?: null | string | HTMLAnchorElement | HTMLButtonElement;
    prevButton?: null | string | HTMLAnchorElement | HTMLButtonElement;
}

Properties

inputElem: null | string | HTMLInputElement
matchInfoElem?: null | string | HTMLElement
modeButton?: null | string | HTMLButtonElement
nextButton?: null | string | HTMLAnchorElement | HTMLButtonElement
prevButton?: null | string | HTMLAnchorElement | HTMLButtonElement
================================================ FILE: docs/api/interfaces/types.FilterNodesOptions.html ================================================ FilterNodesOptions | wunderbaum - v0.14.1

Interface FilterNodesOptions

Possible option values for Wunderbaum.filterNodes. The defaults are inherited from the tree instances ´tree.options.filter` settings (see also FilterOptionsType).

interface FilterNodesOptions {
    autoExpand?: boolean;
    fuzzy?: boolean;
    hideExpanders?: boolean;
    highlight?: boolean;
    leavesOnly?: boolean;
    matchBranch?: boolean;
    mode?: FilterModeType;
    noData?: string | boolean;
}

Properties

autoExpand?: boolean

Expand all branches that contain matches while filtered

false
fuzzy?: boolean

Match single characters in order, e.g. 'fb' will match 'FooBar'

false
hideExpanders?: boolean

Hide expanders if all child nodes are hidden by filter

false
highlight?: boolean

Highlight matches by wrapping inside <mark> tags. Does not work for filter callbacks.

true
leavesOnly?: boolean

Match end nodes only

false
matchBranch?: boolean

Whether to implicitly match all children of matched nodes

false

Grayout unmatched nodes (pass 'hide' to remove instead)

'dim'
noData?: string | boolean

Display a 'no data' status node if result is empty

true
================================================ FILE: docs/api/interfaces/types.GetStateOptions.html ================================================ GetStateOptions | wunderbaum - v0.14.1

Interface GetStateOptions

Possible values for Wunderbaum.getState.

interface GetStateOptions {
    activeKey?: boolean;
    expandedKeys?: boolean;
    selectedKeys?: boolean;
}

Properties

activeKey?: boolean

Include the active node's key (and expand its parents).

true
expandedKeys?: boolean

Include the expanded keys.

false
selectedKeys?: boolean

Include the selected keys.

false
================================================ FILE: docs/api/interfaces/types.IconMapType.html ================================================ IconMapType | wunderbaum - v0.14.1

Interface IconMapType

A plain object (dictionary) that defines node icons.

interface IconMapType {
    checkChecked: string;
    checkUnchecked: string;
    checkUnknown: string;
    colFilter: string;
    colFilterActive: string;
    colMenu: string;
    colSortable: string;
    colSortAsc: string;
    colSortDesc: string;
    doc: string;
    error: string;
    expanderCollapsed: string;
    expanderExpanded: string;
    expanderLazy: string;
    folder: string;
    folderLazy: string;
    folderOpen: string;
    loading: string;
    noData: string;
    radioChecked: string;
    radioUnchecked: string;
    radioUnknown: string;
    [key: string]: string;
}

Indexable

  • [key: string]: string

Properties

checkChecked: string
checkUnchecked: string
checkUnknown: string
colFilter: string
colFilterActive: string
colMenu: string
colSortable: string
colSortAsc: string
colSortDesc: string
doc: string
error: string
expanderCollapsed: string
expanderExpanded: string
expanderLazy: string
folder: string
folderLazy: string
folderOpen: string
loading: string
noData: string
radioChecked: string
radioUnchecked: string
radioUnknown: string
================================================ FILE: docs/api/interfaces/types.LoadLazyNodesOptions.html ================================================ LoadLazyNodesOptions | wunderbaum - v0.14.1

Interface LoadLazyNodesOptions

Possible values for Wunderbaum.loadLazyNodes options argument.

interface LoadLazyNodesOptions {
    expand?: boolean;
    force?: boolean;
    noEvents?: boolean;
}

Properties

expand?: boolean

Expand node (otherwise load, but keep collapsed).

true
force?: boolean

Force reloading even if already loaded.

false
noEvents?: boolean

Do not send events.

false
================================================ FILE: docs/api/interfaces/types.MakeVisibleOptions.html ================================================ MakeVisibleOptions | wunderbaum - v0.14.1

Interface MakeVisibleOptions

Possible values for WunderbaumNode.makeVisible.

interface MakeVisibleOptions {
    noAnimation?: boolean;
    noEvents?: boolean;
    scrollIntoView?: boolean;
}

Properties

noAnimation?: boolean

Do not animate expand (currently not implemented).

false
noEvents?: boolean

Do not send events.

false
scrollIntoView?: boolean

Scroll node into visible viewport area if required.

true
================================================ FILE: docs/api/interfaces/types.NavigateOptions.html ================================================ NavigateOptions | wunderbaum - v0.14.1

Interface NavigateOptions

Possible values for WunderbaumNode.navigate.

interface NavigateOptions {
    activate?: boolean;
    event?: Event;
}

Properties

Properties

activate?: boolean

Activate the new node (otherwise focus only).

true
event?: Event

Originating event (e.g. KeyboardEvent) if any.

================================================ FILE: docs/api/interfaces/types.NodeTypeDefinition.html ================================================ NodeTypeDefinition | wunderbaum - v0.14.1

Interface NodeTypeDefinition

Contains the node's type information, i.e. tree.types[node.type] if defined.

Wunderbaum.types and WunderbaumNode.getOption() to evaluate node.NAME setting and tree.types[node.type].NAME.

interface NodeTypeDefinition {
    checkbox?: CheckboxOption;
    classes?: string;
    colspan?: boolean;
    icon?: IconOption;
    iconTooltip?: TooltipOption;
    [key: string]: unknown;
}

Indexable

  • [key: string]: unknown

Properties

checkbox?: CheckboxOption

En/disable checkbox for matching nodes.

classes?: string

Optional class names that are added to all div.wb-row elements of matching nodes.

colspan?: boolean

Only show title and hide other columns if any.

icon?: IconOption

Default icon for matching nodes.

iconTooltip?: TooltipOption

Default icon tooltip for matching nodes.

================================================ FILE: docs/api/interfaces/types.ReloadOptions.html ================================================ ReloadOptions | wunderbaum - v0.14.1

Interface ReloadOptions

Possible values for Wunderbaum.reload options argument.

interface ReloadOptions {
    reactivate?: boolean;
    source?: SourceType;
}

Properties

Properties

reactivate?: boolean

Reactivate currently active node if any.

true
source?: SourceType

Load this source instead.

initial source (if loaded via ajax)
================================================ FILE: docs/api/interfaces/types.RenderOptions.html ================================================ RenderOptions | wunderbaum - v0.14.1

Interface RenderOptions

Possible values for WunderbaumNode._render.

interface RenderOptions {
    after?: any;
    change?: ChangeType;
    isDataChange?: boolean;
    isNew?: boolean;
    preventScroll?: boolean;
    resizeCols?: boolean;
    top?: number;
}

Properties

after?: any

Where to append a new node.

'last'
change?: ChangeType

Which parts need update?

ChangeType.data
isDataChange?: boolean

@internal.

false
isNew?: boolean

@internal.

false
preventScroll?: boolean

@internal.

false
resizeCols?: boolean

@internal.

true
top?: number

@internal.

false
================================================ FILE: docs/api/interfaces/types.ResetOrderOptions.html ================================================ ResetOrderOptions | wunderbaum - v0.14.1

Interface ResetOrderOptions

Possible values for WunderbaumNode.resetNativeChildOrder options argument.

interface ResetOrderOptions {
    propName?: string;
    recursive?: boolean;
}

Properties

propName?: string

The name of the node property that will be renumbered.

_nativeIndex.

recursive?: boolean

Sort descendants recursively.

true
================================================ FILE: docs/api/interfaces/types.ScrollIntoViewOptions.html ================================================ ScrollIntoViewOptions | wunderbaum - v0.14.1

Interface ScrollIntoViewOptions

Possible values for WunderbaumNode.scrollIntoView options argument.

interface ScrollIntoViewOptions {
    noAnimation?: boolean;
    noEvents?: boolean;
    ofsY?: number;
    topNode?: WunderbaumNode;
}

Hierarchy (View Summary)

Properties

noAnimation?: boolean

Do not animate (currently not implemented).

false
noEvents?: boolean

Do not send events.

false
ofsY?: number

Add N pixel offset at top.

topNode?: WunderbaumNode

Keep this node visible at the top in any case.

================================================ FILE: docs/api/interfaces/types.ScrollToOptions.html ================================================ ScrollToOptions | wunderbaum - v0.14.1

Interface ScrollToOptions

Possible values for Wunderbaum.scrollTo options argument.

interface ScrollToOptions {
    noAnimation?: boolean;
    node: WunderbaumNode;
    noEvents?: boolean;
    ofsY?: number;
    topNode?: WunderbaumNode;
}

Hierarchy (View Summary)

Properties

noAnimation?: boolean

Do not animate (currently not implemented).

false

Which node to scroll into the viewport.

noEvents?: boolean

Do not send events.

false
ofsY?: number

Add N pixel offset at top.

topNode?: WunderbaumNode

Keep this node visible at the top in any case.

================================================ FILE: docs/api/interfaces/types.SetActiveOptions.html ================================================ SetActiveOptions | wunderbaum - v0.14.1

Interface SetActiveOptions

Possible values for WunderbaumNode.setActive options argument.

interface SetActiveOptions {
    colIdx?: string | number;
    edit?: boolean;
    event?: Event;
    focusTree?: boolean;
    noEvents?: boolean;
    retrigger?: boolean;
}

Properties

colIdx?: string | number
edit?: boolean

Focus embedded input control of the grid cell if any (requires colIdx >= 0). If colIdx is 0 or '*', the node title is put into edit mode. Implies focusTree: true, requires colIdx.

event?: Event

Optional original event that will be passed to the (de)activate handler.

focusTree?: boolean

Call tree.setFocus() to acquire keyboard focus (@default: false).

noEvents?: boolean

Do not generate (de)activate event (@default: false).

retrigger?: boolean

Generate (de)activate event, even if node already has this status (@default: false).

================================================ FILE: docs/api/interfaces/types.SetColumnOptions.html ================================================ SetColumnOptions | wunderbaum - v0.14.1

Interface SetColumnOptions

Possible values for Wunderbaum.setColumn options argument.

interface SetColumnOptions {
    edit?: boolean;
    scrollIntoView?: boolean;
}

Properties

edit?: boolean

Focus embedded input control of the grid cell if any . If colIdx is 0 or '*', the node title is put into edit mode.

false
scrollIntoView?: boolean

Horizontically scroll into view. @default: true

================================================ FILE: docs/api/interfaces/types.SetExpandedOptions.html ================================================ SetExpandedOptions | wunderbaum - v0.14.1

Interface SetExpandedOptions

Possible values for WunderbaumNode.setExpanded options argument.

interface SetExpandedOptions {
    force?: boolean;
    immediate?: boolean;
    noAnimation?: boolean;
    noEvents?: boolean;
    resetLazy?: boolean;
    scrollIntoView?: boolean;
}

Properties

force?: boolean

Ignore WunderbaumOptions.minExpandLevel.

false
immediate?: boolean

Immediately update viewport (async otherwise).

false
noAnimation?: boolean

Do not animate expand (currently not implemented).

false
noEvents?: boolean

Do not send events.

false
resetLazy?: boolean

Unload lazily loaded children if any (if collapsing).

false
scrollIntoView?: boolean

Scroll up to bring expanded nodes into viewport.

false
================================================ FILE: docs/api/interfaces/types.SetSelectedOptions.html ================================================ SetSelectedOptions | wunderbaum - v0.14.1

Interface SetSelectedOptions

Possible values for WunderbaumNode.setSelected options argument.

interface SetSelectedOptions {
    callback?: NodeSelectCallback;
    force?: boolean;
    noEvents?: boolean;
    propagateDown?: boolean;
}

Properties

Called for every node. May return false to prevent action.

null
force?: boolean

Ignore restrictions, e.g. (unselectable).

false
noEvents?: boolean

Do not send beforeSelect or select events.

false
propagateDown?: boolean

Apply to all descendant nodes (only for selectMode: 'multi').

false
================================================ FILE: docs/api/interfaces/types.SetStateOptions.html ================================================ SetStateOptions | wunderbaum - v0.14.1

Interface SetStateOptions

Possible values for Wunderbaum.setState.

interface SetStateOptions {
    expandLazy?: boolean;
}

Properties

Properties

expandLazy?: boolean

Recursively load lazy nodes as needed.

false
================================================ FILE: docs/api/interfaces/types.SetStatusOptions.html ================================================ SetStatusOptions | wunderbaum - v0.14.1

Interface SetStatusOptions

Possible values for WunderbaumNode.setStatus options argument.

interface SetStatusOptions {
    details?: string;
    message?: string;
}

Properties

Properties

details?: string

Used as tooltip.

message?: string

Displayed as status node title.

================================================ FILE: docs/api/interfaces/types.SortOptions.html ================================================ SortOptions | wunderbaum - v0.14.1

Interface SortOptions

Possible values for Wunderbaum.sort and WunderbaumNode.sort options argument.

interface SortOptions {
    caseInsensitive?: boolean;
    cmp?: SortCallback;
    colId?: string;
    deep?: boolean;
    key?: SortKeyCallback;
    nativeOrderPropName?: string;
    order?: SortOrderType;
    propName?: string;
    updateColInfo?: boolean;
}

Properties

caseInsensitive?: boolean

Sort string values case insensitive.

false

Callback that determines the order.

use key instead

colId?: string

Column ID as defined in tree.columns definition. Required if updateColInfo is true.

deep?: boolean

Sort descendants recursively.

true

Callback that determines a node representation for comparison.

common.nodeTitleKeyGetter

nativeOrderPropName?: string

Sort by this property if order is undefined. See also WunderbaumNode.resetNativeChildOrder.

_nativeIndex.

Sort order 'asc' or 'desc'.

'asc' (or if updateColInfo is true, the rotated status of the column definition. See also WunderbaumOptions.sortFoldersFirst.

propName?: string

The name of the node property that will be used for sorting. Mandatory, unless key or colId are given.

updateColInfo?: boolean

Rotate sort order (asc -> desc -> none) before sorting. Update the sort icons in the column header Note: Sorting is done in-place. There is no 'unsorted' state, but we can call setCurrentSortOrder() to renumber the node._sortIdx property, which will be used as sort key, when order is undefined.

false
================================================ FILE: docs/api/interfaces/types.SourceAjaxType.html ================================================ SourceAjaxType | wunderbaum - v0.14.1

Interface SourceAjaxType

interface SourceAjaxType {
    body?: any;
    options?: RequestInit;
    params?: any;
    url: string;
}

Properties

body?: any
options?: RequestInit
params?: any
url: string
================================================ FILE: docs/api/interfaces/types.SourceObjectType.html ================================================ SourceObjectType | wunderbaum - v0.14.1

Interface SourceObjectType

interface SourceObjectType {
    _format?: "flat" | "nested";
    _keyMap?: { [key: string]: string };
    _positional?: string[];
    _valueMap?: { [key: string]: string[] };
    _version?: number;
    children: SourceListType;
    columns?: ColumnDefinitionList;
    types?: NodeTypeDefinitionMap;
}

Properties

_format?: "flat" | "nested"
_keyMap?: { [key: string]: string }
_positional?: string[]
_valueMap?: { [key: string]: string[] }
_version?: number
children: SourceListType
================================================ FILE: docs/api/interfaces/types.TreeStateDefinition.html ================================================ TreeStateDefinition | wunderbaum - v0.14.1

Interface TreeStateDefinition

interface TreeStateDefinition {
    activeColIdx: null | number;
    activeKey: null | string;
    expandedKeys: undefined | string[];
    selectedKeys: undefined | string[];
}

Properties

activeColIdx: null | number

The active column index if any.

activeKey: null | string

The active node's key if any.

expandedKeys: undefined | string[]

List of expanded node's keys.

selectedKeys: undefined | string[]

List of selected node's keys.

================================================ FILE: docs/api/interfaces/types.UpdateOptions.html ================================================ UpdateOptions | wunderbaum - v0.14.1

Interface UpdateOptions

Possible values for WunderbaumNode.update options argument.

interface UpdateOptions {
    immediate?: boolean;
}

Properties

Properties

immediate?: boolean

Force immediate redraw instead of throttled/async mode.

false
================================================ FILE: docs/api/interfaces/types.VisitRowsOptions.html ================================================ VisitRowsOptions | wunderbaum - v0.14.1

Interface VisitRowsOptions

Options passed to Wunderbaum.visitRows.

interface VisitRowsOptions {
    includeHidden?: boolean;
    includeSelf?: boolean;
    reverse?: boolean;
    start?: null | WunderbaumNode;
    wrap?: boolean;
}

Properties

includeHidden?: boolean

Skip filtered nodes and children of collapsed nodes.

false
includeSelf?: boolean

Return the start node as first result.

true
reverse?: boolean

Traverse in opposite direction, i.e. bottom up.

false
start?: null | WunderbaumNode

Start traversal at this node

first (topmost) tree node
wrap?: boolean

Wrap around at last node and continue at the top, until the start node is reached again

false
================================================ FILE: docs/api/interfaces/types.WbActivateEventType.html ================================================ WbActivateEventType | wunderbaum - v0.14.1

Interface WbActivateEventType

interface WbActivateEventType {
    event: Event;
    node: WunderbaumNode;
    prevNode: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event: Event

The original event.

The affected target node.

prevNode: WunderbaumNode

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbButtonClickEventType.html ================================================ WbButtonClickEventType | wunderbaum - v0.14.1

Interface WbButtonClickEventType

interface WbButtonClickEventType {
    command: string;
    event?: Event;
    info: WbEventInfo;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

command: string

The associated command, e.g. 'menu', 'sort', 'filter', ...

event?: Event

Originating HTML event if any (e.g. click).

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbChangeEventType.html ================================================ WbChangeEventType | wunderbaum - v0.14.1

Interface WbChangeEventType

interface WbChangeEventType {
    event?: Event;
    info: WbEventInfo;
    inputElem: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
    inputValid: boolean;
    inputValue: any;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

Additional information derived from the original change event.

inputElem: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement

The embedded element that fired the change event.

inputValid: boolean

Result of inputElem.checkValidity().

inputValue: any

The new value of the embedded element, depending on the input element type.

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbClickEventType.html ================================================ WbClickEventType | wunderbaum - v0.14.1

Interface WbClickEventType

interface WbClickEventType {
    event: MouseEvent;
    info: WbEventInfo;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

event: MouseEvent

The original event.

Additional information derived from the original mouse event.

The clicked node if any.

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbDeactivateEventType.html ================================================ WbDeactivateEventType | wunderbaum - v0.14.1

Interface WbDeactivateEventType

interface WbDeactivateEventType {
    event: Event;
    nextNode: WunderbaumNode;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event: Event

The original event.

nextNode: WunderbaumNode

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbEditApplyEventType.html ================================================ WbEditApplyEventType | wunderbaum - v0.14.1

Interface WbEditApplyEventType

interface WbEditApplyEventType {
    event?: Event;
    info: WbEventInfo;
    inputElem: HTMLInputElement;
    inputValid: boolean;
    newValue: string;
    node: WunderbaumNode;
    oldValue: string;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

Additional information derived from the original change event.

inputElem: HTMLInputElement

The input element of the node title that fired the change event.

inputValid: boolean

Result of inputElem.checkValidity().

newValue: string

The new node title.

The affected target node.

oldValue: string

The previous node title.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbEditEditEventType.html ================================================ WbEditEditEventType | wunderbaum - v0.14.1

Interface WbEditEditEventType

interface WbEditEditEventType {
    event?: Event;
    inputElem: HTMLInputElement;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

inputElem: HTMLInputElement

The input element of the node title that was just created.

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbErrorEventType.html ================================================ WbErrorEventType | wunderbaum - v0.14.1

Interface WbErrorEventType

interface WbErrorEventType {
    error: any;
    event?: Event;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

error: any
event?: Event

Originating HTML event if any (e.g. click).

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbEventInfo.html ================================================ WbEventInfo | wunderbaum - v0.14.1

Interface WbEventInfo

Additional information derived from mouse or keyboard events.

interface WbEventInfo {
    canonicalName: string;
    colDef?: ColumnDefinition;
    colElem?: HTMLSpanElement;
    colId?: string;
    colIdx: number;
    event: MouseEvent | KeyboardEvent;
    node: null | WunderbaumNode;
    region: NodeRegion;
    tree: Wunderbaum;
}

Properties

canonicalName: string

Canonical descriptive string of the event type including modifiers, e.g. Ctrl+Down.

The definition of the affected column if any.

colElem?: HTMLSpanElement

The affected column's span tag if any.

colId?: string

The column definition ID of affected column if any.

colIdx: number

The index of affected column or -1.

event: MouseEvent | KeyboardEvent

The original HTTP Event.

node: null | WunderbaumNode

The affected node instance instance if any.

region: NodeRegion

The affected part of the node span (e.g. title, expander, ...).

The tree instance.

================================================ FILE: docs/api/interfaces/types.WbExpandEventType.html ================================================ WbExpandEventType | wunderbaum - v0.14.1

Interface WbExpandEventType

interface WbExpandEventType {
    event?: Event;
    flag: boolean;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

flag: boolean

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbFocusEventType.html ================================================ WbFocusEventType | wunderbaum - v0.14.1

Interface WbFocusEventType

interface WbFocusEventType {
    event: FocusEvent;
    flag: boolean;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

Properties

event: FocusEvent

The original event.

flag: boolean

True if focusin, false if focusout.

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbIconBadgeEventResultType.html ================================================ WbIconBadgeEventResultType | wunderbaum - v0.14.1

Interface WbIconBadgeEventResultType

interface WbIconBadgeEventResultType {
    badge: null | string | number | false | HTMLSpanElement;
    badgeClass?: string;
    badgeTooltip?: string;
}

Properties

badge: null | string | number | false | HTMLSpanElement

Content of the badge <span class='wb-badge'> if any.

badgeClass?: string

Additional class name(s), separate with space.

badgeTooltip?: string

Tooltip for the badge.

================================================ FILE: docs/api/interfaces/types.WbIconBadgeEventType.html ================================================ WbIconBadgeEventType | wunderbaum - v0.14.1

Interface WbIconBadgeEventType

interface WbIconBadgeEventType {
    event?: Event;
    iconSpan: HTMLElement;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

iconSpan: HTMLElement

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbInitEventType.html ================================================ WbInitEventType | wunderbaum - v0.14.1

Interface WbInitEventType

interface WbInitEventType {
    error?: any;
    event?: Event;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

error?: any
event?: Event

Originating HTML event if any (e.g. click).

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbKeydownEventType.html ================================================ WbKeydownEventType | wunderbaum - v0.14.1

Interface WbKeydownEventType

interface WbKeydownEventType {
    event: KeyboardEvent;
    info: WbEventInfo;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

event: KeyboardEvent

The original event.

Additional information derived from the original keyboard event.

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbModifyChildEventType.html ================================================ WbModifyChildEventType | wunderbaum - v0.14.1

Interface WbModifyChildEventType

interface WbModifyChildEventType {
    child: WunderbaumNode;
    event?: Event;
    node: WunderbaumNode;
    operation: string;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

The affected target node.

operation: string

Type of change: 'add', 'remove', 'rename', 'move', 'data', ...

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbNodeData.html ================================================ WbNodeData | wunderbaum - v0.14.1

Interface WbNodeData

A plain object (dictionary) that represents a node instance.

interface WbNodeData {
    _treeId?: string;
    checkbox?: CheckboxOption;
    children?: WbNodeData[];
    classes?: string;
    colspan?: boolean;
    expanded?: boolean;
    icon?: IconOption;
    iconTooltip?: TooltipOption;
    key?: string;
    lazy?: boolean;
    radiogroup?: boolean;
    refKey?: string;
    selected?: boolean;
    statusNodeType?: NodeStatusType;
    title: string;
    tooltip?: TooltipOption;
    type?: string;
    unselectable?: boolean;
    [key: string]: unknown;
}

Indexable

  • [key: string]: unknown

    Other data is passed to node.data and can be accessed via node.data.NAME

Properties

_treeId?: string
checkbox?: CheckboxOption

Defines if the selected state is displayed as checkbox, radio button, or hidden. Defaults to WunderbaumOptions.checkbox.

children?: WbNodeData[]

Optional list of child nodes. If children is an empty array, the node is considered a leaf. If lazy is true and `children is undefined or null, the node, is considered unloaded. Otherwise, the node is considered a leaf.

classes?: string

Additional classes that are added to <div class='wb-row'>.

colspan?: boolean

Only show title in a single, merged column.

expanded?: boolean

Expand this node.

icon?: IconOption

Defaults to standard icons (doc, folder, folderOpen, ...) from WunderbaumOptions.iconMap. Can be overridden by WunderbaumOptions.icon.

iconTooltip?: TooltipOption

Tooltip for the node icon only. Defaults to WunderbaumOptions.iconTooltip.

key?: string

The node's key. Must be unique for the whole tree. Defaults to a sequence number.

lazy?: boolean

If true (and children are undefined or null), the node is considered lazy and WunderbaumOptions.lazyLoad is called when expanded.

radiogroup?: boolean

Make child nodes single-select radio buttons.

refKey?: string

Node's reference key. Unlike WunderbaumNode.key, this value may be non-unique. Nodes within the tree that share the same refKey are considered clones.

selected?: boolean

The node's selection status, typically displayed as a checkbox.

statusNodeType?: NodeStatusType

The node's status, typically displayed as merged single row.

title: string

The node's title. Will be html escaped to prevent XSS.

tooltip?: TooltipOption

Pass true to set node tooltip to the node's title. Defaults to WunderbaumOptions.tooltip.

type?: string

Inherit shared settings from the matching entry in InitWunderbaumOptions.types.

unselectable?: boolean

Set to true to prevent selection. Defaults to WunderbaumOptions.unselectable.

================================================ FILE: docs/api/interfaces/types.WbNodeEventType.html ================================================ WbNodeEventType | wunderbaum - v0.14.1

Interface WbNodeEventType

interface WbNodeEventType {
    event?: Event;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbReceiveEventType.html ================================================ WbReceiveEventType | wunderbaum - v0.14.1

Interface WbReceiveEventType

interface WbReceiveEventType {
    event?: Event;
    node: WunderbaumNode;
    response: any;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

The affected target node.

response: any

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbRenderEventType.html ================================================ WbRenderEventType | wunderbaum - v0.14.1

Interface WbRenderEventType

interface WbRenderEventType {
    allColInfosById: ColumnEventInfoMap;
    event?: Event;
    isColspan: boolean;
    isNew: boolean;
    node: WunderbaumNode;
    nodeElem: HTMLSpanElement;
    renderColInfosById: ColumnEventInfoMap;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

allColInfosById: ColumnEventInfoMap

Array of node's <span class='wb-col'> elements. The first element is <span class='wb-node wb-col'>, which contains the node title and icon (idx: 0, id: '*'`).

event?: Event

Originating HTML event if any (e.g. click).

isColspan: boolean

True if the node only displays the title and is stretched over all remaining columns.

isNew: boolean

True if the node's markup was not yet created. In this case the render event should create embedded input controls (in addition to update the values according to to current node data).
False if the node's markup was already created. In this case the render event should only update the values according to to current node data.

The affected target node.

nodeElem: HTMLSpanElement

The node's <span class='wb-node'> element.

renderColInfosById: ColumnEventInfoMap

Array of node's <span class='wb-node'> elements, that should be rendered by the event handler. In contrast to allColInfosById, the node title is not part of this array. If node.isColspan() is true, this array is empty ([]). This allows to iterate over all relevant in a simple loop:

for (const col of Object.values(e.renderColInfosById)) {
switch (col.id) {
default:
// Assumption: we named column.id === node.data.NAME
col.elem.textContent = node.data[col.id];
break;
}

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbSelectEventType.html ================================================ WbSelectEventType | wunderbaum - v0.14.1

Interface WbSelectEventType

interface WbSelectEventType {
    event?: Event;
    flag: boolean;
    node: WunderbaumNode;
    tree: Wunderbaum;
    type: string;
    typeInfo: NodeTypeDefinition;
    util: any;
}

Hierarchy (View Summary)

Properties

event?: Event

Originating HTML event if any (e.g. click).

flag: boolean

The affected target node.

The affected tree instance.

type: string

Name of the event.

Contains the node's type information, i.e. tree.types[node.type] if defined. Set to {} otherwise.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/types.WbTreeEventType.html ================================================ WbTreeEventType | wunderbaum - v0.14.1

Interface WbTreeEventType

interface WbTreeEventType {
    event?: Event;
    tree: Wunderbaum;
    type: string;
    util: any;
}

Hierarchy (View Summary)

Properties

Properties

event?: Event

Originating HTML event if any (e.g. click).

The affected tree instance.

type: string

Name of the event.

util: any

Exposed utility module methods (see API docs).

================================================ FILE: docs/api/interfaces/wb_options.InitWunderbaumOptions.html ================================================ InitWunderbaumOptions | wunderbaum - v0.14.1

Interface InitWunderbaumOptions

Available options for wunderbaum.Wunderbaum.

Options are passed to the constructor as plain object:

const tree = new mar10.Wunderbaum({
id: "demo",
element: document.getElementById("demo-tree"),
source: "url/of/data/request",
...
});

Event handlers are also passed as callbacks

const tree = new mar10.Wunderbaum({
...
init: (e) => {
console.log(`Tree ${e.tree} was initialized and loaded.`)
},
activate: (e) => {
console.log(`Node ${e.node} was activated.`)
},
...
});

Most of the properties are optional and have resonable default. They are then available as Wunderbaum.options property and can be changed at runtime.
Only the element option is mandatory.

Note that some options passed here, are not available as Wunderbaum.options. They are moved to the tree instance instead:

  • tree.element
  • tree.id
  • tree.columns
  • tree.types
  • ...

Some options are only used during initialization and are not stored in the tree instance:

  • source
interface InitWunderbaumOptions {
    activate?: (e: WbActivateEventType) => void;
    adjustHeight?: boolean;
    autoCollapse?: boolean;
    autoKeys?: boolean;
    beforeActivate?: (e: WbActivateEventType) => WbCancelableEventResultType;
    beforeExpand?: (e: WbExpandEventType) => WbCancelableEventResultType;
    beforeSelect?: (e: WbSelectEventType) => WbCancelableEventResultType;
    buttonClick?: (e: WbButtonClickEventType) => void;
    change?: (e: WbChangeEventType) => void;
    checkbox?: DynamicCheckboxOption;
    click?: (e: WbClickEventType) => WbCancelableEventResultType;
    columns?: ColumnDefinitionList;
    columnsFilterable?: boolean;
    columnsMenu?: boolean;
    columnsResizable?: boolean;
    columnsSortable?: boolean;
    connectTopBreadcrumb?: null | string | HTMLElement;
    dblclick?: (e: WbClickEventType) => WbCancelableEventResultType;
    deactivate?: (e: WbDeactivateEventType) => WbCancelableEventResultType;
    debugLevel?: number;
    discard?: (e: WbNodeEventType) => void;
    dnd?: DndOptionsType;
    edit?: EditOptionsType;
    element: string | HTMLDivElement;
    emptyChildListExpandable?: boolean;
    enabled?: boolean;
    error?: (e: WbErrorEventType) => void;
    expand?: (e: WbTreeEventType) => void;
    filter?: FilterOptionsType;
    fixedCol?: boolean;
    focus?: (e: WbTreeEventType) => void;
    header?: null | string | boolean;
    icon?: DynamicIconOption;
    iconBadge?: (e: WbIconBadgeCallback) => WbIconBadgeEventResultType;
    iconMap?: string | IconMapType;
    iconTooltip?: DynamicBoolOrStringOption;
    id?: string;
    init?: (e: WbInitEventType) => void;
    keydown?: (e: WbKeydownEventType) => WbCancelableEventResultType;
    lazyLoad?: (e: WbNodeEventType) => void;
    load?: (e: WbNodeEventType) => void;
    minExpandLevel?: number;
    modifyChild?: (e: WbNodeEventType) => void;
    navigationModeOption?: NavModeEnum;
    quicksearch?: boolean;
    receive?: (e: WbReceiveEventType) => void;
    render?: (e: WbRenderEventType) => void;
    renderStatusNode?: (e: WbRenderEventType) => void;
    rowHeightPx?: number;
    scrollIntoViewOnExpandClick?: boolean;
    select?: (e: WbNodeEventType) => void;
    selectMode?: SelectModeType;
    showSpinner?: boolean;
    skeleton?: boolean;
    sortFoldersFirst?: DynamicBoolOption;
    source?: SourceType;
    strings?: TranslationsType;
    tooltip?: DynamicBoolOrStringOption;
    types?: NodeTypeDefinitionMap;
    unselectable?: DynamicBoolOption;
    update?: (e: WbTreeEventType) => void;
}

Hierarchy

Callback

activate?: (e: WbActivateEventType) => void

e.node was activated.

e.node is about to be activated. Return false to prevent default handling, i.e. activating the node. See also deactivate event.

e.node is about to be expanded/collapsed. Return false to prevent default handling, i.e. expanding/collapsing the node.

Return false to prevent default handling, i.e. (de)selecting the node.

buttonClick?: (e: WbButtonClickEventType) => void

Return false to prevent default handling, i.e. (de)selecting the node.

change?: (e: WbChangeEventType) => void

Return false to prevent default behavior, e.g. expand/collapse, (de)selection, or activation.

Return false to prevent default behavior, e.g. expand/collapse.

e.node was deactivated.

Return false to prevent default handling, e.g. deactivating the node and activating the next. See also activate event.

discard?: (e: WbNodeEventType) => void

e.node was discarded from the viewport and its HTML markup removed.

error?: (e: WbErrorEventType) => void

An error occurred, e.g. during initialization or lazy loading.

expand?: (e: WbTreeEventType) => void

e.node was expanded (e.flag === true) or collapsed (e.flag === false)

focus?: (e: WbTreeEventType) => void

The tree received or lost focus. Check e.flag for status.

e.node is about to be rendered. We can add a badge to the icon cell here.

init?: (e: WbInitEventType) => void

Fires when the tree markup was created and the initial source data was loaded. Typical use cases would be activating a node, setting focus, enabling other controls on the page, etc.
Also sent if an error occured during initialization (check e.error for status).

Fires when a key was pressed while the tree has focus. e.node is set if a node is currently active. Return false to prevent default navigation.

lazyLoad?: (e: WbNodeEventType) => void

Fires when a node that was marked 'lazy', is expanded for the first time. Typically we return an endpoint URL or the Promise of a fetch request that provides a (potentially nested) list of child nodes.

load?: (e: WbNodeEventType) => void

Fires when data was loaded (initial request, reload, or lazy loading), after the data is applied and rendered.

modifyChild?: (e: WbNodeEventType) => void
receive?: (e: WbReceiveEventType) => void

Fires when data was fetched (initial request, reload, or lazy loading), but before the data is applied and rendered. Here we can modify and adjust the received data, for example to convert an external response to native Wunderbaum syntax.

render?: (e: WbRenderEventType) => void

Fires when a node is about to be displayed. The default HTML markup is already created, but not yet added to the DOM. Now we can tweak the markup, create HTML elements in this node's column cells, etc. See also Custom Rendering for details.

renderStatusNode?: (e: WbRenderEventType) => void

Same as render(e), but for the status nodes, i.e. e.node.statusNodeType.

select?: (e: WbNodeEventType) => void

e.node was selected (e.flag === true) or deselected (e.flag === false)

update?: (e: WbTreeEventType) => void

Fires when the viewport content was updated, after scroling, expanding etc.

Other

adjustHeight?: boolean

If true, the tree will automatically adjust its height to fit the parent container. This is useful when the tree is embedded in a container with a fixed or calculated sized. If the parent container is unsized (e.g. grows with its content, height: auto), then we can define a height: ... or max_height: ... style on the parent. To avoid a recursive resize-loop, it may be helpful to set overflow: hidden on the parent container.

Set this option to false will disable auto-resizing.

@default: true

autoCollapse?: boolean

Collapse siblings when a node is expanded.

false
autoKeys?: boolean

Generate missing keys by hashing a combination of refKey (or title) and the parent key. This is useful when the source data does not contain unique keys but we want stable keys for persisting the active node, selection or expansion state. Note that this still assumes that the same refKey must not appear twice in the same parent node.

false.

If true, render a checkbox before the node tile to allow selection with the mouse. Pass "radio" to render a radio button instead.

false.

A list of maps that define column headers. If this option is set, Wunderbaum becomes a treegrid control instead of a plain tree. Column definitions can be passed as tree option, or be part of a source response.

[] meaning this is a plain tree.

columnsFilterable?: boolean

Default value for ColumnDefinition.filterable option.

false

0.11.0

columnsMenu?: boolean

Default value for ColumnDefinition.menu option.

false

0.11.0

columnsResizable?: boolean

Default value for ColumnDefinition.resizable option.

false

0.10.0

columnsSortable?: boolean

Default value for ColumnDefinition.sortable option.

false

0.11.0

connectTopBreadcrumb?: null | string | HTMLElement

HTMLElement or selector that receives the top nodes breadcrumb.

undefined
debugLevel?: number

0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose

3 (4 in local debug environment)

Configuration options for the drag-and-drop extension.

Configuration options for the edit-title extension.

element: string | HTMLDivElement

The target div element (or selector) that shall become a Wunderbaum.

emptyChildListExpandable?: boolean

If true, allow to expand parent nodes, even if node.children conatains an empty array ([]). This is the the behavior of macOS Finder, for example.

false
enabled?: boolean
true

Configuration options for the node-filter extension.

fixedCol?: boolean
false
header?: null | string | boolean

Show/hide header (default: null) null: assume false for plain tree and true for grids. string: use text as header (only for plain trees) true: display a header (use tree's id as text for plain trees) false: do not display a header

Optional callback to render icons per node.

iconMap?: string | IconMapType

Icon font definition. May be a string (e.g. "fontawesome6" or "bootstrap") or a map of iconName: iconClass pairs. Note: the icon font must be loaded separately. In order to only override some defauöt icons, use this pattern:

const tree = new mar10.Wunderbaum({
...
iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
folder: "bi bi-archive",
}),
});
"bootstrap"

Optional callback to render a tooltip for the icon.

id?: string

The identifier of this tree. Used to reference the instance, especially when multiple trees are present (e.g. tree = mar10.Wunderbaum.getTree("demo")).

"wb_" + COUNTER.

minExpandLevel?: number

Number of levels that are forced to be expanded, and have no expander icon. E.g. 1 would keep all toplevel nodes expanded.

0
navigationModeOption?: NavModeEnum
NavModeEnum.startRow
quicksearch?: boolean
true
rowHeightPx?: number

Height of a node row div.

22
scrollIntoViewOnExpandClick?: boolean

Scroll Node into view on Expand Click

true
selectMode?: SelectModeType
"multi"
showSpinner?: boolean

Show a <progress> element while loading data.

false.
skeleton?: boolean

If true, add a wb-skeleton class to all nodes, that will result in a 'glow' effect. Typically used with initial dummy nodes, while loading the real data.

false.
sortFoldersFirst?: DynamicBoolOption

Group nodes with children or of type: 'folder' at the top when sorting. If a function is passed, it is called with the node as argument to determine whether the node is a folder or not. The function should return true for folders. and should return true for folders.

false

0.14.0

source?: SourceType

Define the initial tree data. Typically a URL of an endpoint that serves a JSON formatted structure, but also a callback, Promise, or static data is allowed.

[].

Translation map for some system messages.

Optional callback to render a tooltip for the node title. Pass true to use the node's title property as tooltip.

Define shared attributes for multiple nodes of the same type. This allows for more compact data models. Type definitions can be passed as tree option, or be part of a source response.

{}.

unselectable?: DynamicBoolOption

Optional callback to make a node unselectable.

================================================ FILE: docs/api/interfaces/wb_options.WunderbaumOptions.html ================================================ WunderbaumOptions | wunderbaum - v0.14.1

Interface WunderbaumOptions

Properties of wunderbaum.Wunderbaum.options.

This is similar, but not identical, to the options that can be passed to the constructor(@see InitWunderbaumOptions).

interface WunderbaumOptions {
    activate?: (e: WbActivateEventType) => void;
    adjustHeight: boolean;
    autoCollapse: boolean;
    autoKeys: boolean;
    beforeActivate?: (e: WbActivateEventType) => WbCancelableEventResultType;
    beforeExpand?: (e: WbExpandEventType) => WbCancelableEventResultType;
    beforeSelect?: (e: WbSelectEventType) => WbCancelableEventResultType;
    buttonClick?: (e: WbButtonClickEventType) => void;
    change?: (e: WbChangeEventType) => void;
    checkbox: DynamicCheckboxOption;
    click?: (e: WbClickEventType) => WbCancelableEventResultType;
    columnsFilterable: boolean;
    columnsMenu: boolean;
    columnsResizable?: boolean;
    columnsSortable?: boolean;
    connectTopBreadcrumb: null | string | HTMLElement;
    dblclick?: (e: WbClickEventType) => WbCancelableEventResultType;
    deactivate?: (e: WbDeactivateEventType) => WbCancelableEventResultType;
    debugLevel: number;
    discard?: (e: WbNodeEventType) => void;
    dnd: DndOptionsType;
    edit: EditOptionsType;
    emptyChildListExpandable: boolean;
    enabled: boolean;
    error?: (e: WbErrorEventType) => void;
    expand?: (e: WbTreeEventType) => void;
    filter: FilterOptionsType;
    fixedCol: boolean;
    focus?: (e: WbTreeEventType) => void;
    header: null | string | boolean;
    icon?: DynamicIconOption;
    iconBadge?: (e: WbIconBadgeCallback) => WbIconBadgeEventResultType;
    iconMap: string | IconMapType;
    iconTooltip?: DynamicBoolOrStringOption;
    init?: (e: WbInitEventType) => void;
    keydown?: (e: WbKeydownEventType) => WbCancelableEventResultType;
    lazyLoad?: (e: WbNodeEventType) => void;
    load?: (e: WbNodeEventType) => void;
    minExpandLevel: number;
    modifyChild?: (e: WbNodeEventType) => void;
    navigationModeOption: NavModeEnum;
    quicksearch: boolean;
    receive?: (e: WbReceiveEventType) => void;
    render?: (e: WbRenderEventType) => void;
    renderStatusNode?: (e: WbRenderEventType) => void;
    rowHeightPx: number;
    scrollIntoViewOnExpandClick: boolean;
    select?: (e: WbNodeEventType) => void;
    selectMode: SelectModeType;
    showSpinner: boolean;
    skeleton: boolean;
    sortFoldersFirst?: DynamicBoolOption;
    strings: TranslationsType;
    tooltip?: DynamicBoolOrStringOption;
    unselectable?: DynamicBoolOption;
    update?: (e: WbTreeEventType) => void;
}

Callback

activate?: (e: WbActivateEventType) => void

e.node was activated.

e.node is about to be activated. Return false to prevent default handling, i.e. activating the node. See also deactivate event.

e.node is about to be expanded/collapsed. Return false to prevent default handling, i.e. expanding/collapsing the node.

Return false to prevent default handling, i.e. (de)selecting the node.

buttonClick?: (e: WbButtonClickEventType) => void

Return false to prevent default handling, i.e. (de)selecting the node.

change?: (e: WbChangeEventType) => void

Return false to prevent default behavior, e.g. expand/collapse, (de)selection, or activation.

Return false to prevent default behavior, e.g. expand/collapse.

e.node was deactivated.

Return false to prevent default handling, e.g. deactivating the node and activating the next. See also activate event.

discard?: (e: WbNodeEventType) => void

e.node was discarded from the viewport and its HTML markup removed.

error?: (e: WbErrorEventType) => void

An error occurred, e.g. during initialization or lazy loading.

expand?: (e: WbTreeEventType) => void

e.node was expanded (e.flag === true) or collapsed (e.flag === false)

focus?: (e: WbTreeEventType) => void

The tree received or lost focus. Check e.flag for status.

e.node is about to be rendered. We can add a badge to the icon cell here.

init?: (e: WbInitEventType) => void

Fires when the tree markup was created and the initial source data was loaded. Typical use cases would be activating a node, setting focus, enabling other controls on the page, etc.
Also sent if an error occured during initialization (check e.error for status).

Fires when a key was pressed while the tree has focus. e.node is set if a node is currently active. Return false to prevent default navigation.

lazyLoad?: (e: WbNodeEventType) => void

Fires when a node that was marked 'lazy', is expanded for the first time. Typically we return an endpoint URL or the Promise of a fetch request that provides a (potentially nested) list of child nodes.

load?: (e: WbNodeEventType) => void

Fires when data was loaded (initial request, reload, or lazy loading), after the data is applied and rendered.

modifyChild?: (e: WbNodeEventType) => void
receive?: (e: WbReceiveEventType) => void

Fires when data was fetched (initial request, reload, or lazy loading), but before the data is applied and rendered. Here we can modify and adjust the received data, for example to convert an external response to native Wunderbaum syntax.

render?: (e: WbRenderEventType) => void

Fires when a node is about to be displayed. The default HTML markup is already created, but not yet added to the DOM. Now we can tweak the markup, create HTML elements in this node's column cells, etc. See also Custom Rendering for details.

renderStatusNode?: (e: WbRenderEventType) => void

Same as render(e), but for the status nodes, i.e. e.node.statusNodeType.

select?: (e: WbNodeEventType) => void

e.node was selected (e.flag === true) or deselected (e.flag === false)

update?: (e: WbTreeEventType) => void

Fires when the viewport content was updated, after scroling, expanding etc.

Other

adjustHeight: boolean

If true, the tree will automatically adjust its height to fit the parent container. This is useful when the tree is embedded in a container with a fixed or calculated sized. If the parent container is unsized (e.g. grows with its content, height: auto), then we can define a height: ... or max_height: ... style on the parent. To avoid a recursive resize-loop, it may be helpful to set overflow: hidden on the parent container.

Set this option to false will disable auto-resizing.

@default: true

autoCollapse: boolean

Collapse siblings when a node is expanded.

false
autoKeys: boolean

Generate missing keys by hashing a combination of refKey (or title) and the parent key. This is useful when the source data does not contain unique keys but we want stable keys for persisting the active node, selection or expansion state. Note that this still assumes that the same refKey must not appear twice in the same parent node.

false.

If true, render a checkbox before the node tile to allow selection with the mouse. Pass "radio" to render a radio button instead.

false.
columnsFilterable: boolean

Default value for ColumnDefinition.filterable option.

false

0.11.0

columnsMenu: boolean

Default value for ColumnDefinition.menu option.

false

0.11.0

columnsResizable?: boolean

Default value for ColumnDefinition.resizable option.

false

0.10.0

columnsSortable?: boolean

Default value for ColumnDefinition.sortable option.

false

0.11.0

connectTopBreadcrumb: null | string | HTMLElement

HTMLElement or selector that receives the top nodes breadcrumb.

undefined
debugLevel: number

0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose

3 (4 in local debug environment)

Configuration options for the drag-and-drop extension.

Configuration options for the edit-title extension.

emptyChildListExpandable: boolean

If true, allow to expand parent nodes, even if node.children conatains an empty array ([]). This is the the behavior of macOS Finder, for example.

false
enabled: boolean
true

Configuration options for the node-filter extension.

fixedCol: boolean
false
header: null | string | boolean

Show/hide header (default: null) null: assume false for plain tree and true for grids. string: use text as header (only for plain trees) true: display a header (use tree's id as text for plain trees) false: do not display a header

Optional callback to render icons per node.

iconMap: string | IconMapType

Icon font definition. May be a string (e.g. "fontawesome6" or "bootstrap") or a map of iconName: iconClass pairs. Note: the icon font must be loaded separately. In order to only override some defauöt icons, use this pattern:

const tree = new mar10.Wunderbaum({
...
iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, {
folder: "bi bi-archive",
}),
});
"bootstrap"

Optional callback to render a tooltip for the icon.

minExpandLevel: number

Number of levels that are forced to be expanded, and have no expander icon. E.g. 1 would keep all toplevel nodes expanded.

0
navigationModeOption: NavModeEnum
NavModeEnum.startRow
quicksearch: boolean
true
rowHeightPx: number

Height of a node row div.

22
scrollIntoViewOnExpandClick: boolean

Scroll Node into view on Expand Click

true
selectMode: SelectModeType
"multi"
showSpinner: boolean

Show a <progress> element while loading data.

false.
skeleton: boolean

If true, add a wb-skeleton class to all nodes, that will result in a 'glow' effect. Typically used with initial dummy nodes, while loading the real data.

false.
sortFoldersFirst?: DynamicBoolOption

Group nodes with children or of type: 'folder' at the top when sorting. If a function is passed, it is called with the node as argument to determine whether the node is a folder or not. The function should return true for folders. and should return true for folders.

false

0.14.0

Translation map for some system messages.

Optional callback to render a tooltip for the node title. Pass true to use the node's title property as tooltip.

unselectable?: DynamicBoolOption

Optional callback to make a node unselectable.

================================================ FILE: docs/api/modules/common.html ================================================ common | wunderbaum - v0.14.1
================================================ FILE: docs/api/modules/types.html ================================================ types | wunderbaum - v0.14.1

Module types

Enumerations

ChangeType
NavModeEnum
NodeRegion
NodeStatusType
RenderFlag

Interfaces

AddChildrenOptions
ApplyCommandOptions
ColumnDefinition
ColumnEventInfo
DragEventType
DropEventType
ExpandAllOptions
FilterConnectType
FilterNodesOptions
GetStateOptions
IconMapType
LoadLazyNodesOptions
MakeVisibleOptions
NavigateOptions
NodeTypeDefinition
ReloadOptions
RenderOptions
ResetOrderOptions
ScrollIntoViewOptions
ScrollToOptions
SetActiveOptions
SetColumnOptions
SetExpandedOptions
SetSelectedOptions
SetStateOptions
SetStatusOptions
SortOptions
SourceAjaxType
SourceObjectType
TreeStateDefinition
UpdateOptions
VisitRowsOptions
WbActivateEventType
WbButtonClickEventType
WbChangeEventType
WbClickEventType
WbDeactivateEventType
WbEditApplyEventType
WbEditEditEventType
WbErrorEventType
WbEventInfo
WbExpandEventType
WbFocusEventType
WbIconBadgeEventResultType
WbIconBadgeEventType
WbInitEventType
WbKeydownEventType
WbModifyChildEventType
WbNodeData
WbNodeEventType
WbReceiveEventType
WbRenderEventType
WbSelectEventType
WbTreeEventType

Type Aliases

ApplyCommandType
BoolOptionResolver
BoolOrStringOptionResolver
CheckboxOption
ColumnDefinitionList
ColumnEventInfoMap
DeprecationOptions
DndOptionsType
DropEffectAllowedType
DropEffectType
DropRegionType
DropRegionTypeList
DropRegionTypeSet
DynamicBoolOption
DynamicBoolOrStringOption
DynamicCheckboxOption
DynamicIconOption
DynamicStringOption
DynamicTooltipOption
EditOptionsType
FilterModeType
FilterOptionsType
GridOptionsType
IconOption
InsertNodeType
KeynavOptionsType
LoggerOptionsType
MatcherCallback
NavigationType
NodeAnyCallback
NodeFilterCallback
NodeFilterResponse
NodePropertyGetterCallback
NodeSelectCallback
NodeStringCallback
NodeToDictCallback
NodeTypeDefinitionMap
NodeVisitCallback
NodeVisitResponse
SelectModeType
SortByPropertyOptions
SortCallback
SortKeyCallback
SortOrderType
SourceListType
SourceType
TooltipOption
TranslationsType
TristateType
WbCancelableEventResultType
WbIconBadgeCallback
================================================ FILE: docs/api/modules/util.html ================================================ util | wunderbaum - v0.14.1
================================================ FILE: docs/api/modules/wb_node.html ================================================ wb_node | wunderbaum - v0.14.1

Module wb_node

Classes

WunderbaumNode
================================================ FILE: docs/api/modules/wb_options.html ================================================ wb_options | wunderbaum - v0.14.1
================================================ FILE: docs/api/modules/wunderbaum.html ================================================ wunderbaum | wunderbaum - v0.14.1

Module wunderbaum

Classes

Wunderbaum
================================================ FILE: docs/api/modules.html ================================================ wunderbaum - v0.14.1
================================================ FILE: docs/api/types/types.ApplyCommandType.html ================================================ ApplyCommandType | wunderbaum - v0.14.1

Type Alias ApplyCommandType

ApplyCommandType:
    | NavigationType
    | "addChild"
    | "addSibling"
    | "collapse"
    | "collapseAll"
    | "copy"
    | "cut"
    | "edit"
    | "expand"
    | "expandAll"
    | "indent"
    | "moveDown"
    | "moveUp"
    | "outdent"
    | "paste"
    | "remove"
    | "rename"
    | "toggleSelect"
================================================ FILE: docs/api/types/types.BoolOptionResolver.html ================================================ BoolOptionResolver | wunderbaum - v0.14.1

Type Alias BoolOptionResolver

BoolOptionResolver: (node: WunderbaumNode) => boolean

When set as option, called when the value is needed (e.g. colspan type definition).

Type declaration

================================================ FILE: docs/api/types/types.BoolOrStringOptionResolver.html ================================================ BoolOrStringOptionResolver | wunderbaum - v0.14.1

Type Alias BoolOrStringOptionResolver

BoolOrStringOptionResolver: (node: WunderbaumNode) => boolean | string

When set as option, called when the value is needed (e.g. icon type definition).

Type declaration

================================================ FILE: docs/api/types/types.CheckboxOption.html ================================================ CheckboxOption | wunderbaum - v0.14.1

Type Alias CheckboxOption

CheckboxOption: boolean | "radio"

Show/hide checkbox or display a radiobutton icon instead.

================================================ FILE: docs/api/types/types.ColumnDefinitionList.html ================================================ ColumnDefinitionList | wunderbaum - v0.14.1

Type Alias ColumnDefinitionList

ColumnDefinitionList: ColumnDefinition[]
================================================ FILE: docs/api/types/types.ColumnEventInfoMap.html ================================================ ColumnEventInfoMap | wunderbaum - v0.14.1

Type Alias ColumnEventInfoMap

ColumnEventInfoMap: { [colId: string]: ColumnEventInfo }

Type declaration

================================================ FILE: docs/api/types/types.DeprecationOptions.html ================================================ DeprecationOptions | wunderbaum - v0.14.1

Type Alias DeprecationOptionsInternal

DeprecationOptions: { hint?: string; since?: string }

Type declaration

  • Optionalhint?: string
  • Optionalsince?: string
================================================ FILE: docs/api/types/types.DndOptionsType.html ================================================ DndOptionsType | wunderbaum - v0.14.1

Type Alias DndOptionsType

DndOptionsType: {
    autoExpandMS?: 1500;
    drag?: null | (e: DragEventType) => void;
    dragEnd?: null | (e: DragEventType) => void;
    dragEnter?:
        | null
        | (
            e: DropEventType,
        ) => DropRegionType | DropRegionTypeSet | DropRegionTypeList | boolean;
    dragExpand?: null | (e: DropEventType) => boolean;
    dragLeave?: null | (e: DropEventType) => void;
    dragOver?: null | (e: DropEventType) => void;
    dragStart?: null | (e: DragEventType) => boolean;
    drop?:
        | null
        | (
            e: WbNodeEventType & {
                event: DragEvent;
                region: DropRegionType;
                sourceNode: WunderbaumNode;
                sourceNodeData: WbNodeData | null;
                suggestedDropEffect: DropEffectType;
                suggestedDropMode: InsertNodeType;
            },
        ) => void;
    dropEffectDefault?: DropEffectType;
    effectAllowed?: DropEffectAllowedType;
    guessDropEffect: boolean;
    multiSource?: false;
    preventForeignNodes?: boolean;
    preventLazyParents?: boolean;
    preventNonNodes?: boolean;
    preventRecursion?: boolean;
    preventSameParent?: boolean;
    preventVoidMoves?: boolean;
    scroll?: boolean;
    scrollSensitivity?: 20;
    scrollSpeed?: 5;
    serializeClipboardData?: | boolean
    | (nodeData: WbNodeData, node: WunderbaumNode) => string;
    sourceCopyHook?: null;
}

Type declaration

  • OptionalautoExpandMS?: 1500

    Expand nodes after n milliseconds of hovering

    1500
    
  • Optionaldrag?: null | (e: DragEventType) => void

    Callback(sourceNode, data)

    null
    
  • OptionaldragEnd?: null | (e: DragEventType) => void

    Callback(sourceNode, data)

    null
    
  • OptionaldragEnter?:
        | null
        | (
            e: DropEventType,
        ) => DropRegionType | DropRegionTypeSet | DropRegionTypeList | boolean

    Callback(targetNode, data), return true, to enable dnd drop

    null
    
  • OptionaldragExpand?: null | (e: DropEventType) => boolean

    Callback(targetNode, data), return false to prevent autoExpand

    null
    
  • OptionaldragLeave?: null | (e: DropEventType) => void

    Callback(targetNode, data)

    null
    
  • OptionaldragOver?: null | (e: DropEventType) => void

    Callback(targetNode, data)

    null
    
  • OptionaldragStart?: null | (e: DragEventType) => boolean

    Callback(sourceNode, data), return true, to enable dnd drag

    null
    
  • Optionaldrop?:
        | null
        | (
            e: WbNodeEventType & {
                event: DragEvent;
                region: DropRegionType;
                sourceNode: WunderbaumNode;
                sourceNodeData: WbNodeData | null;
                suggestedDropEffect: DropEffectType;
                suggestedDropMode: InsertNodeType;
            },
        ) => void

    Callback(targetNode, data)

    null
    
  • OptionaldropEffectDefault?: DropEffectType

    Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed. Overidable in the dragEnter or dragOver event.

    "move"
    
  • OptionaleffectAllowed?: DropEffectAllowedType

    Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)

    "all"
    
  • guessDropEffect: boolean

    Use opinionated heuristics to determine the dropEffect ('copy', 'link', or 'move') based on effectAllowed, dropEffectDefault, and modifier keys. This is recalculated before each dragEnter and dragOver event and can be overridden there.

    true
    
  • OptionalmultiSource?: false

    true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed

    false
    
  • OptionalpreventForeignNodes?: boolean

    Prevent dropping nodes from different Wunderbaum trees

    false
    
  • OptionalpreventLazyParents?: boolean

    Prevent dropping items on unloaded lazy Wunderbaum tree nodes

    true
    
  • OptionalpreventNonNodes?: boolean

    Prevent dropping items other than Wunderbaum tree nodes

    false
    
  • OptionalpreventRecursion?: boolean

    Prevent dropping nodes on own descendants

    true
    
  • OptionalpreventSameParent?: boolean

    Prevent dropping nodes under same direct parent

    false
    
  • OptionalpreventVoidMoves?: boolean

    Prevent dropping nodes 'before self', etc. (move only)

    true
    
  • Optionalscroll?: boolean

    Enable auto-scrolling while dragging

    true
    
  • OptionalscrollSensitivity?: 20

    Active top/bottom margin in pixel

    20
    
  • OptionalscrollSpeed?: 5

    Pixel per event

    5
    
  • OptionalserializeClipboardData?: boolean | (nodeData: WbNodeData, node: WunderbaumNode) => string

    Serialize Node Data to datatransfer object

    true
    
  • OptionalsourceCopyHook?: null

    Optional callback passed to toDict on dragStart

    null
    
================================================ FILE: docs/api/types/types.DropEffectAllowedType.html ================================================ DropEffectAllowedType | wunderbaum - v0.14.1

Type Alias DropEffectAllowedType

DropEffectAllowedType:
    | "none"
    | "copy"
    | "copyLink"
    | "copyMove"
    | "link"
    | "linkMove"
    | "move"
    | "all"
================================================ FILE: docs/api/types/types.DropEffectType.html ================================================ DropEffectType | wunderbaum - v0.14.1

Type Alias DropEffectType

DropEffectType: "none" | "copy" | "link" | "move"
================================================ FILE: docs/api/types/types.DropRegionType.html ================================================ DropRegionType | wunderbaum - v0.14.1

Type Alias DropRegionType

DropRegionType: "over" | "before" | "after"
================================================ FILE: docs/api/types/types.DropRegionTypeList.html ================================================ DropRegionTypeList | wunderbaum - v0.14.1

Type Alias DropRegionTypeList

DropRegionTypeList: DropRegionType[]
================================================ FILE: docs/api/types/types.DropRegionTypeSet.html ================================================ DropRegionTypeSet | wunderbaum - v0.14.1

Type Alias DropRegionTypeSet

DropRegionTypeSet: Set<DropRegionType>
================================================ FILE: docs/api/types/types.DynamicBoolOption.html ================================================ DynamicBoolOption | wunderbaum - v0.14.1

Type Alias DynamicBoolOption

DynamicBoolOption: boolean | BoolOptionResolver

See also WunderbaumNode.getOption() to evaluate node.NAME setting and tree.types[node.type].NAME.

================================================ FILE: docs/api/types/types.DynamicBoolOrStringOption.html ================================================ DynamicBoolOrStringOption | wunderbaum - v0.14.1

Type Alias DynamicBoolOrStringOption

DynamicBoolOrStringOption: boolean | string | BoolOrStringOptionResolver
================================================ FILE: docs/api/types/types.DynamicCheckboxOption.html ================================================ DynamicCheckboxOption | wunderbaum - v0.14.1

Type Alias DynamicCheckboxOption

================================================ FILE: docs/api/types/types.DynamicIconOption.html ================================================ DynamicIconOption | wunderbaum - v0.14.1

Type Alias DynamicIconOption

================================================ FILE: docs/api/types/types.DynamicStringOption.html ================================================ DynamicStringOption | wunderbaum - v0.14.1

Type Alias DynamicStringOption

DynamicStringOption: string | BoolOptionResolver
================================================ FILE: docs/api/types/types.DynamicTooltipOption.html ================================================ DynamicTooltipOption | wunderbaum - v0.14.1

Type Alias DynamicTooltipOption

================================================ FILE: docs/api/types/types.EditOptionsType.html ================================================ EditOptionsType | wunderbaum - v0.14.1

Type Alias EditOptionsType

EditOptionsType: {
    apply?:
        | null
        | (e: WbNodeEventType & { inputElem: HTMLInputElement }) => any
        | Promise<any>;
    beforeEdit?: null | (e: WbNodeEventType) => boolean | string;
    debounce?: number;
    edit?:
        | null
        | (e: WbNodeEventType & { inputElem: HTMLInputElement }) => void;
    maxlength?: null | number;
    minlength?: number;
    select?: boolean;
    slowClickDelay?: number;
    trigger?: string[];
    trim?: boolean;
    validity?: boolean;
}

Note:
This options are used for renaming node titles.
There is also the tree.change event to handle modifying node data from input controls that are embedded in grid cells.

Type declaration

  • Optionalapply?:
        | null
        | (e: WbNodeEventType & { inputElem: HTMLInputElement }) => any
        | Promise<any>
  • OptionalbeforeEdit?: null | (e: WbNodeEventType) => boolean | string

    beforeEdit(e) may return an input HTML string. Otherwise use a default.

  • Optionaldebounce?: number

    Used to debounce the change event handler for grid cells [ms].

    100
    
  • Optionaledit?: null | (e: WbNodeEventType & { inputElem: HTMLInputElement }) => void
  • Optionalmaxlength?: null | number

    Maximum number of characters allowed for node title input field.

    null;
    
  • Optionalminlength?: number

    Minimum number of characters required for node title input field.

    1
    
  • Optionalselect?: boolean

    Select all text of a node title, so it can be overwritten by typing.

    true
    
  • OptionalslowClickDelay?: number

    Handle 'clickActive' only if last click is less than this ms old (0: always)

    1000
    
  • Optionaltrigger?: string[]

    Array of strings to determine which user input should trigger edit mode. E.g. ["clickActive", "F2", "macEnter"]:
    'clickActive': single click on active node title
    'F2': press F2 key
    'macEnter': press Enter (on macOS only)
    Pass an empty array to disable edit mode.

    []
    
  • Optionaltrim?: boolean

    Trim whitespace before saving a node title.

    true
    
  • Optionalvalidity?: boolean

    Permanently apply node title input validations (CSS and tooltip) on keydown.

    true
    
================================================ FILE: docs/api/types/types.FilterModeType.html ================================================ FilterModeType | wunderbaum - v0.14.1

Type Alias FilterModeType

FilterModeType: null | "mark" | "dim" | "hide"
================================================ FILE: docs/api/types/types.FilterOptionsType.html ================================================ FilterOptionsType | wunderbaum - v0.14.1

Type Alias FilterOptionsType

FilterOptionsType: { autoApply?: boolean; connect?: null | FilterConnectType } & FilterNodesOptions

Passed as tree options to configure default filtering behavior.

Type declaration

  • OptionalautoApply?: boolean

    Re-apply last filter if lazy data is loaded

    true
    
  • Optional Experimentalconnect?: null | FilterConnectType

    Element or selector of input controls and buttons for filter query strings.

    0.13

    null
    
================================================ FILE: docs/api/types/types.GridOptionsType.html ================================================ GridOptionsType | wunderbaum - v0.14.1

Type Alias GridOptionsType

GridOptionsType: object
================================================ FILE: docs/api/types/types.IconOption.html ================================================ IconOption | wunderbaum - v0.14.1

Type Alias IconOption

IconOption: boolean | string

An icon may either be a string-tag that references an entry in the iconMap (e.g. "folderOpen")), an HTML string that contains a < and is used as-is, an image URL string that contains a . or / and is rendered as <img src='...'>, a class string such as "bi bi-folder", or a boolean value that indicates if the default icon should be used or hidden.

================================================ FILE: docs/api/types/types.InsertNodeType.html ================================================ InsertNodeType | wunderbaum - v0.14.1

Type Alias InsertNodeType

InsertNodeType: "before" | "after" | "prependChild" | "appendChild"
================================================ FILE: docs/api/types/types.KeynavOptionsType.html ================================================ KeynavOptionsType | wunderbaum - v0.14.1

Type Alias KeynavOptionsType

KeynavOptionsType: object
================================================ FILE: docs/api/types/types.LoggerOptionsType.html ================================================ LoggerOptionsType | wunderbaum - v0.14.1

Type Alias LoggerOptionsType

LoggerOptionsType: object
================================================ FILE: docs/api/types/types.MatcherCallback.html ================================================ MatcherCallback | wunderbaum - v0.14.1

Type Alias MatcherCallback

MatcherCallback: (node: WunderbaumNode) => boolean

Passed to find...() methods. Should return true if node matches.

Type declaration

================================================ FILE: docs/api/types/types.NavigationType.html ================================================ NavigationType | wunderbaum - v0.14.1

Type Alias NavigationType

NavigationType:
    | "down"
    | "first"
    | "firstCol"
    | "last"
    | "lastCol"
    | "left"
    | "nextMatch"
    | "pageDown"
    | "pageUp"
    | "parent"
    | "prevMatch"
    | "right"
    | "up"
================================================ FILE: docs/api/types/types.NodeAnyCallback.html ================================================ NodeAnyCallback | wunderbaum - v0.14.1

Type Alias NodeAnyCallback

NodeAnyCallback: (node: WunderbaumNode) => any

A callback that receives a node instance and returns an arbitrary value type.

Type declaration

================================================ FILE: docs/api/types/types.NodeFilterCallback.html ================================================ NodeFilterCallback | wunderbaum - v0.14.1

Type Alias NodeFilterCallback

NodeFilterCallback: (node: WunderbaumNode) => NodeFilterResponse

Type declaration

================================================ FILE: docs/api/types/types.NodeFilterResponse.html ================================================ NodeFilterResponse | wunderbaum - v0.14.1

Type Alias NodeFilterResponse

NodeFilterResponse: "skip" | "branch" | boolean | void
================================================ FILE: docs/api/types/types.NodePropertyGetterCallback.html ================================================ NodePropertyGetterCallback | wunderbaum - v0.14.1

Type Alias NodePropertyGetterCallback

NodePropertyGetterCallback: (node: WunderbaumNode, propName: string) => any

A callback that receives a node instance and property name returns a value.

Type declaration

================================================ FILE: docs/api/types/types.NodeSelectCallback.html ================================================ NodeSelectCallback | wunderbaum - v0.14.1

Type Alias NodeSelectCallback

NodeSelectCallback: (node: WunderbaumNode) => boolean | void

A callback that receives a node instance and may returnsa false to prevent (de)selection.

Type declaration

================================================ FILE: docs/api/types/types.NodeStringCallback.html ================================================ NodeStringCallback | wunderbaum - v0.14.1

Type Alias NodeStringCallback

NodeStringCallback: (node: WunderbaumNode) => string

A callback that receives a node instance and returns a string value.

Type declaration

================================================ FILE: docs/api/types/types.NodeToDictCallback.html ================================================ NodeToDictCallback | wunderbaum - v0.14.1

Type Alias NodeToDictCallback

NodeToDictCallback: (
    dict: WbNodeData,
    node: WunderbaumNode,
) => NodeVisitResponse

A callback that receives a node-data dictionary and a node instance and returns an iteration modifier.

Type declaration

================================================ FILE: docs/api/types/types.NodeTypeDefinitionMap.html ================================================ NodeTypeDefinitionMap | wunderbaum - v0.14.1

Type Alias NodeTypeDefinitionMap

NodeTypeDefinitionMap: { [type: string]: NodeTypeDefinition }

Type declaration

================================================ FILE: docs/api/types/types.NodeVisitCallback.html ================================================ NodeVisitCallback | wunderbaum - v0.14.1

Type Alias NodeVisitCallback

NodeVisitCallback: (node: WunderbaumNode) => NodeVisitResponse

A callback that receives a node instance and returns an iteration modifier.

Type declaration

================================================ FILE: docs/api/types/types.NodeVisitResponse.html ================================================ NodeVisitResponse | wunderbaum - v0.14.1

Type Alias NodeVisitResponse

NodeVisitResponse: "skip" | boolean | void

Returned by NodeVisitCallback to control iteration. false stops iteration, skip skips descendants but continues. All other values continue iteration.

================================================ FILE: docs/api/types/types.SelectModeType.html ================================================ SelectModeType | wunderbaum - v0.14.1

Type Alias SelectModeType

SelectModeType: "single" | "multi" | "hier"
================================================ FILE: docs/api/types/types.SortByPropertyOptions.html ================================================ SortByPropertyOptions | wunderbaum - v0.14.1

Type Alias SortByPropertyOptions

SortByPropertyOptions: SortOptions

Possible values for WunderbaumNode.sortByProperty options argument.

================================================ FILE: docs/api/types/types.SortCallback.html ================================================ SortCallback | wunderbaum - v0.14.1

Type Alias SortCallback

SortCallback: (a: WunderbaumNode, b: WunderbaumNode) => number

Passed to sort() methods. Should return -1, 0, or 1.

Type declaration

Use SortKeyCallback instead

================================================ FILE: docs/api/types/types.SortKeyCallback.html ================================================ SortKeyCallback | wunderbaum - v0.14.1

Type Alias SortKeyCallback

SortKeyCallback: (node: WunderbaumNode) => string | number | any[]

Passed to sort() methods. Should return a representation that can be compared using <.

Type declaration

================================================ FILE: docs/api/types/types.SortOrderType.html ================================================ SortOrderType | wunderbaum - v0.14.1

Type Alias SortOrderType

SortOrderType: "asc" | "desc" | undefined

A value that can either be true, false, or undefined.

================================================ FILE: docs/api/types/types.SourceListType.html ================================================ SourceListType | wunderbaum - v0.14.1

Type Alias SourceListType

SourceListType: WbNodeData[]
================================================ FILE: docs/api/types/types.SourceType.html ================================================ SourceType | wunderbaum - v0.14.1

Type Alias SourceType

Possible initilization for tree nodes.

================================================ FILE: docs/api/types/types.TooltipOption.html ================================================ TooltipOption | wunderbaum - v0.14.1

Type Alias TooltipOption

TooltipOption: boolean | string

Show/hide default tooltip or display a string.

================================================ FILE: docs/api/types/types.TranslationsType.html ================================================ TranslationsType | wunderbaum - v0.14.1

Type Alias TranslationsType

TranslationsType: {
    breadcrumbDelimiter: string;
    loadError: string;
    loading: string;
    matchIndex: string;
    noData: string;
    noMatch: string;
    queryResult: string;
}

Translatable strings.

Type declaration

  • breadcrumbDelimiter: string
    " » "
    
  • loadError: string
    "Error"
    
  • loading: string
    "Loading..."
    
  • matchIndex: string
    "${match} of ${matches}"
    
  • noData: string
    "No data"
    
  • noMatch: string
    "No result"
    
  • queryResult: string
    "Found ${matches} of ${count}"
    
================================================ FILE: docs/api/types/types.TristateType.html ================================================ TristateType | wunderbaum - v0.14.1

Type Alias TristateType

TristateType: boolean | undefined

A value that can either be true, false, or undefined.

================================================ FILE: docs/api/types/types.WbCancelableEventResultType.html ================================================ WbCancelableEventResultType | wunderbaum - v0.14.1

Type Alias WbCancelableEventResultType

WbCancelableEventResultType: false | void

Retuen value of an event handler that can return false to prevent the default action.

================================================ FILE: docs/api/types/types.WbIconBadgeCallback.html ================================================ WbIconBadgeCallback | wunderbaum - v0.14.1

Type Alias WbIconBadgeCallback

Used for tree.iconBadge event.

================================================ FILE: docs/api/types/util.EventCallbackType.html ================================================ EventCallbackType | wunderbaum - v0.14.1

Type Alias EventCallbackType

EventCallbackType: (e: Event) => boolean | void

Type declaration

    • (e: Event): boolean | void
    • Parameters

      • e: Event

      Returns boolean | void

================================================ FILE: docs/api/types/util.FunctionType.html ================================================ FunctionType | wunderbaum - v0.14.1

Type Alias FunctionType

FunctionType: (...args: any[]) => any

Type declaration

    • (...args: any[]): any
    • Parameters

      • ...args: any[]

      Returns any

================================================ FILE: docs/api/types/util.NotPromise.html ================================================ NotPromise | wunderbaum - v0.14.1

Type Alias NotPromise<T>

NotPromise: T extends Promise<any> ? never : T

Type Parameters

  • T
================================================ FILE: docs/api/variables/common.DEFAULT_DEBUGLEVEL.html ================================================ DEFAULT_DEBUGLEVEL | wunderbaum - v0.14.1

Variable DEFAULT_DEBUGLEVELConst

DEFAULT_DEBUGLEVEL: 4
================================================ FILE: docs/api/variables/common.DEFAULT_MIN_COL_WIDTH.html ================================================ DEFAULT_MIN_COL_WIDTH | wunderbaum - v0.14.1

Variable DEFAULT_MIN_COL_WIDTHConst

DEFAULT_MIN_COL_WIDTH: 4

Minimum column width if not set otherwise.

================================================ FILE: docs/api/variables/common.DEFAULT_ROW_HEIGHT.html ================================================ DEFAULT_ROW_HEIGHT | wunderbaum - v0.14.1

Variable DEFAULT_ROW_HEIGHTConst

DEFAULT_ROW_HEIGHT: 22

Fixed height of a row in pixel. Must match the SCSS variable $row-outer-height.

================================================ FILE: docs/api/variables/common.ICON_WIDTH.html ================================================ ICON_WIDTH | wunderbaum - v0.14.1

Variable ICON_WIDTHConst

ICON_WIDTH: 20

Fixed width of node icons in pixel. Must match the SCSS variable $icon-outer-width.

================================================ FILE: docs/api/variables/common.INPUT_KEYS.html ================================================ INPUT_KEYS | wunderbaum - v0.14.1

Variable INPUT_KEYSConst

INPUT_KEYS: { [key: string]: string[] } = ...

Define which keys are handled by embedded control, and should not be passed to tree navigation handler in cell-edit mode.

Type declaration

  • [key: string]: string[]
================================================ FILE: docs/api/variables/common.KEY_NODATA.html ================================================ KEY_NODATA | wunderbaum - v0.14.1

Variable KEY_NODATAConst

KEY_NODATA: "__not_found__"
================================================ FILE: docs/api/variables/common.KEY_TO_COMMAND_MAP.html ================================================ KEY_TO_COMMAND_MAP | wunderbaum - v0.14.1

Variable KEY_TO_COMMAND_MAPConst

KEY_TO_COMMAND_MAP: { [key: string]: ApplyCommandType } = ...

Map KeyEvent.key to navigation action.

Type declaration

================================================ FILE: docs/api/variables/common.KEY_TO_NAVIGATION_MAP.html ================================================ KEY_TO_NAVIGATION_MAP | wunderbaum - v0.14.1

Variable KEY_TO_NAVIGATION_MAPConst

KEY_TO_NAVIGATION_MAP: { [key: string]: NavigationType } = ...

Map KeyEvent.key to navigation action.

Type declaration

================================================ FILE: docs/api/variables/common.NODE_TYPE_FOLDER.html ================================================ NODE_TYPE_FOLDER | wunderbaum - v0.14.1

Variable NODE_TYPE_FOLDERConst

NODE_TYPE_FOLDER: "folder"

A value for node.type that by convention may be used to mark a node as directory. It may be used to sort 'directories' to the top.

================================================ FILE: docs/api/variables/common.RENDER_MAX_PREFETCH.html ================================================ RENDER_MAX_PREFETCH | wunderbaum - v0.14.1

Variable RENDER_MAX_PREFETCHConst

RENDER_MAX_PREFETCH: 5

Render row markup for N nodes above and below the visible viewport.

================================================ FILE: docs/api/variables/common.RENDER_MIN_PREFETCH.html ================================================ RENDER_MIN_PREFETCH | wunderbaum - v0.14.1

Variable RENDER_MIN_PREFETCHConst

RENDER_MIN_PREFETCH: 5

Skip rendering new rows when we have at least N nodes rendeed above and below the viewport.

================================================ FILE: docs/api/variables/common.RESERVED_TREE_SOURCE_KEYS.html ================================================ RESERVED_TREE_SOURCE_KEYS | wunderbaum - v0.14.1

Variable RESERVED_TREE_SOURCE_KEYSConst

RESERVED_TREE_SOURCE_KEYS: Set<string> = ...

Dict keys that are evaluated by source loader (others are added to tree.data instead).

================================================ FILE: docs/api/variables/common.TEST_FILE_PATH.html ================================================ TEST_FILE_PATH | wunderbaum - v0.14.1

Variable TEST_FILE_PATHConst

TEST_FILE_PATH: RegExp = ...

Regular expression to detect if a string describes an image URL (in contrast to a class name). Strings are considered image urls if they contain '.' or '/'. < is ignored, because it is probably an html tag.

================================================ FILE: docs/api/variables/common.TEST_HTML.html ================================================ TEST_HTML | wunderbaum - v0.14.1

Variable TEST_HTMLConst

TEST_HTML: RegExp = ...

Regular expression to detect if a string describes an HTML element.

================================================ FILE: docs/api/variables/common.TITLE_SPAN_PAD_Y.html ================================================ TITLE_SPAN_PAD_Y | wunderbaum - v0.14.1

Variable TITLE_SPAN_PAD_YConst

TITLE_SPAN_PAD_Y: 7

Adjust the width of the title span, so overflow ellipsis work. (2 x $col-padding-x + 3px rounding errors).

================================================ FILE: docs/api/variables/common.defaultIconMaps.html ================================================ defaultIconMaps | wunderbaum - v0.14.1

Variable defaultIconMapsConst

defaultIconMaps: { [key in IconLibrary]: IconMapType } = ...

Default node icons for icon libraries

================================================ FILE: docs/api/variables/util.MAX_INT.html ================================================ MAX_INT | wunderbaum - v0.14.1

Variable MAX_INTConst

MAX_INT: 9007199254740991
================================================ FILE: docs/api/variables/util.MOUSE_BUTTONS.html ================================================ MOUSE_BUTTONS | wunderbaum - v0.14.1

Variable MOUSE_BUTTONSConst

MOUSE_BUTTONS: { [key: number]: string } = ...

Readable names for MouseEvent.button

Type declaration

  • [key: number]: string
================================================ FILE: docs/api/variables/util.isMac.html ================================================ isMac | wunderbaum - v0.14.1

Variable isMacConst

isMac: boolean = userInfo.isMac

True if the client is using a macOS platform.

================================================ FILE: docs/assets/favicon/site.webmanifest ================================================ {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} ================================================ FILE: docs/assets/json/ajax-lazy-products.json ================================================ [ { "title": "SubNode 1 (lazy)", "type": "folder", "lazy": true }, { "title": "SubNode 2", "author": "n.n", "year": 1999, "qty": 0, "price": 1.99, "details": "(Lazily loaded)"} ] ================================================ FILE: docs/assets/json/ajax-tree-products.json ================================================ [ { "title": "Books", "type": "folder", "expanded": true, "children": [ { "title": "Art of War", "type": "book", "author": "Sun Tzu", "year": -500, "qty": 21, "price": 5.95 }, { "title": "The Hobbit", "type": "book", "author": "J.R.R. Tolkien", "year": 1937, "qty": 32, "price": 8.97 }, { "title": "The Little Prince", "type": "book", "author": "Antoine de Saint-Exupery", "year": 1943, "qty": 2946, "price": 6.82 }, { "title": "Don Quixote", "type": "book", "author": "Miguel de Cervantes", "year": 1615, "qty": 932, "price": 15.99 } ] }, { "title": "Music", "type": "folder", "children": [ { "title": "Nevermind", "type": "music", "author": "Nirvana", "year": 1991, "qty": 916, "price": 15.95 }, { "title": "Autobahn", "type": "music", "author": "Kraftwerk", "year": 1974, "qty": 2261, "price": 23.98 }, { "title": "Kind of Blue", "type": "music", "author": "Miles Davis", "year": 1959, "qty": 9735, "price": 21.9 }, { "title": "Back in Black", "type": "music", "author": "AC/DC", "year": 1980, "qty": 3895, "price": 17.99 }, { "title": "The Dark Side of the Moon", "type": "music", "author": "Pink Floyd", "year": 1973, "qty": 263, "price": 17.99 }, { "title": "Sgt. Pepper's Lonely Hearts Club Band", "type": "music", "author": "The Beatles", "year": 1967, "qty": 521, "price": 13.98 } ] }, { "title": "Electronics & Computers", "expanded": true, "type": "folder", "children": [ { "title": "Cell Phones", "type": "folder", "children": [ { "title": "Moto G", "type": "phone", "author": "Motorola", "year": 2014, "qty": 332, "price": 224.99 }, { "title": "Galaxy S8", "type": "phone", "author": "Samsung", "year": 2016, "qty": 952, "price": 509.99 }, { "title": "iPhone SE", "type": "phone", "author": "Apple", "year": 2016, "qty": 444, "price": 282.75 }, { "title": "G6", "type": "phone", "author": "LG", "year": 2017, "qty": 951, "price": 309.99 }, { "title": "Lumia", "type": "phone", "author": "Microsoft", "year": 2014, "qty": 32, "price": 205.95 }, { "title": "Xperia", "type": "phone", "author": "Sony", "year": 2014, "qty": 77, "price": 195.95 }, { "title": "3210", "type": "phone", "author": "Nokia", "year": 1999, "qty": 3, "price": 85.99 } ] }, { "title": "Computers", "type": "folder", "children": [ { "title": "ThinkPad", "type": "computer", "author": "IBM", "year": 1992, "qty": 16, "price": 749.9 }, { "title": "C64", "type": "computer", "author": "Commodore", "year": 1982, "qty": 83, "price": 595.0 }, { "title": "MacBook Pro", "type": "computer", "author": "Apple", "year": 2006, "qty": 482, "price": 1949.95 }, { "title": "Sinclair ZX Spectrum", "type": "computer", "author": "Sinclair Research", "year": 1982, "qty": 1, "price": 529 }, { "title": "Apple II", "type": "computer", "author": "Apple", "year": 1977, "qty": 17, "price": 1298 }, { "title": "PC AT", "type": "computer", "author": "IBM", "year": 1984, "qty": 3, "price": 1235.0 } ] } ] }, { "title": "More...", "type": "folder", "lazy": true } ] ================================================ FILE: docs/assets/json/ajax-tree-products_t.json ================================================ { "types": { "folder": { "colspan": true }, "book": { "icon": "bi bi-book" }, "computer": { "icon": "bi bi-laptop" }, "music": { "icon": "bi bi-disc" }, "phone": { "icon": "bi bi-phone" } }, "children": [ { "title": "Books", "type": "folder", "expanded": true, "children": [ { "title": "Art of War", "type": "book", "author": "Sun Tzu", "year": -500, "qty": 21, "price": 5.95 }, { "title": "The Hobbit", "type": "book", "author": "J.R.R. Tolkien", "year": 1937, "qty": 32, "price": 8.97 }, { "title": "The Little Prince", "type": "book", "author": "Antoine de Saint-Exupery", "year": 1943, "qty": 2946, "price": 6.82 }, { "title": "Don Quixote", "type": "book", "author": "Miguel de Cervantes", "year": 1615, "qty": 932, "price": 15.99 } ] }, { "title": "Music", "type": "folder", "children": [ { "title": "Nevermind", "type": "music", "author": "Nirvana", "year": 1991, "qty": 916, "price": 15.95 }, { "title": "Autobahn", "type": "music", "author": "Kraftwerk", "year": 1974, "qty": 2261, "price": 23.98 }, { "title": "Kind of Blue", "type": "music", "author": "Miles Davis", "year": 1959, "qty": 9735, "price": 21.9 }, { "title": "Back in Black", "type": "music", "author": "AC/DC", "year": 1980, "qty": 3895, "price": 17.99 }, { "title": "The Dark Side of the Moon", "type": "music", "author": "Pink Floyd", "year": 1973, "qty": 263, "price": 17.99 }, { "title": "Sgt. Pepper's Lonely Hearts Club Band", "type": "music", "author": "The Beatles", "year": 1967, "qty": 521, "price": 13.98 } ] }, { "title": "Electronics & Computers", "expanded": true, "type": "folder", "children": [ { "title": "Cell Phones", "type": "folder", "children": [ { "title": "Moto G", "type": "phone", "author": "Motorola", "year": 2014, "qty": 332, "price": 224.99 }, { "title": "Galaxy S8", "type": "phone", "author": "Samsung", "year": 2016, "qty": 952, "price": 509.99 }, { "title": "iPhone SE", "type": "phone", "author": "Apple", "year": 2016, "qty": 444, "price": 282.75 }, { "title": "G6", "type": "phone", "author": "LG", "year": 2017, "qty": 951, "price": 309.99 }, { "title": "Lumia", "type": "phone", "author": "Microsoft", "year": 2014, "qty": 32, "price": 205.95 }, { "title": "Xperia", "type": "phone", "author": "Sony", "year": 2014, "qty": 77, "price": 195.95 }, { "title": "3210", "type": "phone", "author": "Nokia", "year": 1999, "qty": 3, "price": 85.99 } ] }, { "title": "Computers", "type": "folder", "children": [ { "title": "ThinkPad", "type": "computer", "author": "IBM", "year": 1992, "qty": 16, "price": 749.9 }, { "title": "C64", "type": "computer", "author": "Commodore", "year": 1982, "qty": 83, "price": 595.0 }, { "title": "MacBook Pro", "type": "computer", "author": "Apple", "year": 2006, "qty": 482, "price": 1949.95 }, { "title": "Sinclair ZX Spectrum", "type": "computer", "author": "Sinclair Research", "year": 1982, "qty": 1, "price": 529 }, { "title": "Apple II", "type": "computer", "author": "Apple", "year": 1977, "qty": 17, "price": 1298 }, { "title": "PC AT", "type": "computer", "author": "IBM", "year": 1984, "qty": 3, "price": 1235.0 } ] } ] }, { "title": "More...", "type": "folder", "lazy": true } ] } ================================================ FILE: docs/assets/json/fixture_department_1k_3_6_p.json ================================================ [{"title": "Dept. for Fails and Pounds", "type": "department", "children": [{"title": "Forget visuals", "type": "role"}, {"title": "Treasure designers", "type": "role", "expanded": true, "children": [{"title": "Zoe M. Rampling", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1048028400000.0}, {"title": "Carolyn Wilkins", "type": "person", "avail": true, "age": 83, "date": 879375600000.0}, {"title": "Diane Vance", "type": "person", "avail": true, "age": 66, "date": 289609200000.0, "remarks": "Improves desires helpfully."}, {"title": "Lauren Rees", "type": "person", "state": "s", "avail": true, "age": 57, "remarks": "Imposes birthdaies unbelievably."}, {"title": "Audrey Miller", "type": "person", "avail": true, "age": 77, "date": 432252000000.0}, {"title": "David Marshall", "type": "person", "state": "s", "avail": true, "age": 43}, {"title": "Richard Hemmings", "type": "person", "avail": true, "age": 61, "date": 1106262000000.0, "remarks": "Implants bands greatly."}, {"title": "Anthony C. Peake", "type": "person", "avail": true, "age": 48, "remarks": "Stirs antelopes zestfully."}, {"title": "Alan Terry", "type": "person", "state": "h", "avail": true, "age": 97, "date": 1220479200000.0}, {"title": "Max May", "type": "person", "avail": true, "age": 61, "date": 1572303600000.0, "remarks": "Measures middles warmly."}, {"title": "Nicola G. Scott", "type": "person", "avail": true, "age": 66}, {"title": "Ian C. Welch", "type": "person", "avail": true, "age": 76, "date": 926546400000.0}, {"title": "Christian G. Glover", "type": "person", "state": "h", "avail": true, "age": 45, "date": 1080428400000.0}, {"title": "Corey A. Alsop", "type": "person", "avail": true, "age": 29}, {"title": "Paul McDonald", "type": "person", "age": 75, "remarks": "Understands moms amazingly."}, {"title": "Chris Lewis", "type": "person", "age": 62, "date": 1059516000000.0}, {"title": "Taylor C. Lyman", "type": "person", "avail": true, "age": 91, "date": 1500933600000.0}]}, {"title": "Point securities", "type": "role", "children": [{"title": "Jamie A. Gill", "type": "person", "state": "h", "avail": true, "age": 92}, {"title": "Una Sutherland", "type": "person", "avail": true, "age": 93, "date": 687222000000.0}, {"title": "Ian D. Hamilton", "type": "person", "avail": true, "age": 44, "date": 574383600000.0, "remarks": "Closes fails carefully."}, {"title": "Felicity Morrison", "type": "person", "avail": true, "age": 91, "date": 1028844000000.0}, {"title": "Eric Glover", "type": "person", "avail": true, "age": 21}, {"title": "Tracey I. Tucker", "type": "person", "avail": true, "age": 65, "remarks": "Deprives notes considerately."}, {"title": "Julian S. Walker", "type": "person", "avail": true, "age": 56}, {"title": "Taylor Nash", "type": "person", "avail": true, "age": 30, "date": 1272751200000.0, "remarks": "Caresses beginnings truly."}, {"title": "Pippa Y. Piper", "type": "person", "avail": true, "age": 64}, {"title": "Kelly C. Buckland", "type": "person", "avail": true, "age": 66}, {"title": "Victoria Johnston", "type": "person", "avail": true, "age": 63, "date": 1630274400000.0}, {"title": "William Z. Turner", "type": "person", "state": "h", "avail": true, "age": 81, "remarks": "Presets healths poignantly."}, {"title": "Michelle W. Ross", "type": "person", "avail": true, "age": 73, "date": 583365600000.0}]}, {"title": "Resell courages", "type": "role", "expanded": true, "children": [{"title": "Gavin S. Rees", "type": "person", "avail": true, "age": 74}, {"title": "Casey R. Poole", "type": "person", "avail": true, "age": 33, "date": 1363993200000.0}, {"title": "Ruth S. Oliver", "type": "person", "avail": true, "age": 34, "date": 273452400000.0}, {"title": "Taylor Anderson", "type": "person", "state": "s", "avail": true, "age": 66, "date": 805068000000.0, "remarks": "Treads texts gleefully."}, {"title": "Una Sanderson", "type": "person", "avail": true, "age": 85, "date": 1275602400000.0}, {"title": "Sam D. Terry", "type": "person", "avail": true, "age": 78, "date": 481932000000.0}, {"title": "John James", "type": "person", "avail": true, "age": 26, "remarks": "Loves kids markedly."}, {"title": "Carolyn C. Davidson", "type": "person", "age": 31}, {"title": "Kyle A. Reid", "type": "person", "avail": true, "age": 54, "date": 1499119200000.0, "remarks": "Melts sells markedly."}, {"title": "Keith Bond", "type": "person", "state": "h", "avail": true, "age": 54, "date": 38358000000.0}, {"title": "Penelope A. Rampling", "type": "person", "avail": true, "age": 95}, {"title": "Deirdre C. Skinner", "type": "person", "avail": true, "age": 50, "date": 927410400000.0}, {"title": "Leonard Slater", "type": "person", "avail": true, "age": 40, "date": 1561586400000.0, "remarks": "Feels weathers graciously."}, {"title": "Austin Sutherland", "type": "person", "avail": true, "age": 47}, {"title": "Charles Lyman", "type": "person", "avail": true, "age": 40, "date": 571964400000.0}, {"title": "Nathan N. Ross", "type": "person", "state": "s", "avail": true, "age": 52}, {"title": "Chris Oliver", "type": "person", "state": "s", "avail": true, "age": 30, "remarks": "Bends ages appropriately."}, {"title": "Sydney F. Wilkins", "type": "person", "avail": true, "age": 66, "date": 1041030000000.0}, {"title": "Corey D. Bailey", "type": "person", "avail": true, "age": 92}]}, {"title": "Salve hours", "type": "role", "children": [{"title": "Jasmine Lawrence", "type": "person", "avail": true, "age": 50}, {"title": "Lauren B. Hudson", "type": "person", "avail": true, "age": 96}]}, {"title": "Detach fews", "type": "role", "children": [{"title": "Ian Black", "type": "person", "age": 45, "remarks": "Suffers failures upbeat."}, {"title": "Joan P. May", "type": "person", "age": 22}, {"title": "Blake P. Murray", "type": "person", "avail": true, "age": 45, "date": 118105200000.0, "remarks": "Ponders priorities appropriately."}, {"title": "Stephanie Piper", "type": "person", "avail": true, "age": 54, "date": 185670000000.0}, {"title": "Stephanie Mitchell", "type": "person", "state": "s", "avail": true, "age": 27}, {"title": "Alexandra Arnold", "type": "person", "age": 41}, {"title": "Fiona U. Sharp", "type": "person", "state": "s", "avail": true, "age": 59, "date": 143247600000.0, "remarks": "Wrings marriages appropriately."}, {"title": "Anthony G. Paterson", "type": "person", "avail": true, "age": 80, "date": 345164400000.0, "remarks": "Stores motors freely."}, {"title": "Austin G. Hodges", "type": "person", "avail": true, "age": 87, "date": 597884400000.0}, {"title": "Faith U. Dowd", "type": "person", "avail": true, "age": 77, "date": 472604400000.0}, {"title": "Cameron X. Welch", "type": "person", "state": "h", "avail": true, "age": 53, "date": 879634800000.0, "remarks": "Sings guidances efficiently."}, {"title": "Jack K. Jones", "type": "person", "avail": true, "age": 63, "date": 1489532400000.0, "remarks": "Lands lawyers happily."}, {"title": "Chloe Buckland", "type": "person", "avail": true, "age": 53, "date": 525564000000.0}, {"title": "Emma Tucker", "type": "person", "state": "s", "age": 49, "date": 1467324000000.0}, {"title": "Colin Davidson", "type": "person", "avail": true, "age": 48, "date": 1069110000000.0}, {"title": "Lou MacDonald", "type": "person", "state": "s", "avail": true, "age": 33, "date": 588895200000.0, "remarks": "Consoles charities generously."}]}, {"title": "Indulge fishings", "type": "role", "expanded": true, "children": [{"title": "Austin North", "type": "person", "state": "s", "avail": true, "age": 49, "date": 941497200000.0}, {"title": "Jamie Mackay", "type": "person", "age": 89, "remarks": "Chips tooths substantially."}, {"title": "Andrew Brown", "type": "person", "state": "s", "avail": true, "age": 83, "date": 1208469600000.0}, {"title": "Stephanie W. Fraser", "type": "person", "avail": true, "age": 83, "date": 725324400000.0}, {"title": "Donna Z. Chapman", "type": "person", "avail": true, "age": 97, "date": 918255600000.0, "remarks": "Smashes ages touchingly."}, {"title": "Deirdre J. Wallace", "type": "person", "state": "s", "avail": true, "age": 70}, {"title": "Eddie Alsop", "type": "person", "state": "s", "avail": true, "age": 42}, {"title": "Kylie Ince", "type": "person", "avail": true, "age": 25, "remarks": "Moulds positives exclusively."}, {"title": "Liam Hardacre", "type": "person", "avail": true, "age": 62}, {"title": "John G. Knox", "type": "person", "age": 53, "date": 840146400000.0, "remarks": "Paints funerals clearly."}, {"title": "Taylor E. Rees", "type": "person", "avail": true, "age": 98, "date": 114994800000.0, "remarks": "Supports asides cleanly."}, {"title": "Keith M. Duncan", "type": "person", "state": "h", "avail": true, "age": 36, "date": 1132095600000.0}, {"title": "Sarah I. Dickens", "type": "person", "avail": true, "age": 23, "date": 106873200000.0}, {"title": "Eddie Manning", "type": "person", "state": "s", "avail": true, "age": 75, "date": 670629600000.0}, {"title": "Jan O. Hemmings", "type": "person", "avail": true, "age": 98, "date": 1116280800000.0}]}]}, {"title": "Dept. for Forces and Entrances", "type": "department", "children": [{"title": "Shit slips", "type": "role", "expanded": true}, {"title": "Fry juniors", "type": "role", "expanded": true, "children": [{"title": "Kelly Alsop", "type": "person", "avail": true, "age": 65, "date": 1151100000000.0}, {"title": "Brian P. Anderson", "type": "person", "state": "s", "avail": true, "age": 75}, {"title": "Dominic Watson", "type": "person", "avail": true, "age": 23, "date": 1137452400000.0, "remarks": "Enlightens sorts courageously."}, {"title": "John Greene", "type": "person", "avail": true, "age": 57, "remarks": "Shaves weaknesses punctually."}, {"title": "Samantha Randall", "type": "person", "avail": true, "age": 46}, {"title": "Thomas Jones", "type": "person", "avail": true, "age": 71, "date": 1299279600000.0, "remarks": "Persuades telephones comfortably."}, {"title": "Eddie Langdon", "type": "person", "avail": true, "age": 70, "date": 1350770400000.0}, {"title": "Sarah Alsop", "type": "person", "avail": true, "age": 21}, {"title": "Virginia B. Reid", "type": "person", "avail": true, "age": 30, "remarks": "Expresses toughs optimistically."}, {"title": "Jake R. Rampling", "type": "person", "state": "h", "avail": true, "age": 69, "date": 50886000000.0}, {"title": "Virginia Q. Coleman", "type": "person", "avail": true, "age": 43, "date": 1185055200000.0, "remarks": "Ceases codes smoothly."}, {"title": "Kylie U. May", "type": "person", "state": "s", "avail": true, "age": 38, "remarks": "Dips descriptions magically."}, {"title": "Simon Davies", "type": "person", "avail": true, "age": 23}, {"title": "Boris Reid", "type": "person", "avail": true, "age": 89, "date": 1462140000000.0}, {"title": "Jean I. North", "type": "person", "state": "h", "avail": true, "age": 45}, {"title": "Christopher W. Alsop", "type": "person", "state": "s", "avail": true, "age": 88, "date": 1171321200000.0, "remarks": "Contests pleasures fervently."}, {"title": "Simon Clark", "type": "person", "avail": true, "age": 30, "date": 295830000000.0, "remarks": "Persuades lines optimistically."}, {"title": "Carol U. Langdon", "type": "person", "state": "s", "avail": true, "age": 72}]}, {"title": "Exist turtles", "type": "role", "children": [{"title": "Ava Cameron", "type": "person", "avail": true, "age": 26}, {"title": "Julian Russell", "type": "person", "avail": true, "age": 83}, {"title": "Bella Tucker", "type": "person", "avail": true, "age": 24, "date": 1096063200000.0}]}, {"title": "Grip restaurants", "type": "role", "children": [{"title": "Trevor N. Murray", "type": "person", "state": "h", "avail": true, "age": 26, "date": 1406239200000.0}, {"title": "Nathan Brown", "type": "person", "age": 29}, {"title": "Mary A. Gray", "type": "person", "age": 69, "remarks": "Shears presses restfully."}, {"title": "Alison Miller", "type": "person", "state": "s", "avail": true, "age": 84, "date": 521330400000.0}, {"title": "Austin C. Fisher", "type": "person", "state": "h", "age": 83, "date": 473814000000.0}, {"title": "Luke R. Johnston", "type": "person", "avail": true, "age": 91, "date": 1648422000000.0}, {"title": "Nicholas S. Dickens", "type": "person", "avail": true, "age": 87, "date": 1237503600000.0}, {"title": "Jesse Harris", "type": "person", "avail": true, "age": 52, "date": 469321200000.0}, {"title": "Dan Piper", "type": "person", "avail": true, "age": 88, "date": 1317852000000.0, "remarks": "Buys rises openly."}, {"title": "Victoria K. Hart", "type": "person", "age": 52, "date": 1093384800000.0, "remarks": "Retches crickets longingly."}, {"title": "Max Hodges", "type": "person", "state": "s", "avail": true, "age": 73, "date": 145494000000.0, "remarks": "Inputs deposits valiantly."}, {"title": "Zoe Clark", "type": "person", "avail": true, "age": 28, "date": 1288911600000.0}, {"title": "Andrea J. Piper", "type": "person", "avail": true, "age": 53}, {"title": "Brian Dickens", "type": "person", "state": "h", "avail": true, "age": 85, "date": 165625200000.0}, {"title": "Lucas G. Avery", "type": "person", "state": "s", "avail": true, "age": 36, "date": 1442527200000.0, "remarks": "Wets hells positively."}, {"title": "Bella J. Dowd", "type": "person", "avail": true, "age": 87}]}, {"title": "Overtake temporaries", "type": "role"}, {"title": "Imperil cables", "type": "role", "expanded": true, "children": [{"title": "Matt Lawrence", "type": "person", "state": "h", "avail": true, "age": 96, "date": 235782000000.0}, {"title": "Daryl B. Randall", "type": "person", "avail": true, "age": 77, "date": 1580166000000.0}, {"title": "Jake V. Burgess", "type": "person", "state": "h", "avail": true, "age": 73}, {"title": "Dylan Howard", "type": "person", "state": "h", "avail": true, "age": 59}]}, {"title": "Gash refuses", "type": "role"}, {"title": "Sleep couples", "type": "role", "expanded": true, "children": [{"title": "Piers Q. Rutherford", "type": "person", "avail": true, "age": 49, "date": 197334000000.0}, {"title": "Lucas U. Kelly", "type": "person", "avail": true, "age": 56, "date": 657846000000.0}, {"title": "Chris T. Black", "type": "person", "avail": true, "age": 81, "date": 1177884000000.0}, {"title": "Phoenix Q. Burgess", "type": "person", "state": "h", "avail": true, "age": 61, "date": 629766000000.0}, {"title": "Isaac A. Thomson", "type": "person", "avail": true, "age": 84, "date": 430437600000.0, "remarks": "Retains boats securely."}, {"title": "Anna North", "type": "person", "state": "s", "age": 25, "date": 1482620400000.0, "remarks": "Consents fishes intensely."}, {"title": "Jean E. Turner", "type": "person", "avail": true, "age": 79, "date": 1293836400000.0, "remarks": "Divides excuses thoroughly."}, {"title": "Jesse Q. Gill", "type": "person", "avail": true, "age": 40, "remarks": "Motivates focuses deeply."}, {"title": "Eric E. Hardacre", "type": "person", "avail": true, "age": 95}]}]}, {"title": "Dept. for Walks and Pasts", "type": "department", "children": [{"title": "Crib choices", "type": "role", "children": [{"title": "Alexander G. Lewis", "type": "person", "avail": true, "age": 92, "date": 1192226400000.0, "remarks": "Realizes breaks cleanly."}, {"title": "Theresa Bailey", "type": "person", "state": "s", "age": 50, "remarks": "Scats shelters spectacularly."}, {"title": "Wanda Sutherland", "type": "person", "avail": true, "age": 48, "date": 677109600000.0, "remarks": "Sails points candidly."}, {"title": "Charles H. Oliver", "type": "person", "age": 47}, {"title": "Diana C. Ferguson", "type": "person", "avail": true, "age": 67, "remarks": "Pouts rewards agreeably."}, {"title": "Dylan Berry", "type": "person", "avail": true, "age": 97}]}, {"title": "Express functions", "type": "role", "children": [{"title": "Eddie Simpson", "type": "person", "avail": true, "age": 83, "date": 767570400000.0}]}, {"title": "Correct hands", "type": "role", "children": [{"title": "Diane Fisher", "type": "person", "avail": true, "age": 30, "date": 25484400000.0}, {"title": "Heather O. Blake", "type": "person", "avail": true, "age": 51}, {"title": "Adrian M. Miller", "type": "person", "avail": true, "age": 61}, {"title": "James Sutherland", "type": "person", "state": "s", "avail": true, "age": 36, "date": 1162072800000.0}, {"title": "Audrey May", "type": "person", "state": "s", "avail": true, "age": 58, "date": 1072825200000.0}, {"title": "Melanie Edmunds", "type": "person", "avail": true, "age": 69, "date": 699922800000.0}, {"title": "Jasmine Oliver", "type": "person", "state": "h", "avail": true, "age": 23, "date": 1206399600000.0}, {"title": "Kylie Morgan", "type": "person", "state": "s", "avail": true, "age": 97}, {"title": "Dominic L. Taylor", "type": "person", "avail": true, "age": 67, "remarks": "Induces passages gracefully."}, {"title": "Kylie J. White", "type": "person", "state": "s", "avail": true, "age": 65, "date": 330904800000.0}, {"title": "Sue M. Roberts", "type": "person", "avail": true, "age": 77, "date": 1485126000000.0}, {"title": "Jamie Slater", "type": "person", "avail": true, "age": 91}, {"title": "Cameron Turner", "type": "person", "state": "h", "avail": true, "age": 41, "date": 1491256800000.0}, {"title": "Penelope H. Grant", "type": "person", "avail": true, "age": 83, "date": 183164400000.0}, {"title": "Christopher Z. North", "type": "person", "avail": true, "age": 88, "date": 667090800000.0}, {"title": "Emily Grant", "type": "person", "state": "s", "age": 31, "date": 1294786800000.0}, {"title": "Justin Mathis", "type": "person", "avail": true, "age": 47, "date": 1609455600000.0, "remarks": "Crowds exercises entirely."}]}, {"title": "Inherit draws", "type": "role", "expanded": true, "children": [{"title": "Jake R. Walsh", "type": "person", "state": "h", "avail": true, "age": 70, "date": 178844400000.0, "remarks": "Bears functions wholly."}, {"title": "Phil Simpson", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1098568800000.0, "remarks": "Phones efficiencies bravely."}, {"title": "William Q. Ferguson", "type": "person", "avail": true, "age": 52, "date": 898380000000.0}]}, {"title": "Move skies", "type": "role", "children": [{"title": "Rose May", "type": "person", "state": "h", "avail": true, "age": 89, "date": 1491170400000.0}, {"title": "Harry James", "type": "person", "avail": true, "age": 81, "date": 970783200000.0}, {"title": "Edward Z. Taylor", "type": "person", "state": "h", "avail": true, "age": 26}, {"title": "Alex Q. Henderson", "type": "person", "avail": true, "age": 62}, {"title": "Jesse Ross", "type": "person", "state": "s", "avail": true, "age": 38}, {"title": "Kimberly Berry", "type": "person", "avail": true, "age": 93}, {"title": "Harry T. McLean", "type": "person", "avail": true, "age": 59}, {"title": "Blake P. Walsh", "type": "person", "avail": true, "age": 89}, {"title": "Kyle D. Gill", "type": "person", "avail": true, "age": 61, "date": 1132527600000.0}, {"title": "Gordon J. Black", "type": "person", "avail": true, "age": 87}, {"title": "Stephanie Springer", "type": "person", "state": "s", "avail": true, "age": 74, "date": 268959600000.0}, {"title": "Madeleine Y. Brown", "type": "person", "avail": true, "age": 46}, {"title": "Samantha O. Powell", "type": "person", "state": "s", "age": 71}, {"title": "Molly S. Anderson", "type": "person", "state": "h", "avail": true, "age": 60, "date": 315615600000.0}, {"title": "Zoe P. Hemmings", "type": "person", "avail": true, "age": 87, "date": 1437343200000.0, "remarks": "Addresses shirts absolutely."}, {"title": "Victoria J. Cornish", "type": "person", "avail": true, "age": 40}]}, {"title": "Beat meats", "type": "role", "expanded": true, "children": [{"title": "Hannah Y. Gray", "type": "person", "age": 57, "date": 1449529200000.0}, {"title": "Andy Clark", "type": "person", "state": "s", "age": 46, "date": 512434800000.0}, {"title": "Gabrielle B. Hart", "type": "person", "state": "h", "avail": true, "age": 82, "date": 907624800000.0}, {"title": "Sydney Black", "type": "person", "avail": true, "age": 70}, {"title": "Andy Tucker", "type": "person", "state": "h", "avail": true, "age": 53}, {"title": "Tim A. Alsop", "type": "person", "state": "s", "avail": true, "age": 68, "date": 499129200000.0}, {"title": "Carl Nash", "type": "person", "state": "h", "age": 39, "date": 851900400000.0}, {"title": "Megan Y. Burgess", "type": "person", "avail": true, "age": 34}, {"title": "Justin Duncan", "type": "person", "avail": true, "age": 62, "date": 1328914800000.0}, {"title": "Michelle Mackay", "type": "person", "state": "h", "avail": true, "age": 73, "date": 512348400000.0, "remarks": "Spills situations handily."}, {"title": "Heather C. Peake", "type": "person", "state": "s", "avail": true, "age": 91, "date": 801871200000.0}, {"title": "Anthony W. King", "type": "person", "avail": true, "age": 49, "date": 1519340400000.0}, {"title": "Audrey U. Hemmings", "type": "person", "avail": true, "age": 97, "date": 1343599200000.0, "remarks": "Recognizes hyenas rightfully."}, {"title": "Victoria Miller", "type": "person", "state": "s", "avail": true, "age": 73}, {"title": "Frances Smith", "type": "person", "state": "h", "avail": true, "age": 53, "date": 257814000000.0}, {"title": "Sue O. Fisher", "type": "person", "avail": true, "age": 84}, {"title": "Lou N. Paterson", "type": "person", "avail": true, "age": 26, "date": 832111200000.0, "remarks": "Roars frames universally."}]}, {"title": "Live spaces", "type": "role", "children": [{"title": "Sean Hudson", "type": "person", "avail": true, "age": 63, "date": 289177200000.0, "remarks": "Increases coaches infinitely."}, {"title": "Audrey F. Jones", "type": "person", "state": "h", "avail": true, "age": 26, "date": 1278626400000.0, "remarks": "Leans nothings extensively."}, {"title": "Charlie Pullman", "type": "person", "state": "s", "avail": true, "age": 49}, {"title": "Virginia Hardacre", "type": "person", "avail": true, "age": 61, "date": 730854000000.0}, {"title": "Michael Abraham", "type": "person", "avail": true, "age": 88, "date": 980204400000.0}, {"title": "Daryl P. Manning", "type": "person", "avail": true, "age": 45}, {"title": "Thomas E. Brown", "type": "person", "avail": true, "age": 37, "date": 503449200000.0}, {"title": "Natalie Dyer", "type": "person", "state": "s", "avail": true, "age": 59, "date": 1304028000000.0}, {"title": "Matt H. Campbell", "type": "person", "state": "s", "avail": true, "age": 52, "date": 1569276000000.0}, {"title": "Joanne Arnold", "type": "person", "avail": true, "age": 48}, {"title": "Jennifer Q. Howard", "type": "person", "state": "s", "avail": true, "age": 54}]}, {"title": "Blush agreements", "type": "role", "children": [{"title": "Chloe S. Henderson", "type": "person", "age": 77}, {"title": "Jessica Gill", "type": "person", "avail": true, "age": 48, "date": 192927600000.0, "remarks": "Inputs prints kindly."}, {"title": "Gabrielle C. Black", "type": "person", "avail": true, "age": 35, "remarks": "Scabs suspects knowledgeably."}, {"title": "Keith Grant", "type": "person", "avail": true, "age": 55, "date": 1656972000000.0}, {"title": "Heather Vaughan", "type": "person", "avail": true, "age": 84, "date": 4662000000.0}, {"title": "Cameron Randall", "type": "person", "state": "h", "avail": true, "age": 81, "date": 637887600000.0, "remarks": "Flings bunches helpfully."}, {"title": "Sydney N. Quinn", "type": "person", "avail": true, "age": 55, "date": 1582153200000.0}, {"title": "Sophie V. Payne", "type": "person", "avail": true, "age": 23, "date": 72140400000.0}, {"title": "Max McGrath", "type": "person", "avail": true, "age": 54}, {"title": "Diane B. Fraser", "type": "person", "avail": true, "age": 61, "remarks": "Winds claims reasonably."}, {"title": "Karen Buckland", "type": "person", "avail": true, "age": 27}, {"title": "Warren R. Hemmings", "type": "person", "state": "s", "avail": true, "age": 27, "date": 946854000000.0}, {"title": "Justin May", "type": "person", "state": "h", "avail": true, "age": 71, "date": 1528840800000.0}, {"title": "Toby Rutherford", "type": "person", "avail": true, "age": 34, "date": 1418943600000.0}, {"title": "Austin I. McLean", "type": "person", "avail": true, "age": 95, "date": 1553727600000.0, "remarks": "Donates holes significantly."}]}, {"title": "Plot wests", "type": "role", "expanded": true, "children": [{"title": "Bobbie Fisher", "type": "person", "avail": true, "age": 53, "date": 1111359600000.0}, {"title": "Liam Roberts", "type": "person", "avail": true, "age": 22, "date": 1228086000000.0}, {"title": "Taylor Johnston", "type": "person", "avail": true, "age": 88}, {"title": "Irene Russell", "type": "person", "state": "s", "avail": true, "age": 87, "date": 1388098800000.0}, {"title": "Stephanie Arnold", "type": "person", "state": "h", "avail": true, "age": 62, "date": 1135724400000.0}, {"title": "Karen S. Coleman", "type": "person", "avail": true, "age": 36}, {"title": "Brandon MacLeod", "type": "person", "state": "h", "avail": true, "age": 72, "remarks": "States currents whole-heartedly."}, {"title": "Kelly Peake", "type": "person", "avail": true, "age": 59, "date": 1235775600000.0}, {"title": "Rebecca Terry", "type": "person", "avail": true, "age": 23, "date": 117068400000.0}, {"title": "Adrian Allan", "type": "person", "avail": true, "age": 37, "date": 483746400000.0, "remarks": "Hisses hearts zestfully."}, {"title": "Chris Fisher", "type": "person", "state": "s", "avail": true, "age": 74, "date": 169686000000.0}, {"title": "Paul Carr", "type": "person", "state": "h", "avail": true, "age": 66, "date": 451000800000.0}, {"title": "Bobbie James", "type": "person", "avail": true, "age": 22, "date": 155862000000.0}, {"title": "Dorothy Langdon", "type": "person", "avail": true, "age": 40, "date": 426204000000.0}]}, {"title": "Hurt punches", "type": "role", "expanded": true, "children": [{"title": "Faith Campbell", "type": "person", "avail": true, "age": 87, "date": 1650319200000.0, "remarks": "Bets sisters poetically."}, {"title": "Michelle Z. Ellison", "type": "person", "avail": true, "age": 81, "date": 1044399600000.0, "remarks": "Saddles sources righteously."}, {"title": "Dylan G. Dyer", "type": "person", "avail": true, "age": 85}, {"title": "Jessica L. Gibson", "type": "person", "avail": true, "age": 47}]}]}, {"title": "Dept. for Recovers and Recordings", "type": "department", "children": [{"title": "Construe influences", "type": "role", "children": [{"title": "Jamie Y. Gill", "type": "person", "avail": true, "age": 86}, {"title": "Diana Ball", "type": "person", "avail": true, "age": 37, "date": 674172000000.0}, {"title": "Abigail Roberts", "type": "person", "avail": true, "age": 38}, {"title": "Chris K. Fraser", "type": "person", "avail": true, "age": 54, "remarks": "Lifts childhoods restfully."}]}, {"title": "Tread assignments", "type": "role", "expanded": true, "children": [{"title": "Warren Clark", "type": "person", "avail": true, "age": 47, "date": 785631600000.0, "remarks": "Stresses safeties optimistically."}, {"title": "Charlie W. Bailey", "type": "person", "avail": true, "age": 36}, {"title": "Anthony P. Marshall", "type": "person", "avail": true, "age": 53, "date": 1588111200000.0}]}, {"title": "Nip tracks", "type": "role", "expanded": true, "children": [{"title": "Tracey S. Nolan", "type": "person", "state": "h", "avail": true, "age": 96, "date": 527637600000.0}, {"title": "Brandon W. Parr", "type": "person", "state": "h", "avail": true, "age": 38, "date": 1036623600000.0}, {"title": "Diane F. Burgess", "type": "person", "avail": true, "age": 26, "date": 1457132400000.0}, {"title": "Brandon Chapman", "type": "person", "avail": true, "age": 60, "date": 85532400000.0}, {"title": "Deirdre Quinn", "type": "person", "avail": true, "age": 45, "date": 97714800000.0}, {"title": "Bella Glover", "type": "person", "avail": true, "age": 95, "remarks": "Passes sticks considerately."}, {"title": "Harry Rees", "type": "person", "avail": true, "age": 61, "remarks": "Boxes passes nicely."}, {"title": "Hannah K. Wilson", "type": "person", "avail": true, "age": 40, "date": 550533600000.0, "remarks": "Knits openings fully."}, {"title": "Keith X. Newman", "type": "person", "state": "h", "avail": true, "age": 88, "date": 367797600000.0}, {"title": "Rachel McDonald", "type": "person", "avail": true, "age": 32, "date": 1261522800000.0}, {"title": "Toby Henderson", "type": "person", "avail": true, "age": 70, "date": 298249200000.0}, {"title": "Frank Anderson", "type": "person", "avail": true, "age": 70, "remarks": "Dares debates courageously."}, {"title": "Corey E. Wallace", "type": "person", "state": "h", "avail": true, "age": 98, "date": 64882800000.0}, {"title": "Dylan McDonald", "type": "person", "avail": true, "age": 68, "date": 1209852000000.0}, {"title": "Pat E. Glover", "type": "person", "avail": true, "age": 45, "date": 686181600000.0}]}, {"title": "Mean boxes", "type": "role", "expanded": true, "children": [{"title": "Angela Oliver", "type": "person", "avail": true, "age": 26, "date": 48812400000.0}, {"title": "Stephen A. Hart", "type": "person", "avail": true, "age": 35, "remarks": "Integrates shoes bravely."}, {"title": "Evan G. Ross", "type": "person", "avail": true, "age": 49, "date": 679701600000.0, "remarks": "Saddles affects sympathetically."}, {"title": "Faith H. Coleman", "type": "person", "state": "h", "avail": true, "age": 29, "remarks": "Provides tensions richly."}, {"title": "Gabrielle Bailey", "type": "person", "avail": true, "age": 36, "date": 1058824800000.0}, {"title": "Stephanie I. Tucker", "type": "person", "avail": true, "age": 22}]}, {"title": "Dispose carpets", "type": "role", "children": [{"title": "Donna C. Tucker", "type": "person", "avail": true, "age": 84, "remarks": "Praises perceptions calmly."}, {"title": "Jamie R. Ince", "type": "person", "state": "h", "avail": true, "age": 21, "date": 1319061600000.0}, {"title": "Sonia Y. Butler", "type": "person", "state": "s", "avail": true, "age": 55, "date": 1367359200000.0}, {"title": "Simon Parr", "type": "person", "state": "h", "avail": true, "age": 77, "date": 1497823200000.0}, {"title": "Kylie Short", "type": "person", "avail": true, "age": 28, "date": 52354800000.0}]}, {"title": "Endanger weeks", "type": "role", "children": [{"title": "Sophie Q. Wright", "type": "person", "avail": true, "age": 25, "date": 469321200000.0}, {"title": "Vanessa Martin", "type": "person", "avail": true, "age": 78, "date": 771458400000.0}, {"title": "Lillian Roberts", "type": "person", "age": 39, "remarks": "Sniffs processes jubilantly."}, {"title": "Corey B. Knox", "type": "person", "avail": true, "age": 80, "date": 1583190000000.0, "remarks": "Vanishes witnesses promptly."}, {"title": "Kyle Turner", "type": "person", "avail": true, "age": 28, "date": 165193200000.0}]}, {"title": "Waste cups", "type": "role", "expanded": true, "children": [{"title": "David L. Oliver", "type": "person", "avail": true, "age": 39}, {"title": "Owen Z. Fraser", "type": "person", "avail": true, "age": 52, "date": 273193200000.0}, {"title": "Lou Nash", "type": "person", "avail": true, "age": 51, "date": 966895200000.0}, {"title": "Carolyn S. Smith", "type": "person", "avail": true, "age": 82, "date": 53823600000.0}, {"title": "Jennifer Sutherland", "type": "person", "avail": true, "age": 65, "date": 401234400000.0}]}, {"title": "Cry hunts", "type": "role", "children": [{"title": "Steven Davies", "type": "person", "avail": true, "age": 42, "date": 479430000000.0}, {"title": "Jake F. North", "type": "person", "age": 44}, {"title": "Tim Sanderson", "type": "person", "avail": true, "age": 94}, {"title": "Vanessa P. Wallace", "type": "person", "age": 80}, {"title": "Keith Paterson", "type": "person", "state": "h", "avail": true, "age": 56, "date": 1244325600000.0}, {"title": "Christopher Jackson", "type": "person", "avail": true, "age": 33, "remarks": "Appears sheeps fast."}, {"title": "Alison Bond", "type": "person", "state": "h", "avail": true, "age": 94, "date": 1285797600000.0, "remarks": "Offers incomes brilliantly."}, {"title": "Christopher B. Forsyth", "type": "person", "avail": true, "age": 79}, {"title": "Jean Grant", "type": "person", "avail": true, "age": 77, "date": 1360105200000.0, "remarks": "Hinders clubs bravely."}, {"title": "Lillian Forsyth", "type": "person", "avail": true, "age": 69, "remarks": "Varies broads busily."}, {"title": "Chloe Grant", "type": "person", "avail": true, "age": 71, "date": 513986400000.0}, {"title": "Anthony L. Abraham", "type": "person", "avail": true, "age": 86, "date": 800834400000.0}, {"title": "Jasmine Reid", "type": "person", "avail": true, "age": 57, "remarks": "Fits belts quickly."}]}, {"title": "Cross breasts", "type": "role", "children": [{"title": "Kylie Miller", "type": "person", "avail": true, "age": 39, "date": 1620165600000.0}, {"title": "Pat Greene", "type": "person", "age": 94, "date": 1649714400000.0, "remarks": "Stops essaies beautifully."}, {"title": "Daryl Miller", "type": "person", "avail": true, "age": 77}, {"title": "Ryan White", "type": "person", "avail": true, "age": 91, "date": 306284400000.0}, {"title": "Sue Morrison", "type": "person", "avail": true, "age": 59, "date": 982364400000.0}]}, {"title": "Vary strangers", "type": "role", "children": [{"title": "Wanda Z. Vance", "type": "person", "avail": true, "age": 81, "date": 1415574000000.0, "remarks": "Swallows feels eagerly."}, {"title": "Jason Hudson", "type": "person", "avail": true, "age": 97}, {"title": "Sophie Blake", "type": "person", "avail": true, "age": 48, "date": 611013600000.0}, {"title": "Thomas Ross", "type": "person", "avail": true, "age": 82, "date": 944262000000.0}, {"title": "Adrian MacLeod", "type": "person", "avail": true, "age": 42, "remarks": "Blushes disks wholly."}, {"title": "Dylan W. Mathis", "type": "person", "state": "s", "avail": true, "age": 33}, {"title": "Zoe Dickens", "type": "person", "state": "s", "avail": true, "age": 55, "remarks": "Brings targets explicitly."}, {"title": "Andrea Morrison", "type": "person", "avail": true, "age": 63, "date": 634604400000.0}, {"title": "Carolyn Hill", "type": "person", "avail": true, "age": 58, "date": 462837600000.0, "remarks": "Folds equivalents securely."}, {"title": "Oliver E. Lyman", "type": "person", "state": "h", "avail": true, "age": 82, "date": 449877600000.0}, {"title": "Connor S. Paterson", "type": "person", "avail": true, "age": 38, "date": 1417388400000.0}, {"title": "Faith R. Dyer", "type": "person", "avail": true, "age": 77}, {"title": "Pat P. Brown", "type": "person", "avail": true, "age": 83, "remarks": "Sparkles matters markedly."}, {"title": "Tracey U. MacDonald", "type": "person", "state": "s", "avail": true, "age": 64, "date": 477097200000.0}, {"title": "Victoria C. Hart", "type": "person", "age": 58, "remarks": "Attacks prides early."}, {"title": "Julian M. Ince", "type": "person", "avail": true, "age": 84}, {"title": "Jennifer Brown", "type": "person", "state": "s", "avail": true, "age": 57}]}]}, {"title": "Dept. for Fees and Skunks", "type": "department", "children": [{"title": "Subtract connections", "type": "role", "children": [{"title": "Sebastian Simpson", "type": "person", "avail": true, "age": 84, "date": 135126000000.0}, {"title": "Harry K. Butler", "type": "person", "state": "s", "avail": true, "age": 47, "date": 559954800000.0}, {"title": "Julia N. Vaughan", "type": "person", "avail": true, "age": 51, "date": 1517007600000.0}, {"title": "Edward Rampling", "type": "person", "state": "s", "avail": true, "age": 63}, {"title": "Stewart Vance", "type": "person", "state": "h", "age": 36}, {"title": "Jonathan Paige", "type": "person", "avail": true, "age": 50, "date": 268354800000.0, "remarks": "Endorses factors efficiently."}, {"title": "Maria X. Churchill", "type": "person", "state": "h", "age": 97, "date": 1043276400000.0, "remarks": "Strains objects healthily."}, {"title": "Kimberly May", "type": "person", "avail": true, "age": 76, "remarks": "Salutes manners actively."}, {"title": "Sally Parsons", "type": "person", "avail": true, "age": 90}, {"title": "Amanda Glover", "type": "person", "state": "s", "avail": true, "age": 89, "date": 347497200000.0}, {"title": "Oliver Ellison", "type": "person", "avail": true, "age": 49, "date": 1419548400000.0}, {"title": "Wendy L. Ball", "type": "person", "state": "h", "avail": true, "age": 75, "remarks": "Refers debates properly."}, {"title": "Frank H. Terry", "type": "person", "age": 35}, {"title": "Irene B. May", "type": "person", "state": "h", "avail": true, "age": 60, "date": 1458687600000.0}]}, {"title": "Cower particulars", "type": "role", "expanded": true, "children": [{"title": "Ella W. Chapman", "type": "person", "avail": true, "age": 74, "date": 86742000000.0, "remarks": "Adjusts presences consistently."}, {"title": "Charles J. Bailey", "type": "person", "avail": true, "age": 53}, {"title": "Adam Graham", "type": "person", "state": "s", "avail": true, "age": 57, "date": 1119477600000.0}, {"title": "Pat Hodges", "type": "person", "state": "s", "avail": true, "age": 73, "date": 1260226800000.0}, {"title": "Corey Springer", "type": "person", "avail": true, "age": 53}, {"title": "Piers D. Powell", "type": "person", "avail": true, "age": 86, "date": 797983200000.0, "remarks": "Imprints experts rightly."}, {"title": "Rachel P. Graham", "type": "person", "avail": true, "age": 34, "date": 280537200000.0}, {"title": "Jayden L. McGrath", "type": "person", "avail": true, "age": 48, "date": 292374000000.0}, {"title": "Casey P. Greene", "type": "person", "avail": true, "age": 28}, {"title": "Chris M. Parsons", "type": "person", "age": 50}, {"title": "Chris Churchill", "type": "person", "state": "s", "avail": true, "age": 28}, {"title": "Piers Ogden", "type": "person", "avail": true, "age": 27}]}, {"title": "Hew computers", "type": "role", "children": [{"title": "Michael V. Turner", "type": "person", "state": "s", "avail": true, "age": 86, "date": 1386802800000.0}, {"title": "Alexander I. Howard", "type": "person", "state": "h", "avail": true, "age": 82, "date": 742773600000.0, "remarks": "Feels ferrets studiously."}, {"title": "Madeleine F. Nash", "type": "person", "state": "s", "avail": true, "age": 35, "remarks": "Ignites considerations cleanly."}, {"title": "Amanda Tucker", "type": "person", "avail": true, "age": 88, "date": 1316988000000.0}, {"title": "Jane King", "type": "person", "state": "h", "avail": true, "age": 51, "date": 1304028000000.0}, {"title": "Andy Miller", "type": "person", "state": "s", "avail": true, "age": 28, "date": 1122501600000.0}, {"title": "Lily Simpson", "type": "person", "state": "h", "avail": true, "age": 88, "date": 109119600000.0, "remarks": "Proceeds industries wonderfully."}, {"title": "Tracey Y. Hodges", "type": "person", "avail": true, "age": 23, "date": 950396400000.0}, {"title": "Simon B. Wilson", "type": "person", "avail": true, "age": 89, "date": 500425200000.0}, {"title": "Daryl Pullman", "type": "person", "state": "s", "avail": true, "age": 61, "date": 580428000000.0, "remarks": "Fights smokes splendidly."}]}, {"title": "Lay vacations", "type": "role", "children": [{"title": "Amelia Burgess", "type": "person", "state": "s", "avail": true, "age": 25, "date": 493509600000.0, "remarks": "Wrings packages luckily."}, {"title": "Yvonne G. Mackenzie", "type": "person", "avail": true, "age": 31, "date": 1575846000000.0}, {"title": "Benjamin H. Fraser", "type": "person", "avail": true, "age": 47, "date": 936914400000.0, "remarks": "Triumphs difficulties really."}]}, {"title": "Kiss marriages", "type": "role", "children": [{"title": "Sonia Z. Johnston", "type": "person", "state": "h", "age": 62}, {"title": "Lou Q. Carr", "type": "person", "avail": true, "age": 95}, {"title": "Anne W. Wright", "type": "person", "avail": true, "age": 73, "date": 985816800000.0, "remarks": "Neglects employees assuredly."}, {"title": "Anne Lambert", "type": "person", "avail": true, "age": 48}, {"title": "Nathan Q. Murray", "type": "person", "state": "s", "avail": true, "age": 24, "date": 349225200000.0, "remarks": "Speeds youngs unquestionably."}, {"title": "Peter Coleman", "type": "person", "avail": true, "age": 32}, {"title": "Jayden F. Morgan", "type": "person", "age": 45, "date": 1007679600000.0, "remarks": "Selects dots abundantly."}, {"title": "Audrey B. Gibson", "type": "person", "avail": true, "age": 44}, {"title": "Justin Q. Wilkins", "type": "person", "avail": true, "age": 27, "date": 1263423600000.0, "remarks": "Constrains pugs intensely."}, {"title": "Oliver Nash", "type": "person", "avail": true, "age": 40}, {"title": "Joanne J. Reid", "type": "person", "avail": true, "age": 51, "remarks": "Spells alternatives proudly."}]}, {"title": "Owe televisions", "type": "role", "expanded": true, "children": [{"title": "Connor K. Allan", "type": "person", "avail": true, "age": 36, "date": 1482534000000.0}, {"title": "Frances R. Fraser", "type": "person", "avail": true, "age": 51, "date": 1218924000000.0}, {"title": "Julia Rutherford", "type": "person", "avail": true, "age": 60, "date": 338767200000.0, "remarks": "Encourages conversations valiantly."}, {"title": "Trevor Black", "type": "person", "avail": true, "age": 62, "date": 118450800000.0}, {"title": "Kimberly L. Nolan", "type": "person", "state": "h", "avail": true, "age": 88, "date": 1198623600000.0, "remarks": "Rids decisions intelligently."}, {"title": "Edward Y. Campbell", "type": "person", "avail": true, "age": 88, "date": 389829600000.0}, {"title": "Kyle X. Springer", "type": "person", "state": "s", "avail": true, "age": 71, "date": 1061244000000.0}, {"title": "Lou Dyer", "type": "person", "avail": true, "age": 93, "date": 203036400000.0}, {"title": "Frank H. Gill", "type": "person", "avail": true, "age": 92, "date": 1395442800000.0}, {"title": "Jasmine I. Mitchell", "type": "person", "avail": true, "age": 55, "date": 1638486000000.0}, {"title": "Casey O. Langdon", "type": "person", "state": "s", "avail": true, "age": 45, "remarks": "Bows priors agreeably."}, {"title": "Simon Coleman", "type": "person", "avail": true, "age": 97, "date": 302655600000.0, "remarks": "Tramples taps beautifully."}, {"title": "Brian K. Glover", "type": "person", "avail": true, "age": 44, "remarks": "Redoes secretaries upwardly."}, {"title": "Fiona Y. Jackson", "type": "person", "avail": true, "age": 49, "date": 1627768800000.0, "remarks": "Fights bigs heartily."}]}, {"title": "Last campaigns", "type": "role", "children": [{"title": "Daryl B. Dowd", "type": "person", "avail": true, "age": 60, "date": 944607600000.0}, {"title": "Kelly B. Lyman", "type": "person", "avail": true, "age": 65, "remarks": "Tends tricks majestically."}, {"title": "Keith H. Stewart", "type": "person", "avail": true, "age": 71, "date": 547423200000.0}, {"title": "Natalie W. Short", "type": "person", "avail": true, "age": 66, "date": 875138400000.0, "remarks": "Burns sillies infinitely."}, {"title": "Corey Wilkins", "type": "person", "avail": true, "age": 88}, {"title": "Oliver Allan", "type": "person", "state": "h", "age": 33}]}, {"title": "Undo positives", "type": "role", "children": [{"title": "Toby Quinn", "type": "person", "avail": true, "age": 36, "date": 1147989600000.0}]}, {"title": "Sight articles", "type": "role", "children": [{"title": "Adam E. Scott", "type": "person", "avail": true, "age": 31, "date": 1093989600000.0, "remarks": "Curbs alpacas agreeably."}, {"title": "Jesse Y. Blake", "type": "person", "age": 22}, {"title": "Amelia Bond", "type": "person", "avail": true, "age": 83, "date": 863992800000.0, "remarks": "Consoles disciplines dreamily."}, {"title": "Diane Dyer", "type": "person", "avail": true, "age": 24}, {"title": "William MacDonald", "type": "person", "avail": true, "age": 33, "date": 1458342000000.0}, {"title": "Alan J. Wright", "type": "person", "state": "h", "avail": true, "age": 78}, {"title": "Max Z. Duncan", "type": "person", "avail": true, "age": 37, "date": 1180821600000.0}, {"title": "Warren Walker", "type": "person", "state": "s", "avail": true, "age": 27, "remarks": "Incises walruses diligently."}, {"title": "Sam Kelly", "type": "person", "age": 84}, {"title": "Alan Scott", "type": "person", "state": "h", "avail": true, "age": 32}, {"title": "Olivia Vance", "type": "person", "age": 61}]}]}, {"title": "Dept. for Uses and Wars", "type": "department", "children": [{"title": "Dedicate issues", "type": "role", "children": [{"title": "Maria Piper", "type": "person", "age": 44, "date": 435711600000.0}, {"title": "Anne Gill", "type": "person", "avail": true, "age": 70}, {"title": "Phil L. Gibson", "type": "person", "avail": true, "age": 50, "date": 1072652400000.0}, {"title": "Corey Wilson", "type": "person", "avail": true, "age": 49, "date": 752281200000.0}, {"title": "Richard Berry", "type": "person", "age": 78, "date": 83890800000.0}]}, {"title": "Transform departures", "type": "role", "children": [{"title": "Casey M. North", "type": "person", "avail": true, "age": 36, "date": 909788400000.0}, {"title": "Lisa R. Gibson", "type": "person", "avail": true, "age": 59, "remarks": "Sacks dramas merrily."}, {"title": "Mary K. Newman", "type": "person", "age": 95, "date": 2070000000.0, "remarks": "Swallows fruits boldly."}, {"title": "Sarah Wilson", "type": "person", "avail": true, "age": 88, "remarks": "Inflates numbers proudly."}, {"title": "Jamie Lewis", "type": "person", "avail": true, "age": 35, "date": 164070000000.0, "remarks": "Becomes bunches restfully."}, {"title": "Adrian Hughes", "type": "person", "avail": true, "age": 74, "date": 1351461600000.0}, {"title": "Jean A. Clarkson", "type": "person", "avail": true, "age": 47, "date": 1125352800000.0}, {"title": "Diana Campbell", "type": "person", "state": "h", "avail": true, "age": 27, "date": 852850800000.0}, {"title": "Claire E. Mackenzie", "type": "person", "avail": true, "age": 60, "date": 1588456800000.0}, {"title": "Donna Black", "type": "person", "avail": true, "age": 97, "date": 198716400000.0}, {"title": "Gabrielle K. North", "type": "person", "avail": true, "age": 95, "date": 1013468400000.0}, {"title": "Katherine K. Henderson", "type": "person", "state": "s", "avail": true, "age": 55}, {"title": "Joan M. Poole", "type": "person", "state": "s", "avail": true, "age": 53, "date": 1251756000000.0}, {"title": "Karen Rees", "type": "person", "avail": true, "age": 41, "date": 1074898800000.0}, {"title": "Taylor Slater", "type": "person", "avail": true, "age": 72}, {"title": "Jack Vaughan", "type": "person", "avail": true, "age": 51, "date": 1050357600000.0, "remarks": "Sates mouths lovingly."}]}, {"title": "Carve caribous", "type": "role", "expanded": true, "children": [{"title": "Lauren Y. Newman", "type": "person", "avail": true, "age": 60, "date": 227487600000.0}, {"title": "Jean E. Stewart", "type": "person", "avail": true, "age": 22}, {"title": "John F. McDonald", "type": "person", "age": 65, "date": 1129932000000.0}, {"title": "Amy R. Burgess", "type": "person", "avail": true, "age": 89}, {"title": "Lou Q. Scott", "type": "person", "avail": true, "age": 25, "date": 1628892000000.0}, {"title": "Diana E. Hamilton", "type": "person", "state": "h", "avail": true, "age": 89, "date": 527205600000.0}, {"title": "Isaac O. Young", "type": "person", "state": "s", "avail": true, "age": 96, "date": 925423200000.0, "remarks": "Steps hares fervently."}]}, {"title": "Contrast flights", "type": "role", "expanded": true, "children": [{"title": "Vanessa V. Paterson", "type": "person", "avail": true, "age": 92}, {"title": "Piers Y. Harris", "type": "person", "avail": true, "age": 72, "date": 1087336800000.0}, {"title": "Rose Q. Tucker", "type": "person", "state": "h", "avail": true, "age": 57, "date": 1389308400000.0}, {"title": "Rebecca North", "type": "person", "avail": true, "age": 81, "date": 1180908000000.0, "remarks": "Buys switches promptly."}, {"title": "Emily Edmunds", "type": "person", "age": 75, "date": 220489200000.0}, {"title": "Christopher R. Peters", "type": "person", "state": "h", "avail": true, "age": 42}, {"title": "Ryan C. Clark", "type": "person", "avail": true, "age": 40, "date": 1623880800000.0}, {"title": "Emma Wilson", "type": "person", "avail": true, "age": 45, "date": 63154800000.0, "remarks": "Says singles very."}, {"title": "Amanda Tucker", "type": "person", "state": "s", "avail": true, "age": 52, "date": 443574000000.0}, {"title": "Connor Mackay", "type": "person", "state": "h", "avail": true, "age": 36, "date": 350607600000.0}, {"title": "Sue T. Cameron", "type": "person", "state": "h", "avail": true, "age": 89, "date": 1090274400000.0}, {"title": "Kelly Vaughan", "type": "person", "state": "s", "avail": true, "age": 82, "date": 10796400000.0}, {"title": "Angela Oliver", "type": "person", "age": 83, "remarks": "Disuses meets amazingly."}, {"title": "Carl Wallace", "type": "person", "state": "h", "age": 56, "remarks": "Sprays monitors exceptionally."}, {"title": "Gabrielle T. Buckland", "type": "person", "avail": true, "age": 27, "date": 870645600000.0}, {"title": "Michelle O. Smith", "type": "person", "state": "h", "avail": true, "age": 56, "date": 250383600000.0}, {"title": "Kimberly I. Powell", "type": "person", "state": "s", "age": 66, "date": 1117749600000.0, "remarks": "Pleads eggs boldly."}, {"title": "Sarah Rampling", "type": "person", "avail": true, "age": 45}, {"title": "Phil Tucker", "type": "person", "avail": true, "age": 53}]}, {"title": "Astonish presentations", "type": "role", "expanded": true, "children": [{"title": "Irene M. Walker", "type": "person", "avail": true, "age": 39}, {"title": "Mary McDonald", "type": "person", "age": 93, "date": 698108400000.0, "remarks": "Introduces taxes optimistically."}, {"title": "Kevin G. Hemmings", "type": "person", "avail": true, "age": 38, "date": 793407600000.0}, {"title": "Neil Z. Welch", "type": "person", "state": "s", "avail": true, "age": 70, "date": 1394924400000.0, "remarks": "Inquires celebrations joyously."}, {"title": "Harry K. Newman", "type": "person", "state": "s", "avail": true, "age": 42, "remarks": "Copes burns sympathetically."}, {"title": "Megan Slater", "type": "person", "avail": true, "age": 43, "date": 356652000000.0}, {"title": "Karen D. Vance", "type": "person", "avail": true, "age": 26, "date": 1414360800000.0}, {"title": "Adam E. Springer", "type": "person", "avail": true, "age": 98, "date": 910306800000.0}, {"title": "Cameron Jones", "type": "person", "avail": true, "age": 71, "date": 129855600000.0}, {"title": "Phoenix B. Ellison", "type": "person", "avail": true, "age": 36}, {"title": "Ryan Morgan", "type": "person", "avail": true, "age": 21, "date": 324597600000.0}, {"title": "Anthony Buckland", "type": "person", "state": "h", "avail": true, "age": 58, "remarks": "Checks finishes tenderly."}, {"title": "Jesse White", "type": "person", "avail": true, "age": 38}, {"title": "Mary Gibson", "type": "person", "state": "h", "avail": true, "age": 55, "date": 64710000000.0, "remarks": "Conquers families gladly."}, {"title": "Eddie Hamilton", "type": "person", "age": 23, "date": 791420400000.0}]}, {"title": "Carve calendars", "type": "role", "children": [{"title": "Charlie McGrath", "type": "person", "state": "s", "avail": true, "age": 92, "date": 912985200000.0}, {"title": "Amelia H. Mitchell", "type": "person", "avail": true, "age": 71, "remarks": "Glances holidaies deftly."}, {"title": "Nicholas Edmunds", "type": "person", "state": "h", "age": 94, "date": 1538172000000.0}, {"title": "Jack F. Paterson", "type": "person", "avail": true, "age": 84}, {"title": "Anthony Henderson", "type": "person", "avail": true, "age": 50, "date": 1159567200000.0}, {"title": "Alexandra H. Rampling", "type": "person", "state": "h", "avail": true, "age": 70, "date": 584920800000.0, "remarks": "Wastes pages tenderly."}]}, {"title": "Utter falls", "type": "role", "expanded": true, "children": [{"title": "Brian Robertson", "type": "person", "avail": true, "age": 68, "remarks": "Beautifies reserves unfailingly."}, {"title": "Ava P. Dyer", "type": "person", "state": "s", "avail": true, "age": 81, "date": 545522400000.0}, {"title": "Brian D. Lewis", "type": "person", "avail": true, "age": 97, "date": 1097013600000.0, "remarks": "Folds criticisms knowingly."}, {"title": "Andrea Edmunds", "type": "person", "avail": true, "age": 98, "date": 997999200000.0}, {"title": "Toby Carr", "type": "person", "age": 59, "date": 728780400000.0}, {"title": "Jean Payne", "type": "person", "state": "h", "avail": true, "age": 87}, {"title": "Jason F. Lawrence", "type": "person", "state": "s", "avail": true, "age": 91, "date": 228351600000.0}, {"title": "Amy E. Harris", "type": "person", "avail": true, "age": 50, "date": 323046000000.0, "remarks": "Lays stands affirmatively."}, {"title": "Phoenix A. Paige", "type": "person", "avail": true, "age": 26, "date": 1506981600000.0, "remarks": "Breaks cakes usefully."}, {"title": "Alexandra Greene", "type": "person", "avail": true, "age": 90}, {"title": "Pippa Randall", "type": "person", "avail": true, "age": 60}, {"title": "Leah Cameron", "type": "person", "avail": true, "age": 32}, {"title": "Elizabeth Coleman", "type": "person", "state": "s", "avail": true, "age": 87, "date": 271119600000.0, "remarks": "Adds missions efficiently."}]}, {"title": "Reflect structures", "type": "role", "children": [{"title": "Steven J. Turner", "type": "person", "avail": true, "age": 85}, {"title": "Chris Miller", "type": "person", "state": "s", "avail": true, "age": 76, "remarks": "Approves horses charmingly."}, {"title": "Pat T. Arnold", "type": "person", "avail": true, "age": 61, "date": 775692000000.0}, {"title": "Cameron R. Roberts", "type": "person", "avail": true, "age": 47, "date": 668646000000.0, "remarks": "Inputs cods merrily."}, {"title": "Ella Stewart", "type": "person", "avail": true, "age": 73, "remarks": "Inhabits truths sympathetically."}]}, {"title": "Innovate poets", "type": "role", "expanded": true, "children": [{"title": "Jamie Quinn", "type": "person", "avail": true, "age": 96}, {"title": "Dominic Mitchell", "type": "person", "state": "s", "avail": true, "age": 64}, {"title": "Olivia A. Bell", "type": "person", "state": "s", "avail": true, "age": 44}, {"title": "Justin J. Coleman", "type": "person", "avail": true, "age": 96, "date": 1234047600000.0, "remarks": "Refuses calendars gently."}, {"title": "Jason Gray", "type": "person", "state": "s", "avail": true, "age": 47, "date": 370303200000.0, "remarks": "Heaves boats victoriously."}, {"title": "Gabrielle D. Henderson", "type": "person", "avail": true, "age": 74}, {"title": "Alan Henderson", "type": "person", "avail": true, "age": 73}, {"title": "Warren D. Slater", "type": "person", "avail": true, "age": 84, "date": 874015200000.0}, {"title": "Diane Tucker", "type": "person", "avail": true, "age": 53, "date": 948668400000.0, "remarks": "Illustrates bridges unquestionably."}, {"title": "Deirdre F. Kerr", "type": "person", "avail": true, "age": 63, "date": 299977200000.0}, {"title": "Jessica Nash", "type": "person", "state": "s", "avail": true, "age": 21, "remarks": "Matches mortgages deeply."}, {"title": "Joe Bailey", "type": "person", "age": 33}, {"title": "Jessica Forsyth", "type": "person", "avail": true, "age": 61, "date": 1239660000000.0, "remarks": "Greets substances reassuringly."}, {"title": "Leah V. Quinn", "type": "person", "avail": true, "age": 46, "date": 342140400000.0}, {"title": "Lillian Taylor", "type": "person", "state": "h", "age": 88}, {"title": "Sarah Sutherland", "type": "person", "avail": true, "age": 33, "remarks": "Advises successes stylishly."}, {"title": "Frances Quinn", "type": "person", "state": "s", "avail": true, "age": 95, "date": 358207200000.0, "remarks": "Idealizes daughters jubilantly."}, {"title": "Joe Q. Peake", "type": "person", "avail": true, "age": 77, "date": 167094000000.0, "remarks": "Shines wolfs properly."}, {"title": "Sam O. Graham", "type": "person", "avail": true, "age": 95, "remarks": "Stretches philosophies abundantly."}]}, {"title": "Arrest codes", "type": "role", "children": [{"title": "Warren B. Dyer", "type": "person", "state": "s", "age": 95, "date": 1481756400000.0, "remarks": "Chooses tables properly."}, {"title": "Joan N. Rutherford", "type": "person", "state": "s", "avail": true, "age": 50, "date": 1351116000000.0, "remarks": "Boards plants interestingly."}, {"title": "Neil E. Ross", "type": "person", "avail": true, "age": 53, "remarks": "Replies freedoms properly."}, {"title": "Paul Wallace", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1068418800000.0, "remarks": "Sees tries clearly."}, {"title": "Jasmine A. Rees", "type": "person", "state": "h", "avail": true, "age": 75, "date": 1288738800000.0, "remarks": "Expresses bones worthily."}, {"title": "Tracey Bailey", "type": "person", "state": "s", "avail": true, "age": 33, "remarks": "Magnifies taxes enjoyably."}, {"title": "Faith Stewart", "type": "person", "avail": true, "age": 49}, {"title": "Keith North", "type": "person", "avail": true, "age": 72, "date": 1036105200000.0}, {"title": "Adrian M. Dyer", "type": "person", "avail": true, "age": 94}, {"title": "Austin Buckland", "type": "person", "avail": true, "age": 54, "remarks": "Praises turtles jauntily."}, {"title": "Sean L. Jackson", "type": "person", "age": 41, "date": 1424214000000.0}, {"title": "Angela Gray", "type": "person", "avail": true, "age": 94, "date": 1444082400000.0}, {"title": "Natalie N. Taylor", "type": "person", "avail": true, "age": 63, "date": 1436652000000.0}, {"title": "Corey K. Smith", "type": "person", "avail": true, "age": 31}, {"title": "Ryan W. Kelly", "type": "person", "avail": true, "age": 21, "date": 496274400000.0}, {"title": "Sydney Langdon", "type": "person", "avail": true, "age": 86, "remarks": "Seizes grandfathers usefully."}]}]}, {"title": "Dept. for Greens and Angers", "type": "department", "children": [{"title": "Practise properties", "type": "role", "expanded": true}, {"title": "Impede beaches", "type": "role", "children": [{"title": "Dorothy Skinner", "type": "person", "avail": true, "age": 57}, {"title": "Cameron Robertson", "type": "person", "state": "s", "avail": true, "age": 34, "remarks": "Touches questions enthusiastically."}, {"title": "Christian F. Berry", "type": "person", "avail": true, "age": 44, "date": 1146348000000.0, "remarks": "Drills crows infinitely."}, {"title": "James Lambert", "type": "person", "avail": true, "age": 64, "date": 1065132000000.0, "remarks": "Does hurries positively."}, {"title": "Richard Wilkins", "type": "person", "state": "h", "avail": true, "age": 89}]}, {"title": "Kill options", "type": "role", "children": [{"title": "Daryl D. Lyman", "type": "person", "age": 73, "date": 683589600000.0}, {"title": "Sean Taylor", "type": "person", "state": "s", "avail": true, "age": 55, "remarks": "Notices offers diligently."}, {"title": "Lisa Jones", "type": "person", "avail": true, "age": 72, "date": 1539208800000.0}, {"title": "Christopher Walker", "type": "person", "avail": true, "age": 32, "date": 806623200000.0}, {"title": "Jack Hughes", "type": "person", "state": "s", "age": 76}, {"title": "Lauren M. Blake", "type": "person", "state": "h", "avail": true, "age": 58, "date": 1297897200000.0, "remarks": "Founds greens genuinely."}, {"title": "Rebecca Mathis", "type": "person", "avail": true, "age": 34, "remarks": "Breaks classrooms cutely."}, {"title": "Alison E. Duncan", "type": "person", "avail": true, "age": 76, "date": 1028671200000.0}, {"title": "Jean J. Miller", "type": "person", "state": "h", "avail": true, "age": 84, "date": 327016800000.0}, {"title": "Gavin V. Simpson", "type": "person", "avail": true, "age": 81, "date": 471135600000.0}, {"title": "Neil Mathis", "type": "person", "avail": true, "age": 91, "date": 543538800000.0, "remarks": "Belongs familiars substantially."}, {"title": "Gordon Edmunds", "type": "person", "avail": true, "age": 31}, {"title": "Emily J. Vaughan", "type": "person", "avail": true, "age": 91}, {"title": "Steven Young", "type": "person", "state": "s", "avail": true, "age": 93, "date": 1589839200000.0}, {"title": "Evan A. Turner", "type": "person", "age": 31, "remarks": "Prefers tooths unfailingly."}, {"title": "Kelly K. Robertson", "type": "person", "state": "s", "avail": true, "age": 38, "date": 1032386400000.0}, {"title": "Nicola H. Buckland", "type": "person", "avail": true, "age": 61, "date": 604191600000.0, "remarks": "Places equipments lightly."}]}, {"title": "Integrate rents", "type": "role", "expanded": true, "children": [{"title": "Phoenix X. Avery", "type": "person", "avail": true, "age": 92}, {"title": "Sophie A. Parr", "type": "person", "state": "s", "avail": true, "age": 79, "date": 782780400000.0}, {"title": "Kevin N. Marshall", "type": "person", "avail": true, "age": 67, "date": 777765600000.0, "remarks": "Supposes parts restfully."}, {"title": "Matt R. Payne", "type": "person", "avail": true, "age": 32, "date": 398556000000.0}, {"title": "Sydney P. McDonald", "type": "person", "avail": true, "age": 63, "date": 1193871600000.0}, {"title": "Nicholas G. Mathis", "type": "person", "avail": true, "age": 76, "date": 1291417200000.0, "remarks": "Distributes squirrels fashionably."}, {"title": "Victoria X. King", "type": "person", "avail": true, "age": 94}, {"title": "Pat S. Kerr", "type": "person", "avail": true, "age": 61, "date": 39913200000.0}, {"title": "Wanda Z. Sanderson", "type": "person", "state": "s", "avail": true, "age": 24, "date": 370389600000.0}, {"title": "Steven U. Miller", "type": "person", "state": "h", "avail": true, "age": 25, "date": 739231200000.0}, {"title": "Frances Morrison", "type": "person", "state": "h", "avail": true, "age": 21}, {"title": "Leonard S. Watson", "type": "person", "avail": true, "age": 85, "date": 1144360800000.0}]}, {"title": "Maintain clotheses", "type": "role", "expanded": true, "children": [{"title": "Anna W. Wilkins", "type": "person", "avail": true, "age": 29, "date": 634863600000.0, "remarks": "Mashes extents upwardly."}, {"title": "Wanda H. Hart", "type": "person", "state": "s", "avail": true, "age": 70}, {"title": "Carolyn P. Parr", "type": "person", "avail": true, "age": 27, "remarks": "Scales bits blissfully."}, {"title": "Amanda C. Morgan", "type": "person", "avail": true, "age": 80, "date": 1136847600000.0}, {"title": "Lauren P. Grant", "type": "person", "avail": true, "age": 58, "date": 732409200000.0}, {"title": "Nicola B. Berry", "type": "person", "avail": true, "age": 95}, {"title": "Rachel U. James", "type": "person", "avail": true, "age": 83, "date": 755218800000.0}, {"title": "Charlie Nolan", "type": "person", "avail": true, "age": 27}, {"title": "Isaac B. Simpson", "type": "person", "avail": true, "age": 66}]}, {"title": "Sterilize rides", "type": "role", "expanded": true, "children": [{"title": "Harry MacLeod", "type": "person", "avail": true, "age": 48, "date": 298422000000.0}, {"title": "Lucas D. Chapman", "type": "person", "age": 94}, {"title": "Alison King", "type": "person", "state": "s", "avail": true, "age": 40, "remarks": "Impeaches noses lively."}, {"title": "Daryl Nolan", "type": "person", "age": 86, "date": 215046000000.0, "remarks": "Ties notices richly."}, {"title": "Kyle A. Miller", "type": "person", "avail": true, "age": 89}, {"title": "Joshua Greene", "type": "person", "state": "s", "avail": true, "age": 39, "date": 618444000000.0}, {"title": "Colin X. Clark", "type": "person", "avail": true, "age": 60, "date": 1417215600000.0}, {"title": "Luke Vance", "type": "person", "state": "s", "avail": true, "age": 52, "date": 115772400000.0, "remarks": "Varies locks exceptionally."}, {"title": "Blake Paige", "type": "person", "avail": true, "age": 74, "date": 1455663600000.0, "remarks": "Stitches asks poignantly."}, {"title": "Zoe Martin", "type": "person", "avail": true, "age": 95, "remarks": "Writes administrations joyously."}, {"title": "Wendy Burgess", "type": "person", "avail": true, "age": 50, "remarks": "Speaks implements forever."}, {"title": "Lou Hill", "type": "person", "avail": true, "age": 81, "date": 390520800000.0, "remarks": "Surges personalities zestfully."}, {"title": "Steven H. Sanderson", "type": "person", "avail": true, "age": 68, "date": 441414000000.0}, {"title": "Anna Z. Murray", "type": "person", "avail": true, "age": 80, "date": 1216159200000.0, "remarks": "Cribs hands tenderly."}, {"title": "Nicholas F. Ross", "type": "person", "avail": true, "age": 64, "date": 643845600000.0, "remarks": "Favours babies exclusively."}, {"title": "Lisa Mills", "type": "person", "avail": true, "age": 92, "remarks": "Treasures braves infinitely."}, {"title": "Harry I. Randall", "type": "person", "avail": true, "age": 54, "date": 412988400000.0, "remarks": "Convicts discounts carefully."}]}, {"title": "Grade tourists", "type": "role", "expanded": true, "children": [{"title": "Joe Glover", "type": "person", "avail": true, "age": 37}, {"title": "Toby V. Peake", "type": "person", "avail": true, "age": 60, "date": 573778800000.0, "remarks": "Prosecutes damages eagerly."}, {"title": "Thomas L. Turner", "type": "person", "avail": true, "age": 60, "date": 1224540000000.0, "remarks": "Spoils texts believably."}, {"title": "Heather T. Dowd", "type": "person", "avail": true, "age": 80, "date": 842738400000.0}, {"title": "Alexandra McGrath", "type": "person", "avail": true, "age": 86}, {"title": "David S. Lawrence", "type": "person", "avail": true, "age": 64, "remarks": "Indicates sessions lightly."}, {"title": "Joe Lyman", "type": "person", "state": "s", "avail": true, "age": 53, "date": 122770800000.0, "remarks": "Impends laughs luxuriously."}, {"title": "Victoria Carr", "type": "person", "age": 28, "date": 180226800000.0}, {"title": "Joe U. Davidson", "type": "person", "avail": true, "age": 68, "date": 655167600000.0, "remarks": "Supplies kills unselfishly."}, {"title": "Toby W. Dyer", "type": "person", "avail": true, "age": 26}, {"title": "Piers Graham", "type": "person", "avail": true, "age": 44}]}]}, {"title": "Dept. for Singles and Anybodies", "type": "department", "children": [{"title": "Resist trainings", "type": "role", "children": [{"title": "Corey Wilkins", "type": "person", "avail": true, "age": 21, "remarks": "Subscribes rocks gladly."}, {"title": "Rose U. Wilkins", "type": "person", "state": "h", "avail": true, "age": 51, "date": 1379368800000.0}, {"title": "Lisa Metcalfe", "type": "person", "state": "s", "avail": true, "age": 98, "date": 1232146800000.0}, {"title": "Jacob Newman", "type": "person", "avail": true, "age": 77, "remarks": "Decorates swims graciously."}, {"title": "Owen O. Dowd", "type": "person", "avail": true, "age": 41, "remarks": "Varies meals forever."}, {"title": "Alex K. Welch", "type": "person", "age": 72, "date": 620863200000.0}, {"title": "Frances McDonald", "type": "person", "age": 36, "date": 211417200000.0}]}, {"title": "Owe safes", "type": "role", "children": [{"title": "Sarah Hodges", "type": "person", "avail": true, "age": 39, "date": 182732400000.0, "remarks": "Enters shes spectacularly."}, {"title": "Sydney Manning", "type": "person", "state": "h", "avail": true, "age": 36, "date": 1448146800000.0, "remarks": "Sights grasshoppers cleanly."}, {"title": "Rose Y. Lawrence", "type": "person", "avail": true, "age": 94, "date": 1384902000000.0}, {"title": "Nathan Robertson", "type": "person", "age": 79, "remarks": "Courses credits reponsibly."}, {"title": "Alexandra I. Clarkson", "type": "person", "age": 35, "date": 841615200000.0}, {"title": "Liam S. Edmunds", "type": "person", "avail": true, "age": 60, "date": 28422000000.0, "remarks": "Samples indications specially."}, {"title": "Emma D. Blake", "type": "person", "avail": true, "age": 96, "date": 653781600000.0}, {"title": "Rachel H. Terry", "type": "person", "state": "h", "avail": true, "age": 35}, {"title": "Katherine Bower", "type": "person", "avail": true, "age": 29, "date": 515628000000.0, "remarks": "Decorates satisfactions intelligently."}, {"title": "Adam F. Hardacre", "type": "person", "state": "h", "avail": true, "age": 69, "date": 773618400000.0, "remarks": "Pretends frogs admiringly."}, {"title": "Toby Brown", "type": "person", "avail": true, "age": 95, "date": 1215295200000.0, "remarks": "Cycles creams appreciably."}, {"title": "Tim Martin", "type": "person", "avail": true, "age": 38}]}, {"title": "Pollute mules", "type": "role", "children": [{"title": "Stephen W. King", "type": "person", "avail": true, "age": 53}, {"title": "Stewart O. Turner", "type": "person", "state": "h", "avail": true, "age": 71, "date": 1254866400000.0}, {"title": "Rose Fisher", "type": "person", "avail": true, "age": 45, "date": 1065650400000.0}, {"title": "Molly S. Paterson", "type": "person", "avail": true, "age": 50, "date": 1010271600000.0}, {"title": "Daryl V. Black", "type": "person", "avail": true, "age": 87, "date": 163033200000.0}, {"title": "Felicity P. Coleman", "type": "person", "avail": true, "age": 42}, {"title": "James Taylor", "type": "person", "avail": true, "age": 80, "date": 23583600000.0}, {"title": "Phoenix A. Rees", "type": "person", "state": "h", "avail": true, "age": 70, "date": 673135200000.0}, {"title": "Colin G. Randall", "type": "person", "state": "s", "avail": true, "age": 83, "date": 405903600000.0, "remarks": "Decays universities handily."}, {"title": "Kelly W. Mills", "type": "person", "avail": true, "age": 76, "date": 832716000000.0}, {"title": "Jan Powell", "type": "person", "state": "s", "avail": true, "age": 57}, {"title": "Bella N. McGrath", "type": "person", "avail": true, "age": 68}, {"title": "Alison Cameron", "type": "person", "age": 21, "date": 104108400000.0}, {"title": "Emma Churchill", "type": "person", "state": "s", "avail": true, "age": 73, "date": 1272751200000.0}, {"title": "Jack A. Davidson", "type": "person", "avail": true, "age": 43, "date": 1205190000000.0}, {"title": "Piers K. Brown", "type": "person", "avail": true, "age": 23, "date": 1319407200000.0}, {"title": "Una R. McLean", "type": "person", "avail": true, "age": 79, "remarks": "Whispers foxes unselfishly."}, {"title": "Sebastian Parr", "type": "person", "avail": true, "age": 73}, {"title": "Jesse Jones", "type": "person", "avail": true, "age": 46, "remarks": "Wastes horrors intensely."}]}, {"title": "Beseech mammoths", "type": "role", "children": [{"title": "Charles Smith", "type": "person", "avail": true, "age": 91, "remarks": "Drives ropes affirmatively."}, {"title": "Simon Hunter", "type": "person", "state": "h", "avail": true, "age": 44, "date": 954885600000.0}, {"title": "Samantha Peake", "type": "person", "state": "s", "avail": true, "age": 58}, {"title": "Austin Burgess", "type": "person", "avail": true, "age": 59, "date": 63759600000.0, "remarks": "Saddles souths highly."}, {"title": "Daryl Arnold", "type": "person", "state": "s", "age": 67, "date": 230770800000.0, "remarks": "Restrains eyes abundantly."}, {"title": "Christopher Lee", "type": "person", "state": "s", "avail": true, "age": 43}, {"title": "Alan C. MacDonald", "type": "person", "state": "s", "avail": true, "age": 93}, {"title": "Emma I. Lawrence", "type": "person", "avail": true, "age": 42, "remarks": "Lends potentials brightly."}, {"title": "Anne H. Oliver", "type": "person", "avail": true, "age": 26, "date": 1286488800000.0}, {"title": "Neil Poole", "type": "person", "avail": true, "age": 93, "date": 1069887600000.0, "remarks": "Implants steaks upright."}]}, {"title": "Signify yellows", "type": "role", "expanded": true, "children": [{"title": "Jean F. Rees", "type": "person", "state": "s", "avail": true, "age": 45}, {"title": "Fiona Y. Forsyth", "type": "person", "state": "s", "avail": true, "age": 31, "remarks": "Excludes comparisons faithfully."}, {"title": "Benjamin Buckland", "type": "person", "avail": true, "age": 67, "date": 26262000000.0}, {"title": "Dan Hunter", "type": "person", "age": 48, "date": 487980000000.0}, {"title": "Boris Churchill", "type": "person", "state": "h", "avail": true, "age": 81}, {"title": "Brian Nash", "type": "person", "avail": true, "age": 52}, {"title": "Chris Black", "type": "person", "avail": true, "age": 90, "date": 1081720800000.0, "remarks": "Clicks comfortables upright."}, {"title": "Joanne O. Langdon", "type": "person", "avail": true, "age": 27}, {"title": "Simon J. Greene", "type": "person", "state": "h", "avail": true, "age": 73, "date": 1180476000000.0}]}, {"title": "Contrast towers", "type": "role", "children": [{"title": "Charlie Duncan", "type": "person", "state": "h", "avail": true, "age": 53, "remarks": "Shines passengers decently."}, {"title": "Ella F. Gibson", "type": "person", "avail": true, "age": 87, "remarks": "Swears witnesses entirely."}, {"title": "Elizabeth North", "type": "person", "age": 69, "date": 618616800000.0}, {"title": "Blake Hart", "type": "person", "state": "h", "avail": true, "age": 95, "date": 75682800000.0}, {"title": "Tim Grant", "type": "person", "avail": true, "age": 38}, {"title": "Elizabeth R. Simpson", "type": "person", "avail": true, "age": 57, "date": 402274800000.0}, {"title": "Chloe Tucker", "type": "person", "avail": true, "age": 79, "remarks": "Contradicts airports generously."}, {"title": "Charlie Mitchell", "type": "person", "avail": true, "age": 72}, {"title": "Alexandra D. Short", "type": "person", "avail": true, "age": 62}, {"title": "Frank Z. Fraser", "type": "person", "avail": true, "age": 79}, {"title": "Rose Gill", "type": "person", "avail": true, "age": 86, "date": 1446591600000.0, "remarks": "Arises educations explicitly."}, {"title": "Owen Paterson", "type": "person", "avail": true, "age": 91}]}, {"title": "Fight trades", "type": "role", "expanded": true, "children": [{"title": "Abigail W. Greene", "type": "person", "avail": true, "age": 36}, {"title": "Yvonne Q. Vaughan", "type": "person", "state": "s", "avail": true, "age": 81, "date": 1423436400000.0}, {"title": "Andy Y. McGrath", "type": "person", "state": "s", "avail": true, "age": 79, "date": 1526508000000.0}, {"title": "Alex J. Gibson", "type": "person", "avail": true, "age": 74, "date": 1418770800000.0}, {"title": "Dorothy Roberts", "type": "person", "avail": true, "age": 86, "remarks": "Verifies asses explicitly."}, {"title": "Toby Parsons", "type": "person", "avail": true, "age": 60, "date": 231980400000.0}, {"title": "Hannah North", "type": "person", "avail": true, "age": 49, "date": 184806000000.0}, {"title": "Sophie Q. Bell", "type": "person", "avail": true, "age": 98, "date": 1153605600000.0}, {"title": "Hannah U. Bailey", "type": "person", "avail": true, "age": 71, "date": 111020400000.0}, {"title": "Brandon J. Clarkson", "type": "person", "state": "h", "avail": true, "age": 35, "date": 1037746800000.0}, {"title": "Jean O. Dickens", "type": "person", "avail": true, "age": 97, "remarks": "Prepares spends thoroughly."}, {"title": "Colin Langdon", "type": "person", "age": 95, "date": 1660255200000.0}, {"title": "Phoenix Morrison", "type": "person", "avail": true, "age": 65, "date": 780706800000.0, "remarks": "Blushes ages carefully."}, {"title": "Sophie E. Bower", "type": "person", "state": "h", "avail": true, "age": 25}, {"title": "Edward E. Mitchell", "type": "person", "state": "s", "avail": true, "age": 34, "date": 542070000000.0}]}]}, {"title": "Dept. for Schools and Standards", "type": "department", "children": [{"title": "Fight messages", "type": "role", "children": [{"title": "Blake Paige", "type": "person", "avail": true, "age": 76, "remarks": "Swims wifes fluently."}, {"title": "Max G. Metcalfe", "type": "person", "avail": true, "age": 42}, {"title": "Jessica I. Martin", "type": "person", "avail": true, "age": 51, "date": 659574000000.0}, {"title": "Victor C. Roberts", "type": "person", "avail": true, "age": 79, "date": 1599948000000.0}, {"title": "Lauren Hamilton", "type": "person", "avail": true, "age": 77, "date": 1228258800000.0}, {"title": "Jessica W. Bell", "type": "person", "avail": true, "age": 94, "date": 316825200000.0}, {"title": "Pat Paterson", "type": "person", "avail": true, "age": 61, "date": 721609200000.0}, {"title": "Toby Poole", "type": "person", "avail": true, "age": 83, "date": 1659996000000.0, "remarks": "Admits joins perfectly."}, {"title": "Frances Z. Parr", "type": "person", "state": "s", "age": 52}, {"title": "Frances Randall", "type": "person", "avail": true, "age": 44, "date": 365983200000.0}, {"title": "Casey Gibson", "type": "person", "state": "s", "avail": true, "age": 72, "remarks": "Sanctions routines passionately."}, {"title": "Emma Parsons", "type": "person", "avail": true, "age": 50, "date": 303692400000.0, "remarks": "Wanders mixtures remarkably."}, {"title": "Austin Gray", "type": "person", "avail": true, "age": 24, "date": 904255200000.0}, {"title": "Colin F. Wilkins", "type": "person", "avail": true, "age": 72, "date": 1446418800000.0, "remarks": "Hinders teaches well."}]}, {"title": "Eye grasses", "type": "role", "expanded": true, "children": [{"title": "Eric Hemmings", "type": "person", "avail": true, "age": 37, "date": 1368914400000.0, "remarks": "Weaves lows boldly."}, {"title": "Olivia G. Cornish", "type": "person", "avail": true, "age": 65, "date": 1144792800000.0}, {"title": "Max M. Hart", "type": "person", "avail": true, "age": 61, "date": 1486594800000.0}, {"title": "Edward Short", "type": "person", "avail": true, "age": 84, "date": 313455600000.0}, {"title": "Irene R. Simpson", "type": "person", "avail": true, "age": 54}, {"title": "Daryl X. Paterson", "type": "person", "avail": true, "age": 63}]}, {"title": "Saw insides", "type": "role", "expanded": true, "children": [{"title": "Daryl Kerr", "type": "person", "state": "h", "avail": true, "age": 53, "date": 1233788400000.0, "remarks": "Counsels generals efficiently."}, {"title": "Jane I. Rampling", "type": "person", "avail": true, "age": 21, "date": 1291330800000.0}, {"title": "Steven U. Ferguson", "type": "person", "avail": true, "age": 40, "date": 1621461600000.0, "remarks": "Scams categories healthily."}, {"title": "Corey Fisher", "type": "person", "state": "s", "avail": true, "age": 35, "date": 1285192800000.0}, {"title": "Stephanie Rampling", "type": "person", "avail": true, "age": 25, "date": 165625200000.0}, {"title": "Amelia F. Roberts", "type": "person", "state": "h", "avail": true, "age": 79, "date": 1166569200000.0}]}, {"title": "Imagine tensions", "type": "role", "children": [{"title": "Luke P. Bond", "type": "person", "avail": true, "age": 67}, {"title": "Julia R. Clarkson", "type": "person", "avail": true, "age": 26, "date": 751590000000.0}, {"title": "Eric Ince", "type": "person", "avail": true, "age": 31}, {"title": "Toby K. Hodges", "type": "person", "avail": true, "age": 70}, {"title": "Jan E. Johnston", "type": "person", "avail": true, "age": 89, "date": 1469052000000.0}, {"title": "Charlie G. Ross", "type": "person", "avail": true, "age": 29}, {"title": "Rose N. Blake", "type": "person", "state": "h", "avail": true, "age": 52, "date": 1148076000000.0}, {"title": "Lillian E. Knox", "type": "person", "avail": true, "age": 21, "date": 1576882800000.0}, {"title": "Alison I. Welch", "type": "person", "avail": true, "age": 23}, {"title": "Toby North", "type": "person", "avail": true, "age": 41, "date": 284511600000.0}]}, {"title": "Incise dogs", "type": "role", "children": [{"title": "Wanda B. May", "type": "person", "age": 72, "date": 1010271600000.0}, {"title": "Keith Bower", "type": "person", "avail": true, "age": 51, "date": 948063600000.0}, {"title": "Una Piper", "type": "person", "state": "s", "avail": true, "age": 97, "date": 486252000000.0, "remarks": "Glitters policies amazingly."}, {"title": "Victor J. Scott", "type": "person", "age": 43, "date": 1457910000000.0, "remarks": "Pours prompts rapidly."}, {"title": "Matt B. Rampling", "type": "person", "avail": true, "age": 70, "remarks": "Retires softs ethically."}, {"title": "Molly Q. Nash", "type": "person", "avail": true, "age": 59, "date": 1531519200000.0}, {"title": "Sydney Stewart", "type": "person", "avail": true, "age": 85, "date": 440118000000.0}, {"title": "Steven D. Gill", "type": "person", "state": "s", "avail": true, "age": 21}, {"title": "Sydney N. Piper", "type": "person", "avail": true, "age": 27, "date": 582847200000.0}]}, {"title": "Recur creams", "type": "role", "children": [{"title": "Joshua P. Poole", "type": "person", "age": 97, "remarks": "Tests crickets jubilantly."}]}, {"title": "March wonders", "type": "role", "children": [{"title": "Elizabeth Lee", "type": "person", "avail": true, "age": 51}, {"title": "Taylor U. Vaughan", "type": "person", "avail": true, "age": 57}, {"title": "Gavin Peake", "type": "person", "avail": true, "age": 91, "date": 186620400000.0}, {"title": "Anne K. Hart", "type": "person", "state": "h", "avail": true, "age": 89, "date": 163465200000.0, "remarks": "Controls readings extensively."}, {"title": "Brian Hughes", "type": "person", "avail": true, "age": 64, "remarks": "Ponders grandmothers luckily."}]}, {"title": "Rewind reasons", "type": "role", "children": [{"title": "Edward I. Taylor", "type": "person", "avail": true, "age": 93, "date": 633394800000.0, "remarks": "Greets cities delightfully."}, {"title": "Elizabeth Q. Bower", "type": "person", "state": "h", "avail": true, "age": 83, "date": 543970800000.0}, {"title": "Christian Underwood", "type": "person", "avail": true, "age": 50, "date": 225068400000.0}]}, {"title": "Donate regulars", "type": "role", "expanded": true, "children": [{"title": "Jennifer I. Manning", "type": "person", "state": "h", "avail": true, "age": 56, "date": 889052400000.0}, {"title": "Karen T. Chapman", "type": "person", "avail": true, "age": 63, "date": 1140044400000.0}, {"title": "Taylor Mackay", "type": "person", "avail": true, "age": 46}, {"title": "Thomas Ogden", "type": "person", "avail": true, "age": 41, "date": 1520809200000.0, "remarks": "Imprisons leads reliably."}, {"title": "Blake Carr", "type": "person", "state": "s", "avail": true, "age": 26, "remarks": "Scorches instances diligently."}, {"title": "Charlie J. Hodges", "type": "person", "avail": true, "age": 92, "date": 702424800000.0}]}, {"title": "Quit editors", "type": "role", "children": [{"title": "Lucas K. North", "type": "person", "state": "h", "avail": true, "age": 76, "date": 424994400000.0}, {"title": "Adrian Piper", "type": "person", "avail": true, "age": 51, "date": 295138800000.0}, {"title": "Jayden Hill", "type": "person", "state": "s", "avail": true, "age": 93, "date": 1248300000000.0, "remarks": "Damps hosts explicitly."}, {"title": "Ruth Wilson", "type": "person", "avail": true, "age": 33, "date": 505263600000.0}, {"title": "Neil Tucker", "type": "person", "state": "h", "avail": true, "age": 24}, {"title": "Adam Dickens", "type": "person", "avail": true, "age": 44, "date": 1088114400000.0, "remarks": "Guards illegals happily."}, {"title": "Sean Alsop", "type": "person", "avail": true, "age": 69, "date": 650757600000.0}, {"title": "Kylie D. Powell", "type": "person", "avail": true, "age": 58}, {"title": "Yvonne X. King", "type": "person", "age": 52, "date": 1590703200000.0, "remarks": "Teaches tops respectably."}, {"title": "Paul Mitchell", "type": "person", "avail": true, "age": 72, "date": 1337205600000.0}, {"title": "Heather Campbell", "type": "person", "avail": true, "age": 50, "date": 67734000000.0}, {"title": "Sam E. Dyer", "type": "person", "avail": true, "age": 30}, {"title": "Anne F. Mitchell", "type": "person", "avail": true, "age": 28, "remarks": "Constitutes natures earnestly."}, {"title": "Frances P. Gill", "type": "person", "avail": true, "age": 69, "date": 186361200000.0}, {"title": "Angela J. Anderson", "type": "person", "avail": true, "age": 95}, {"title": "Sarah B. McDonald", "type": "person", "avail": true, "age": 96}, {"title": "Kimberly Peters", "type": "person", "avail": true, "age": 94, "date": 697503600000.0}, {"title": "Carl M. May", "type": "person", "avail": true, "age": 34, "remarks": "Implodes stars assuredly."}, {"title": "Gordon T. McDonald", "type": "person", "avail": true, "age": 49, "date": 1083880800000.0}]}, {"title": "Crave horrors", "type": "role", "children": [{"title": "Andrea D. Newman", "type": "person", "state": "s", "avail": true, "age": 45}, {"title": "Jonathan B. Vaughan", "type": "person", "avail": true, "age": 26}]}]}, {"title": "Dept. for Doors and Backgrounds", "type": "department", "children": [{"title": "Reply lifts", "type": "role", "children": [{"title": "Kimberly Cornish", "type": "person", "avail": true, "age": 61, "date": 243558000000.0, "remarks": "Shaves slides amazingly."}, {"title": "Molly Z. Ball", "type": "person", "avail": true, "age": 45, "date": 1403388000000.0, "remarks": "Retches jumps helpfully."}, {"title": "Fiona L. Short", "type": "person", "avail": true, "age": 33}, {"title": "Madeleine W. Vaughan", "type": "person", "avail": true, "age": 39, "remarks": "Adjusts designs delicately."}, {"title": "Jacob Turner", "type": "person", "avail": true, "age": 47, "date": 615592800000.0}, {"title": "Stephen M. Mills", "type": "person", "avail": true, "age": 90, "date": 1399586400000.0, "remarks": "Fetches amounts early."}]}, {"title": "Persuade natives", "type": "role", "expanded": true, "children": [{"title": "Frances J. Blake", "type": "person", "state": "s", "avail": true, "age": 89, "remarks": "Cools midnights suitably."}, {"title": "Toby J. Morgan", "type": "person", "age": 60}, {"title": "Jane McDonald", "type": "person", "avail": true, "age": 64, "date": 986162400000.0}, {"title": "Una Robertson", "type": "person", "state": "s", "avail": true, "age": 70, "date": 647733600000.0, "remarks": "Flaps structures fluently."}, {"title": "Bobbie O. Watson", "type": "person", "state": "h", "avail": true, "age": 28, "remarks": "States brothers extraordinarily."}, {"title": "Gordon Lawrence", "type": "person", "avail": true, "age": 29, "date": 1221861600000.0}, {"title": "Ryan K. Dickens", "type": "person", "state": "s", "avail": true, "age": 21, "remarks": "Shears levels interestingly."}, {"title": "Michelle Howard", "type": "person", "state": "s", "avail": true, "age": 92, "date": 823129200000.0}, {"title": "Theresa Nash", "type": "person", "state": "h", "avail": true, "age": 29, "date": 733788000000.0}, {"title": "Max McDonald", "type": "person", "state": "h", "avail": true, "age": 82, "date": 1511218800000.0, "remarks": "Improves histories intently."}, {"title": "Emma A. Black", "type": "person", "avail": true, "age": 23, "date": 197938800000.0, "remarks": "Hustles beginnings happily."}, {"title": "Brandon L. Turner", "type": "person", "state": "s", "avail": true, "age": 40, "remarks": "Scolds angers deeply."}, {"title": "Andy Y. Ross", "type": "person", "avail": true, "age": 77, "date": 519775200000.0, "remarks": "Inflames tests skillfully."}]}, {"title": "Rest arrivals", "type": "role", "expanded": true, "children": [{"title": "Jonathan B. McDonald", "type": "person", "avail": true, "age": 81}, {"title": "Jesse Berry", "type": "person", "avail": true, "age": 62}, {"title": "Sydney Harris", "type": "person", "avail": true, "age": 67, "date": 586130400000.0}, {"title": "Irene Johnston", "type": "person", "avail": true, "age": 61, "date": 787791600000.0}, {"title": "Jesse N. Reid", "type": "person", "avail": true, "age": 32, "date": 643327200000.0}, {"title": "Nicholas Peters", "type": "person", "avail": true, "age": 46}, {"title": "Heather Abraham", "type": "person", "avail": true, "age": 43, "date": 848185200000.0}, {"title": "Christopher Q. Watson", "type": "person", "avail": true, "age": 37, "remarks": "Smoothes obligations brilliantly."}, {"title": "Julian B. Marshall", "type": "person", "avail": true, "age": 23, "date": 156812400000.0}, {"title": "Felicity M. Sharp", "type": "person", "avail": true, "age": 49}, {"title": "Connor Ellison", "type": "person", "avail": true, "age": 68, "remarks": "Scarps guies fortunately."}]}, {"title": "Help rows", "type": "role", "children": [{"title": "Angela A. Coleman", "type": "person", "avail": true, "age": 75}, {"title": "Ian Paige", "type": "person", "avail": true, "age": 37, "date": 1026770400000.0}, {"title": "Maria Wilson", "type": "person", "avail": true, "age": 98, "date": 426808800000.0}]}, {"title": "Breed girls", "type": "role", "expanded": true, "children": [{"title": "James Metcalfe", "type": "person", "avail": true, "age": 61, "remarks": "Waxes grandmothers upwardly."}, {"title": "Grace McGrath", "type": "person", "avail": true, "age": 30}, {"title": "Joshua Reid", "type": "person", "state": "h", "avail": true, "age": 38}, {"title": "Abigail H. Hunter", "type": "person", "avail": true, "age": 33, "date": 1429308000000.0}, {"title": "Felicity F. Rees", "type": "person", "avail": true, "age": 85, "remarks": "Frees mights significantly."}, {"title": "Jesse Graham", "type": "person", "avail": true, "age": 33, "date": 1652738400000.0}, {"title": "Frances U. Reid", "type": "person", "state": "h", "avail": true, "age": 24, "date": 210898800000.0}, {"title": "Julia I. Walsh", "type": "person", "state": "s", "avail": true, "age": 65, "date": 737589600000.0, "remarks": "Utters requirements very."}, {"title": "Jack Y. Davidson", "type": "person", "avail": true, "age": 62, "date": 1566252000000.0, "remarks": "Scorches hamsters immeasurably."}, {"title": "Tim Hemmings", "type": "person", "avail": true, "age": 57, "date": 1591308000000.0, "remarks": "Sways treats healthily."}, {"title": "Carolyn Young", "type": "person", "avail": true, "age": 44}, {"title": "Oliver Randall", "type": "person", "state": "s", "avail": true, "age": 71, "date": 1361919600000.0}, {"title": "Dylan Gibson", "type": "person", "avail": true, "age": 38, "date": 1131058800000.0}, {"title": "Pippa H. May", "type": "person", "avail": true, "age": 68}]}, {"title": "Scald acts", "type": "role", "children": [{"title": "Tim L. Miller", "type": "person", "avail": true, "age": 31, "date": 1585864800000.0, "remarks": "Boasts farms easily."}, {"title": "Pippa Stewart", "type": "person", "avail": true, "age": 31, "date": 718758000000.0}, {"title": "Charlie Young", "type": "person", "age": 69, "date": 827017200000.0, "remarks": "Looks futures reliably."}, {"title": "Claire Avery", "type": "person", "avail": true, "age": 63, "date": 1360018800000.0}, {"title": "Matt Johnston", "type": "person", "state": "h", "avail": true, "age": 68, "date": 1500156000000.0}, {"title": "Frances Underwood", "type": "person", "avail": true, "age": 30, "date": 1623276000000.0}, {"title": "Chris A. Walker", "type": "person", "avail": true, "age": 45}, {"title": "Caroline Paterson", "type": "person", "avail": true, "age": 41}, {"title": "Phil Z. Morgan", "type": "person", "avail": true, "age": 50}, {"title": "Peter Peake", "type": "person", "state": "s", "avail": true, "age": 74, "date": 122598000000.0}, {"title": "Jamie Scott", "type": "person", "avail": true, "age": 39, "date": 645832800000.0}]}, {"title": "Correct beautifuls", "type": "role", "children": [{"title": "Matt Q. Duncan", "type": "person", "state": "h", "avail": true, "age": 90, "date": 1204844400000.0}, {"title": "Angela Taylor", "type": "person", "state": "s", "avail": true, "age": 82}, {"title": "Sonia Wilson", "type": "person", "state": "h", "avail": true, "age": 98, "date": 1093125600000.0, "remarks": "Contrasts efforts explicitly."}, {"title": "Emma S. Reid", "type": "person", "avail": true, "age": 42}, {"title": "Victoria D. Butler", "type": "person", "state": "h", "avail": true, "age": 46}, {"title": "Eddie Q. Newman", "type": "person", "avail": true, "age": 44, "date": 7686000000.0}, {"title": "Wanda F. King", "type": "person", "avail": true, "age": 45, "date": 410310000000.0}, {"title": "Sue W. Jackson", "type": "person", "avail": true, "age": 57}, {"title": "Jan P. Brown", "type": "person", "state": "s", "age": 58, "date": 432856800000.0}, {"title": "Nicholas T. Sutherland", "type": "person", "state": "s", "avail": true, "age": 21, "date": 1125525600000.0}, {"title": "Rebecca Rampling", "type": "person", "state": "s", "age": 42, "date": 1195945200000.0}, {"title": "Penelope Grant", "type": "person", "avail": true, "age": 89, "date": 703893600000.0, "remarks": "Tears menus usefully."}, {"title": "Eddie Z. Duncan", "type": "person", "avail": true, "age": 42}, {"title": "Michael Reid", "type": "person", "avail": true, "age": 51, "date": 1288998000000.0}, {"title": "Michelle Hughes", "type": "person", "avail": true, "age": 27, "date": 1144620000000.0}, {"title": "Connor S. Ball", "type": "person", "state": "h", "avail": true, "age": 70}, {"title": "Jan K. Scott", "type": "person", "state": "h", "avail": true, "age": 53, "date": 1236726000000.0}, {"title": "Max Abraham", "type": "person", "avail": true, "age": 48, "remarks": "Hopes girlfriends elegantly."}, {"title": "Frances B. McLean", "type": "person", "state": "s", "avail": true, "age": 97, "remarks": "Complains visuals rapidly."}]}, {"title": "Change chains", "type": "role", "expanded": true, "children": [{"title": "Kylie Buckland", "type": "person", "state": "h", "age": 35}, {"title": "Kelly W. Lewis", "type": "person", "avail": true, "age": 37, "date": 1092520800000.0}]}]}] ================================================ FILE: docs/assets/json/fixture_department_1k_3_6_t_c.json ================================================ {"types": {"department": {"icon": "bi bi-diagram-3", "colspan": true}, "role": {"icon": "bi bi-microsoft-teams", "colspan": true}, "person": {"icon": "bi bi-person"}}, "columns": [{"title": "Title", "id": "*", "width": "250px"}, {"title": "Age", "id": "age", "width": "50px", "html": "", "classes": "wb-helper-end"}, {"title": "Date", "id": "date", "width": "100px", "html": ""}, {"title": "Status", "id": "state", "width": "70px", "html": "\n "}, {"title": "Avail", "id": "avail", "width": "30px", "html": ""}, {"title": "Remarks", "id": "remarks", "width": "*", "html": ""}], "children": [{"title": "Dept. for Fails and Pounds", "type": "department", "children": [{"title": "Forget visuals", "type": "role"}, {"title": "Treasure designers", "type": "role", "expanded": true, "children": [{"title": "Zoe M. Rampling", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1048028400000.0}, {"title": "Carolyn Wilkins", "type": "person", "avail": true, "age": 83, "date": 879375600000.0}, {"title": "Diane Vance", "type": "person", "avail": true, "age": 66, "date": 289609200000.0, "remarks": "Improves desires helpfully."}, {"title": "Lauren Rees", "type": "person", "state": "s", "avail": true, "age": 57, "remarks": "Imposes birthdaies unbelievably."}, {"title": "Audrey Miller", "type": "person", "avail": true, "age": 77, "date": 432252000000.0}, {"title": "David Marshall", "type": "person", "state": "s", "avail": true, "age": 43}, {"title": "Richard Hemmings", "type": "person", "avail": true, "age": 61, "date": 1106262000000.0, "remarks": "Implants bands greatly."}, {"title": "Anthony C. Peake", "type": "person", "avail": true, "age": 48, "remarks": "Stirs antelopes zestfully."}, {"title": "Alan Terry", "type": "person", "state": "h", "avail": true, "age": 97, "date": 1220479200000.0}, {"title": "Max May", "type": "person", "avail": true, "age": 61, "date": 1572303600000.0, "remarks": "Measures middles warmly."}, {"title": "Nicola G. Scott", "type": "person", "avail": true, "age": 66}, {"title": "Ian C. Welch", "type": "person", "avail": true, "age": 76, "date": 926546400000.0}, {"title": "Christian G. Glover", "type": "person", "state": "h", "avail": true, "age": 45, "date": 1080428400000.0}, {"title": "Corey A. Alsop", "type": "person", "avail": true, "age": 29}, {"title": "Paul McDonald", "type": "person", "age": 75, "remarks": "Understands moms amazingly."}, {"title": "Chris Lewis", "type": "person", "age": 62, "date": 1059516000000.0}, {"title": "Taylor C. Lyman", "type": "person", "avail": true, "age": 91, "date": 1500933600000.0}]}, {"title": "Point securities", "type": "role", "children": [{"title": "Jamie A. Gill", "type": "person", "state": "h", "avail": true, "age": 92}, {"title": "Una Sutherland", "type": "person", "avail": true, "age": 93, "date": 687222000000.0}, {"title": "Ian D. Hamilton", "type": "person", "avail": true, "age": 44, "date": 574383600000.0, "remarks": "Closes fails carefully."}, {"title": "Felicity Morrison", "type": "person", "avail": true, "age": 91, "date": 1028844000000.0}, {"title": "Eric Glover", "type": "person", "avail": true, "age": 21}, {"title": "Tracey I. Tucker", "type": "person", "avail": true, "age": 65, "remarks": "Deprives notes considerately."}, {"title": "Julian S. Walker", "type": "person", "avail": true, "age": 56}, {"title": "Taylor Nash", "type": "person", "avail": true, "age": 30, "date": 1272751200000.0, "remarks": "Caresses beginnings truly."}, {"title": "Pippa Y. Piper", "type": "person", "avail": true, "age": 64}, {"title": "Kelly C. Buckland", "type": "person", "avail": true, "age": 66}, {"title": "Victoria Johnston", "type": "person", "avail": true, "age": 63, "date": 1630274400000.0}, {"title": "William Z. Turner", "type": "person", "state": "h", "avail": true, "age": 81, "remarks": "Presets healths poignantly."}, {"title": "Michelle W. Ross", "type": "person", "avail": true, "age": 73, "date": 583365600000.0}]}, {"title": "Resell courages", "type": "role", "expanded": true, "children": [{"title": "Gavin S. Rees", "type": "person", "avail": true, "age": 74}, {"title": "Casey R. Poole", "type": "person", "avail": true, "age": 33, "date": 1363993200000.0}, {"title": "Ruth S. Oliver", "type": "person", "avail": true, "age": 34, "date": 273452400000.0}, {"title": "Taylor Anderson", "type": "person", "state": "s", "avail": true, "age": 66, "date": 805068000000.0, "remarks": "Treads texts gleefully."}, {"title": "Una Sanderson", "type": "person", "avail": true, "age": 85, "date": 1275602400000.0}, {"title": "Sam D. Terry", "type": "person", "avail": true, "age": 78, "date": 481932000000.0}, {"title": "John James", "type": "person", "avail": true, "age": 26, "remarks": "Loves kids markedly."}, {"title": "Carolyn C. Davidson", "type": "person", "age": 31}, {"title": "Kyle A. Reid", "type": "person", "avail": true, "age": 54, "date": 1499119200000.0, "remarks": "Melts sells markedly."}, {"title": "Keith Bond", "type": "person", "state": "h", "avail": true, "age": 54, "date": 38358000000.0}, {"title": "Penelope A. Rampling", "type": "person", "avail": true, "age": 95}, {"title": "Deirdre C. Skinner", "type": "person", "avail": true, "age": 50, "date": 927410400000.0}, {"title": "Leonard Slater", "type": "person", "avail": true, "age": 40, "date": 1561586400000.0, "remarks": "Feels weathers graciously."}, {"title": "Austin Sutherland", "type": "person", "avail": true, "age": 47}, {"title": "Charles Lyman", "type": "person", "avail": true, "age": 40, "date": 571964400000.0}, {"title": "Nathan N. Ross", "type": "person", "state": "s", "avail": true, "age": 52}, {"title": "Chris Oliver", "type": "person", "state": "s", "avail": true, "age": 30, "remarks": "Bends ages appropriately."}, {"title": "Sydney F. Wilkins", "type": "person", "avail": true, "age": 66, "date": 1041030000000.0}, {"title": "Corey D. Bailey", "type": "person", "avail": true, "age": 92}]}, {"title": "Salve hours", "type": "role", "children": [{"title": "Jasmine Lawrence", "type": "person", "avail": true, "age": 50}, {"title": "Lauren B. Hudson", "type": "person", "avail": true, "age": 96}]}, {"title": "Detach fews", "type": "role", "children": [{"title": "Ian Black", "type": "person", "age": 45, "remarks": "Suffers failures upbeat."}, {"title": "Joan P. May", "type": "person", "age": 22}, {"title": "Blake P. Murray", "type": "person", "avail": true, "age": 45, "date": 118105200000.0, "remarks": "Ponders priorities appropriately."}, {"title": "Stephanie Piper", "type": "person", "avail": true, "age": 54, "date": 185670000000.0}, {"title": "Stephanie Mitchell", "type": "person", "state": "s", "avail": true, "age": 27}, {"title": "Alexandra Arnold", "type": "person", "age": 41}, {"title": "Fiona U. Sharp", "type": "person", "state": "s", "avail": true, "age": 59, "date": 143247600000.0, "remarks": "Wrings marriages appropriately."}, {"title": "Anthony G. Paterson", "type": "person", "avail": true, "age": 80, "date": 345164400000.0, "remarks": "Stores motors freely."}, {"title": "Austin G. Hodges", "type": "person", "avail": true, "age": 87, "date": 597884400000.0}, {"title": "Faith U. Dowd", "type": "person", "avail": true, "age": 77, "date": 472604400000.0}, {"title": "Cameron X. Welch", "type": "person", "state": "h", "avail": true, "age": 53, "date": 879634800000.0, "remarks": "Sings guidances efficiently."}, {"title": "Jack K. Jones", "type": "person", "avail": true, "age": 63, "date": 1489532400000.0, "remarks": "Lands lawyers happily."}, {"title": "Chloe Buckland", "type": "person", "avail": true, "age": 53, "date": 525564000000.0}, {"title": "Emma Tucker", "type": "person", "state": "s", "age": 49, "date": 1467324000000.0}, {"title": "Colin Davidson", "type": "person", "avail": true, "age": 48, "date": 1069110000000.0}, {"title": "Lou MacDonald", "type": "person", "state": "s", "avail": true, "age": 33, "date": 588895200000.0, "remarks": "Consoles charities generously."}]}, {"title": "Indulge fishings", "type": "role", "expanded": true, "children": [{"title": "Austin North", "type": "person", "state": "s", "avail": true, "age": 49, "date": 941497200000.0}, {"title": "Jamie Mackay", "type": "person", "age": 89, "remarks": "Chips tooths substantially."}, {"title": "Andrew Brown", "type": "person", "state": "s", "avail": true, "age": 83, "date": 1208469600000.0}, {"title": "Stephanie W. Fraser", "type": "person", "avail": true, "age": 83, "date": 725324400000.0}, {"title": "Donna Z. Chapman", "type": "person", "avail": true, "age": 97, "date": 918255600000.0, "remarks": "Smashes ages touchingly."}, {"title": "Deirdre J. Wallace", "type": "person", "state": "s", "avail": true, "age": 70}, {"title": "Eddie Alsop", "type": "person", "state": "s", "avail": true, "age": 42}, {"title": "Kylie Ince", "type": "person", "avail": true, "age": 25, "remarks": "Moulds positives exclusively."}, {"title": "Liam Hardacre", "type": "person", "avail": true, "age": 62}, {"title": "John G. Knox", "type": "person", "age": 53, "date": 840146400000.0, "remarks": "Paints funerals clearly."}, {"title": "Taylor E. Rees", "type": "person", "avail": true, "age": 98, "date": 114994800000.0, "remarks": "Supports asides cleanly."}, {"title": "Keith M. Duncan", "type": "person", "state": "h", "avail": true, "age": 36, "date": 1132095600000.0}, {"title": "Sarah I. Dickens", "type": "person", "avail": true, "age": 23, "date": 106873200000.0}, {"title": "Eddie Manning", "type": "person", "state": "s", "avail": true, "age": 75, "date": 670629600000.0}, {"title": "Jan O. Hemmings", "type": "person", "avail": true, "age": 98, "date": 1116280800000.0}]}]}, {"title": "Dept. for Forces and Entrances", "type": "department", "children": [{"title": "Shit slips", "type": "role", "expanded": true}, {"title": "Fry juniors", "type": "role", "expanded": true, "children": [{"title": "Kelly Alsop", "type": "person", "avail": true, "age": 65, "date": 1151100000000.0}, {"title": "Brian P. Anderson", "type": "person", "state": "s", "avail": true, "age": 75}, {"title": "Dominic Watson", "type": "person", "avail": true, "age": 23, "date": 1137452400000.0, "remarks": "Enlightens sorts courageously."}, {"title": "John Greene", "type": "person", "avail": true, "age": 57, "remarks": "Shaves weaknesses punctually."}, {"title": "Samantha Randall", "type": "person", "avail": true, "age": 46}, {"title": "Thomas Jones", "type": "person", "avail": true, "age": 71, "date": 1299279600000.0, "remarks": "Persuades telephones comfortably."}, {"title": "Eddie Langdon", "type": "person", "avail": true, "age": 70, "date": 1350770400000.0}, {"title": "Sarah Alsop", "type": "person", "avail": true, "age": 21}, {"title": "Virginia B. Reid", "type": "person", "avail": true, "age": 30, "remarks": "Expresses toughs optimistically."}, {"title": "Jake R. Rampling", "type": "person", "state": "h", "avail": true, "age": 69, "date": 50886000000.0}, {"title": "Virginia Q. Coleman", "type": "person", "avail": true, "age": 43, "date": 1185055200000.0, "remarks": "Ceases codes smoothly."}, {"title": "Kylie U. May", "type": "person", "state": "s", "avail": true, "age": 38, "remarks": "Dips descriptions magically."}, {"title": "Simon Davies", "type": "person", "avail": true, "age": 23}, {"title": "Boris Reid", "type": "person", "avail": true, "age": 89, "date": 1462140000000.0}, {"title": "Jean I. North", "type": "person", "state": "h", "avail": true, "age": 45}, {"title": "Christopher W. Alsop", "type": "person", "state": "s", "avail": true, "age": 88, "date": 1171321200000.0, "remarks": "Contests pleasures fervently."}, {"title": "Simon Clark", "type": "person", "avail": true, "age": 30, "date": 295830000000.0, "remarks": "Persuades lines optimistically."}, {"title": "Carol U. Langdon", "type": "person", "state": "s", "avail": true, "age": 72}]}, {"title": "Exist turtles", "type": "role", "children": [{"title": "Ava Cameron", "type": "person", "avail": true, "age": 26}, {"title": "Julian Russell", "type": "person", "avail": true, "age": 83}, {"title": "Bella Tucker", "type": "person", "avail": true, "age": 24, "date": 1096063200000.0}]}, {"title": "Grip restaurants", "type": "role", "children": [{"title": "Trevor N. Murray", "type": "person", "state": "h", "avail": true, "age": 26, "date": 1406239200000.0}, {"title": "Nathan Brown", "type": "person", "age": 29}, {"title": "Mary A. Gray", "type": "person", "age": 69, "remarks": "Shears presses restfully."}, {"title": "Alison Miller", "type": "person", "state": "s", "avail": true, "age": 84, "date": 521330400000.0}, {"title": "Austin C. Fisher", "type": "person", "state": "h", "age": 83, "date": 473814000000.0}, {"title": "Luke R. Johnston", "type": "person", "avail": true, "age": 91, "date": 1648422000000.0}, {"title": "Nicholas S. Dickens", "type": "person", "avail": true, "age": 87, "date": 1237503600000.0}, {"title": "Jesse Harris", "type": "person", "avail": true, "age": 52, "date": 469321200000.0}, {"title": "Dan Piper", "type": "person", "avail": true, "age": 88, "date": 1317852000000.0, "remarks": "Buys rises openly."}, {"title": "Victoria K. Hart", "type": "person", "age": 52, "date": 1093384800000.0, "remarks": "Retches crickets longingly."}, {"title": "Max Hodges", "type": "person", "state": "s", "avail": true, "age": 73, "date": 145494000000.0, "remarks": "Inputs deposits valiantly."}, {"title": "Zoe Clark", "type": "person", "avail": true, "age": 28, "date": 1288911600000.0}, {"title": "Andrea J. Piper", "type": "person", "avail": true, "age": 53}, {"title": "Brian Dickens", "type": "person", "state": "h", "avail": true, "age": 85, "date": 165625200000.0}, {"title": "Lucas G. Avery", "type": "person", "state": "s", "avail": true, "age": 36, "date": 1442527200000.0, "remarks": "Wets hells positively."}, {"title": "Bella J. Dowd", "type": "person", "avail": true, "age": 87}]}, {"title": "Overtake temporaries", "type": "role"}, {"title": "Imperil cables", "type": "role", "expanded": true, "children": [{"title": "Matt Lawrence", "type": "person", "state": "h", "avail": true, "age": 96, "date": 235782000000.0}, {"title": "Daryl B. Randall", "type": "person", "avail": true, "age": 77, "date": 1580166000000.0}, {"title": "Jake V. Burgess", "type": "person", "state": "h", "avail": true, "age": 73}, {"title": "Dylan Howard", "type": "person", "state": "h", "avail": true, "age": 59}]}, {"title": "Gash refuses", "type": "role"}, {"title": "Sleep couples", "type": "role", "expanded": true, "children": [{"title": "Piers Q. Rutherford", "type": "person", "avail": true, "age": 49, "date": 197334000000.0}, {"title": "Lucas U. Kelly", "type": "person", "avail": true, "age": 56, "date": 657846000000.0}, {"title": "Chris T. Black", "type": "person", "avail": true, "age": 81, "date": 1177884000000.0}, {"title": "Phoenix Q. Burgess", "type": "person", "state": "h", "avail": true, "age": 61, "date": 629766000000.0}, {"title": "Isaac A. Thomson", "type": "person", "avail": true, "age": 84, "date": 430437600000.0, "remarks": "Retains boats securely."}, {"title": "Anna North", "type": "person", "state": "s", "age": 25, "date": 1482620400000.0, "remarks": "Consents fishes intensely."}, {"title": "Jean E. Turner", "type": "person", "avail": true, "age": 79, "date": 1293836400000.0, "remarks": "Divides excuses thoroughly."}, {"title": "Jesse Q. Gill", "type": "person", "avail": true, "age": 40, "remarks": "Motivates focuses deeply."}, {"title": "Eric E. Hardacre", "type": "person", "avail": true, "age": 95}]}]}, {"title": "Dept. for Walks and Pasts", "type": "department", "children": [{"title": "Crib choices", "type": "role", "children": [{"title": "Alexander G. Lewis", "type": "person", "avail": true, "age": 92, "date": 1192226400000.0, "remarks": "Realizes breaks cleanly."}, {"title": "Theresa Bailey", "type": "person", "state": "s", "age": 50, "remarks": "Scats shelters spectacularly."}, {"title": "Wanda Sutherland", "type": "person", "avail": true, "age": 48, "date": 677109600000.0, "remarks": "Sails points candidly."}, {"title": "Charles H. Oliver", "type": "person", "age": 47}, {"title": "Diana C. Ferguson", "type": "person", "avail": true, "age": 67, "remarks": "Pouts rewards agreeably."}, {"title": "Dylan Berry", "type": "person", "avail": true, "age": 97}]}, {"title": "Express functions", "type": "role", "children": [{"title": "Eddie Simpson", "type": "person", "avail": true, "age": 83, "date": 767570400000.0}]}, {"title": "Correct hands", "type": "role", "children": [{"title": "Diane Fisher", "type": "person", "avail": true, "age": 30, "date": 25484400000.0}, {"title": "Heather O. Blake", "type": "person", "avail": true, "age": 51}, {"title": "Adrian M. Miller", "type": "person", "avail": true, "age": 61}, {"title": "James Sutherland", "type": "person", "state": "s", "avail": true, "age": 36, "date": 1162072800000.0}, {"title": "Audrey May", "type": "person", "state": "s", "avail": true, "age": 58, "date": 1072825200000.0}, {"title": "Melanie Edmunds", "type": "person", "avail": true, "age": 69, "date": 699922800000.0}, {"title": "Jasmine Oliver", "type": "person", "state": "h", "avail": true, "age": 23, "date": 1206399600000.0}, {"title": "Kylie Morgan", "type": "person", "state": "s", "avail": true, "age": 97}, {"title": "Dominic L. Taylor", "type": "person", "avail": true, "age": 67, "remarks": "Induces passages gracefully."}, {"title": "Kylie J. White", "type": "person", "state": "s", "avail": true, "age": 65, "date": 330904800000.0}, {"title": "Sue M. Roberts", "type": "person", "avail": true, "age": 77, "date": 1485126000000.0}, {"title": "Jamie Slater", "type": "person", "avail": true, "age": 91}, {"title": "Cameron Turner", "type": "person", "state": "h", "avail": true, "age": 41, "date": 1491256800000.0}, {"title": "Penelope H. Grant", "type": "person", "avail": true, "age": 83, "date": 183164400000.0}, {"title": "Christopher Z. North", "type": "person", "avail": true, "age": 88, "date": 667090800000.0}, {"title": "Emily Grant", "type": "person", "state": "s", "age": 31, "date": 1294786800000.0}, {"title": "Justin Mathis", "type": "person", "avail": true, "age": 47, "date": 1609455600000.0, "remarks": "Crowds exercises entirely."}]}, {"title": "Inherit draws", "type": "role", "expanded": true, "children": [{"title": "Jake R. Walsh", "type": "person", "state": "h", "avail": true, "age": 70, "date": 178844400000.0, "remarks": "Bears functions wholly."}, {"title": "Phil Simpson", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1098568800000.0, "remarks": "Phones efficiencies bravely."}, {"title": "William Q. Ferguson", "type": "person", "avail": true, "age": 52, "date": 898380000000.0}]}, {"title": "Move skies", "type": "role", "children": [{"title": "Rose May", "type": "person", "state": "h", "avail": true, "age": 89, "date": 1491170400000.0}, {"title": "Harry James", "type": "person", "avail": true, "age": 81, "date": 970783200000.0}, {"title": "Edward Z. Taylor", "type": "person", "state": "h", "avail": true, "age": 26}, {"title": "Alex Q. Henderson", "type": "person", "avail": true, "age": 62}, {"title": "Jesse Ross", "type": "person", "state": "s", "avail": true, "age": 38}, {"title": "Kimberly Berry", "type": "person", "avail": true, "age": 93}, {"title": "Harry T. McLean", "type": "person", "avail": true, "age": 59}, {"title": "Blake P. Walsh", "type": "person", "avail": true, "age": 89}, {"title": "Kyle D. Gill", "type": "person", "avail": true, "age": 61, "date": 1132527600000.0}, {"title": "Gordon J. Black", "type": "person", "avail": true, "age": 87}, {"title": "Stephanie Springer", "type": "person", "state": "s", "avail": true, "age": 74, "date": 268959600000.0}, {"title": "Madeleine Y. Brown", "type": "person", "avail": true, "age": 46}, {"title": "Samantha O. Powell", "type": "person", "state": "s", "age": 71}, {"title": "Molly S. Anderson", "type": "person", "state": "h", "avail": true, "age": 60, "date": 315615600000.0}, {"title": "Zoe P. Hemmings", "type": "person", "avail": true, "age": 87, "date": 1437343200000.0, "remarks": "Addresses shirts absolutely."}, {"title": "Victoria J. Cornish", "type": "person", "avail": true, "age": 40}]}, {"title": "Beat meats", "type": "role", "expanded": true, "children": [{"title": "Hannah Y. Gray", "type": "person", "age": 57, "date": 1449529200000.0}, {"title": "Andy Clark", "type": "person", "state": "s", "age": 46, "date": 512434800000.0}, {"title": "Gabrielle B. Hart", "type": "person", "state": "h", "avail": true, "age": 82, "date": 907624800000.0}, {"title": "Sydney Black", "type": "person", "avail": true, "age": 70}, {"title": "Andy Tucker", "type": "person", "state": "h", "avail": true, "age": 53}, {"title": "Tim A. Alsop", "type": "person", "state": "s", "avail": true, "age": 68, "date": 499129200000.0}, {"title": "Carl Nash", "type": "person", "state": "h", "age": 39, "date": 851900400000.0}, {"title": "Megan Y. Burgess", "type": "person", "avail": true, "age": 34}, {"title": "Justin Duncan", "type": "person", "avail": true, "age": 62, "date": 1328914800000.0}, {"title": "Michelle Mackay", "type": "person", "state": "h", "avail": true, "age": 73, "date": 512348400000.0, "remarks": "Spills situations handily."}, {"title": "Heather C. Peake", "type": "person", "state": "s", "avail": true, "age": 91, "date": 801871200000.0}, {"title": "Anthony W. King", "type": "person", "avail": true, "age": 49, "date": 1519340400000.0}, {"title": "Audrey U. Hemmings", "type": "person", "avail": true, "age": 97, "date": 1343599200000.0, "remarks": "Recognizes hyenas rightfully."}, {"title": "Victoria Miller", "type": "person", "state": "s", "avail": true, "age": 73}, {"title": "Frances Smith", "type": "person", "state": "h", "avail": true, "age": 53, "date": 257814000000.0}, {"title": "Sue O. Fisher", "type": "person", "avail": true, "age": 84}, {"title": "Lou N. Paterson", "type": "person", "avail": true, "age": 26, "date": 832111200000.0, "remarks": "Roars frames universally."}]}, {"title": "Live spaces", "type": "role", "children": [{"title": "Sean Hudson", "type": "person", "avail": true, "age": 63, "date": 289177200000.0, "remarks": "Increases coaches infinitely."}, {"title": "Audrey F. Jones", "type": "person", "state": "h", "avail": true, "age": 26, "date": 1278626400000.0, "remarks": "Leans nothings extensively."}, {"title": "Charlie Pullman", "type": "person", "state": "s", "avail": true, "age": 49}, {"title": "Virginia Hardacre", "type": "person", "avail": true, "age": 61, "date": 730854000000.0}, {"title": "Michael Abraham", "type": "person", "avail": true, "age": 88, "date": 980204400000.0}, {"title": "Daryl P. Manning", "type": "person", "avail": true, "age": 45}, {"title": "Thomas E. Brown", "type": "person", "avail": true, "age": 37, "date": 503449200000.0}, {"title": "Natalie Dyer", "type": "person", "state": "s", "avail": true, "age": 59, "date": 1304028000000.0}, {"title": "Matt H. Campbell", "type": "person", "state": "s", "avail": true, "age": 52, "date": 1569276000000.0}, {"title": "Joanne Arnold", "type": "person", "avail": true, "age": 48}, {"title": "Jennifer Q. Howard", "type": "person", "state": "s", "avail": true, "age": 54}]}, {"title": "Blush agreements", "type": "role", "children": [{"title": "Chloe S. Henderson", "type": "person", "age": 77}, {"title": "Jessica Gill", "type": "person", "avail": true, "age": 48, "date": 192927600000.0, "remarks": "Inputs prints kindly."}, {"title": "Gabrielle C. Black", "type": "person", "avail": true, "age": 35, "remarks": "Scabs suspects knowledgeably."}, {"title": "Keith Grant", "type": "person", "avail": true, "age": 55, "date": 1656972000000.0}, {"title": "Heather Vaughan", "type": "person", "avail": true, "age": 84, "date": 4662000000.0}, {"title": "Cameron Randall", "type": "person", "state": "h", "avail": true, "age": 81, "date": 637887600000.0, "remarks": "Flings bunches helpfully."}, {"title": "Sydney N. Quinn", "type": "person", "avail": true, "age": 55, "date": 1582153200000.0}, {"title": "Sophie V. Payne", "type": "person", "avail": true, "age": 23, "date": 72140400000.0}, {"title": "Max McGrath", "type": "person", "avail": true, "age": 54}, {"title": "Diane B. Fraser", "type": "person", "avail": true, "age": 61, "remarks": "Winds claims reasonably."}, {"title": "Karen Buckland", "type": "person", "avail": true, "age": 27}, {"title": "Warren R. Hemmings", "type": "person", "state": "s", "avail": true, "age": 27, "date": 946854000000.0}, {"title": "Justin May", "type": "person", "state": "h", "avail": true, "age": 71, "date": 1528840800000.0}, {"title": "Toby Rutherford", "type": "person", "avail": true, "age": 34, "date": 1418943600000.0}, {"title": "Austin I. McLean", "type": "person", "avail": true, "age": 95, "date": 1553727600000.0, "remarks": "Donates holes significantly."}]}, {"title": "Plot wests", "type": "role", "expanded": true, "children": [{"title": "Bobbie Fisher", "type": "person", "avail": true, "age": 53, "date": 1111359600000.0}, {"title": "Liam Roberts", "type": "person", "avail": true, "age": 22, "date": 1228086000000.0}, {"title": "Taylor Johnston", "type": "person", "avail": true, "age": 88}, {"title": "Irene Russell", "type": "person", "state": "s", "avail": true, "age": 87, "date": 1388098800000.0}, {"title": "Stephanie Arnold", "type": "person", "state": "h", "avail": true, "age": 62, "date": 1135724400000.0}, {"title": "Karen S. Coleman", "type": "person", "avail": true, "age": 36}, {"title": "Brandon MacLeod", "type": "person", "state": "h", "avail": true, "age": 72, "remarks": "States currents whole-heartedly."}, {"title": "Kelly Peake", "type": "person", "avail": true, "age": 59, "date": 1235775600000.0}, {"title": "Rebecca Terry", "type": "person", "avail": true, "age": 23, "date": 117068400000.0}, {"title": "Adrian Allan", "type": "person", "avail": true, "age": 37, "date": 483746400000.0, "remarks": "Hisses hearts zestfully."}, {"title": "Chris Fisher", "type": "person", "state": "s", "avail": true, "age": 74, "date": 169686000000.0}, {"title": "Paul Carr", "type": "person", "state": "h", "avail": true, "age": 66, "date": 451000800000.0}, {"title": "Bobbie James", "type": "person", "avail": true, "age": 22, "date": 155862000000.0}, {"title": "Dorothy Langdon", "type": "person", "avail": true, "age": 40, "date": 426204000000.0}]}, {"title": "Hurt punches", "type": "role", "expanded": true, "children": [{"title": "Faith Campbell", "type": "person", "avail": true, "age": 87, "date": 1650319200000.0, "remarks": "Bets sisters poetically."}, {"title": "Michelle Z. Ellison", "type": "person", "avail": true, "age": 81, "date": 1044399600000.0, "remarks": "Saddles sources righteously."}, {"title": "Dylan G. Dyer", "type": "person", "avail": true, "age": 85}, {"title": "Jessica L. Gibson", "type": "person", "avail": true, "age": 47}]}]}, {"title": "Dept. for Recovers and Recordings", "type": "department", "children": [{"title": "Construe influences", "type": "role", "children": [{"title": "Jamie Y. Gill", "type": "person", "avail": true, "age": 86}, {"title": "Diana Ball", "type": "person", "avail": true, "age": 37, "date": 674172000000.0}, {"title": "Abigail Roberts", "type": "person", "avail": true, "age": 38}, {"title": "Chris K. Fraser", "type": "person", "avail": true, "age": 54, "remarks": "Lifts childhoods restfully."}]}, {"title": "Tread assignments", "type": "role", "expanded": true, "children": [{"title": "Warren Clark", "type": "person", "avail": true, "age": 47, "date": 785631600000.0, "remarks": "Stresses safeties optimistically."}, {"title": "Charlie W. Bailey", "type": "person", "avail": true, "age": 36}, {"title": "Anthony P. Marshall", "type": "person", "avail": true, "age": 53, "date": 1588111200000.0}]}, {"title": "Nip tracks", "type": "role", "expanded": true, "children": [{"title": "Tracey S. Nolan", "type": "person", "state": "h", "avail": true, "age": 96, "date": 527637600000.0}, {"title": "Brandon W. Parr", "type": "person", "state": "h", "avail": true, "age": 38, "date": 1036623600000.0}, {"title": "Diane F. Burgess", "type": "person", "avail": true, "age": 26, "date": 1457132400000.0}, {"title": "Brandon Chapman", "type": "person", "avail": true, "age": 60, "date": 85532400000.0}, {"title": "Deirdre Quinn", "type": "person", "avail": true, "age": 45, "date": 97714800000.0}, {"title": "Bella Glover", "type": "person", "avail": true, "age": 95, "remarks": "Passes sticks considerately."}, {"title": "Harry Rees", "type": "person", "avail": true, "age": 61, "remarks": "Boxes passes nicely."}, {"title": "Hannah K. Wilson", "type": "person", "avail": true, "age": 40, "date": 550533600000.0, "remarks": "Knits openings fully."}, {"title": "Keith X. Newman", "type": "person", "state": "h", "avail": true, "age": 88, "date": 367797600000.0}, {"title": "Rachel McDonald", "type": "person", "avail": true, "age": 32, "date": 1261522800000.0}, {"title": "Toby Henderson", "type": "person", "avail": true, "age": 70, "date": 298249200000.0}, {"title": "Frank Anderson", "type": "person", "avail": true, "age": 70, "remarks": "Dares debates courageously."}, {"title": "Corey E. Wallace", "type": "person", "state": "h", "avail": true, "age": 98, "date": 64882800000.0}, {"title": "Dylan McDonald", "type": "person", "avail": true, "age": 68, "date": 1209852000000.0}, {"title": "Pat E. Glover", "type": "person", "avail": true, "age": 45, "date": 686181600000.0}]}, {"title": "Mean boxes", "type": "role", "expanded": true, "children": [{"title": "Angela Oliver", "type": "person", "avail": true, "age": 26, "date": 48812400000.0}, {"title": "Stephen A. Hart", "type": "person", "avail": true, "age": 35, "remarks": "Integrates shoes bravely."}, {"title": "Evan G. Ross", "type": "person", "avail": true, "age": 49, "date": 679701600000.0, "remarks": "Saddles affects sympathetically."}, {"title": "Faith H. Coleman", "type": "person", "state": "h", "avail": true, "age": 29, "remarks": "Provides tensions richly."}, {"title": "Gabrielle Bailey", "type": "person", "avail": true, "age": 36, "date": 1058824800000.0}, {"title": "Stephanie I. Tucker", "type": "person", "avail": true, "age": 22}]}, {"title": "Dispose carpets", "type": "role", "children": [{"title": "Donna C. Tucker", "type": "person", "avail": true, "age": 84, "remarks": "Praises perceptions calmly."}, {"title": "Jamie R. Ince", "type": "person", "state": "h", "avail": true, "age": 21, "date": 1319061600000.0}, {"title": "Sonia Y. Butler", "type": "person", "state": "s", "avail": true, "age": 55, "date": 1367359200000.0}, {"title": "Simon Parr", "type": "person", "state": "h", "avail": true, "age": 77, "date": 1497823200000.0}, {"title": "Kylie Short", "type": "person", "avail": true, "age": 28, "date": 52354800000.0}]}, {"title": "Endanger weeks", "type": "role", "children": [{"title": "Sophie Q. Wright", "type": "person", "avail": true, "age": 25, "date": 469321200000.0}, {"title": "Vanessa Martin", "type": "person", "avail": true, "age": 78, "date": 771458400000.0}, {"title": "Lillian Roberts", "type": "person", "age": 39, "remarks": "Sniffs processes jubilantly."}, {"title": "Corey B. Knox", "type": "person", "avail": true, "age": 80, "date": 1583190000000.0, "remarks": "Vanishes witnesses promptly."}, {"title": "Kyle Turner", "type": "person", "avail": true, "age": 28, "date": 165193200000.0}]}, {"title": "Waste cups", "type": "role", "expanded": true, "children": [{"title": "David L. Oliver", "type": "person", "avail": true, "age": 39}, {"title": "Owen Z. Fraser", "type": "person", "avail": true, "age": 52, "date": 273193200000.0}, {"title": "Lou Nash", "type": "person", "avail": true, "age": 51, "date": 966895200000.0}, {"title": "Carolyn S. Smith", "type": "person", "avail": true, "age": 82, "date": 53823600000.0}, {"title": "Jennifer Sutherland", "type": "person", "avail": true, "age": 65, "date": 401234400000.0}]}, {"title": "Cry hunts", "type": "role", "children": [{"title": "Steven Davies", "type": "person", "avail": true, "age": 42, "date": 479430000000.0}, {"title": "Jake F. North", "type": "person", "age": 44}, {"title": "Tim Sanderson", "type": "person", "avail": true, "age": 94}, {"title": "Vanessa P. Wallace", "type": "person", "age": 80}, {"title": "Keith Paterson", "type": "person", "state": "h", "avail": true, "age": 56, "date": 1244325600000.0}, {"title": "Christopher Jackson", "type": "person", "avail": true, "age": 33, "remarks": "Appears sheeps fast."}, {"title": "Alison Bond", "type": "person", "state": "h", "avail": true, "age": 94, "date": 1285797600000.0, "remarks": "Offers incomes brilliantly."}, {"title": "Christopher B. Forsyth", "type": "person", "avail": true, "age": 79}, {"title": "Jean Grant", "type": "person", "avail": true, "age": 77, "date": 1360105200000.0, "remarks": "Hinders clubs bravely."}, {"title": "Lillian Forsyth", "type": "person", "avail": true, "age": 69, "remarks": "Varies broads busily."}, {"title": "Chloe Grant", "type": "person", "avail": true, "age": 71, "date": 513986400000.0}, {"title": "Anthony L. Abraham", "type": "person", "avail": true, "age": 86, "date": 800834400000.0}, {"title": "Jasmine Reid", "type": "person", "avail": true, "age": 57, "remarks": "Fits belts quickly."}]}, {"title": "Cross breasts", "type": "role", "children": [{"title": "Kylie Miller", "type": "person", "avail": true, "age": 39, "date": 1620165600000.0}, {"title": "Pat Greene", "type": "person", "age": 94, "date": 1649714400000.0, "remarks": "Stops essaies beautifully."}, {"title": "Daryl Miller", "type": "person", "avail": true, "age": 77}, {"title": "Ryan White", "type": "person", "avail": true, "age": 91, "date": 306284400000.0}, {"title": "Sue Morrison", "type": "person", "avail": true, "age": 59, "date": 982364400000.0}]}, {"title": "Vary strangers", "type": "role", "children": [{"title": "Wanda Z. Vance", "type": "person", "avail": true, "age": 81, "date": 1415574000000.0, "remarks": "Swallows feels eagerly."}, {"title": "Jason Hudson", "type": "person", "avail": true, "age": 97}, {"title": "Sophie Blake", "type": "person", "avail": true, "age": 48, "date": 611013600000.0}, {"title": "Thomas Ross", "type": "person", "avail": true, "age": 82, "date": 944262000000.0}, {"title": "Adrian MacLeod", "type": "person", "avail": true, "age": 42, "remarks": "Blushes disks wholly."}, {"title": "Dylan W. Mathis", "type": "person", "state": "s", "avail": true, "age": 33}, {"title": "Zoe Dickens", "type": "person", "state": "s", "avail": true, "age": 55, "remarks": "Brings targets explicitly."}, {"title": "Andrea Morrison", "type": "person", "avail": true, "age": 63, "date": 634604400000.0}, {"title": "Carolyn Hill", "type": "person", "avail": true, "age": 58, "date": 462837600000.0, "remarks": "Folds equivalents securely."}, {"title": "Oliver E. Lyman", "type": "person", "state": "h", "avail": true, "age": 82, "date": 449877600000.0}, {"title": "Connor S. Paterson", "type": "person", "avail": true, "age": 38, "date": 1417388400000.0}, {"title": "Faith R. Dyer", "type": "person", "avail": true, "age": 77}, {"title": "Pat P. Brown", "type": "person", "avail": true, "age": 83, "remarks": "Sparkles matters markedly."}, {"title": "Tracey U. MacDonald", "type": "person", "state": "s", "avail": true, "age": 64, "date": 477097200000.0}, {"title": "Victoria C. Hart", "type": "person", "age": 58, "remarks": "Attacks prides early."}, {"title": "Julian M. Ince", "type": "person", "avail": true, "age": 84}, {"title": "Jennifer Brown", "type": "person", "state": "s", "avail": true, "age": 57}]}]}, {"title": "Dept. for Fees and Skunks", "type": "department", "children": [{"title": "Subtract connections", "type": "role", "children": [{"title": "Sebastian Simpson", "type": "person", "avail": true, "age": 84, "date": 135126000000.0}, {"title": "Harry K. Butler", "type": "person", "state": "s", "avail": true, "age": 47, "date": 559954800000.0}, {"title": "Julia N. Vaughan", "type": "person", "avail": true, "age": 51, "date": 1517007600000.0}, {"title": "Edward Rampling", "type": "person", "state": "s", "avail": true, "age": 63}, {"title": "Stewart Vance", "type": "person", "state": "h", "age": 36}, {"title": "Jonathan Paige", "type": "person", "avail": true, "age": 50, "date": 268354800000.0, "remarks": "Endorses factors efficiently."}, {"title": "Maria X. Churchill", "type": "person", "state": "h", "age": 97, "date": 1043276400000.0, "remarks": "Strains objects healthily."}, {"title": "Kimberly May", "type": "person", "avail": true, "age": 76, "remarks": "Salutes manners actively."}, {"title": "Sally Parsons", "type": "person", "avail": true, "age": 90}, {"title": "Amanda Glover", "type": "person", "state": "s", "avail": true, "age": 89, "date": 347497200000.0}, {"title": "Oliver Ellison", "type": "person", "avail": true, "age": 49, "date": 1419548400000.0}, {"title": "Wendy L. Ball", "type": "person", "state": "h", "avail": true, "age": 75, "remarks": "Refers debates properly."}, {"title": "Frank H. Terry", "type": "person", "age": 35}, {"title": "Irene B. May", "type": "person", "state": "h", "avail": true, "age": 60, "date": 1458687600000.0}]}, {"title": "Cower particulars", "type": "role", "expanded": true, "children": [{"title": "Ella W. Chapman", "type": "person", "avail": true, "age": 74, "date": 86742000000.0, "remarks": "Adjusts presences consistently."}, {"title": "Charles J. Bailey", "type": "person", "avail": true, "age": 53}, {"title": "Adam Graham", "type": "person", "state": "s", "avail": true, "age": 57, "date": 1119477600000.0}, {"title": "Pat Hodges", "type": "person", "state": "s", "avail": true, "age": 73, "date": 1260226800000.0}, {"title": "Corey Springer", "type": "person", "avail": true, "age": 53}, {"title": "Piers D. Powell", "type": "person", "avail": true, "age": 86, "date": 797983200000.0, "remarks": "Imprints experts rightly."}, {"title": "Rachel P. Graham", "type": "person", "avail": true, "age": 34, "date": 280537200000.0}, {"title": "Jayden L. McGrath", "type": "person", "avail": true, "age": 48, "date": 292374000000.0}, {"title": "Casey P. Greene", "type": "person", "avail": true, "age": 28}, {"title": "Chris M. Parsons", "type": "person", "age": 50}, {"title": "Chris Churchill", "type": "person", "state": "s", "avail": true, "age": 28}, {"title": "Piers Ogden", "type": "person", "avail": true, "age": 27}]}, {"title": "Hew computers", "type": "role", "children": [{"title": "Michael V. Turner", "type": "person", "state": "s", "avail": true, "age": 86, "date": 1386802800000.0}, {"title": "Alexander I. Howard", "type": "person", "state": "h", "avail": true, "age": 82, "date": 742773600000.0, "remarks": "Feels ferrets studiously."}, {"title": "Madeleine F. Nash", "type": "person", "state": "s", "avail": true, "age": 35, "remarks": "Ignites considerations cleanly."}, {"title": "Amanda Tucker", "type": "person", "avail": true, "age": 88, "date": 1316988000000.0}, {"title": "Jane King", "type": "person", "state": "h", "avail": true, "age": 51, "date": 1304028000000.0}, {"title": "Andy Miller", "type": "person", "state": "s", "avail": true, "age": 28, "date": 1122501600000.0}, {"title": "Lily Simpson", "type": "person", "state": "h", "avail": true, "age": 88, "date": 109119600000.0, "remarks": "Proceeds industries wonderfully."}, {"title": "Tracey Y. Hodges", "type": "person", "avail": true, "age": 23, "date": 950396400000.0}, {"title": "Simon B. Wilson", "type": "person", "avail": true, "age": 89, "date": 500425200000.0}, {"title": "Daryl Pullman", "type": "person", "state": "s", "avail": true, "age": 61, "date": 580428000000.0, "remarks": "Fights smokes splendidly."}]}, {"title": "Lay vacations", "type": "role", "children": [{"title": "Amelia Burgess", "type": "person", "state": "s", "avail": true, "age": 25, "date": 493509600000.0, "remarks": "Wrings packages luckily."}, {"title": "Yvonne G. Mackenzie", "type": "person", "avail": true, "age": 31, "date": 1575846000000.0}, {"title": "Benjamin H. Fraser", "type": "person", "avail": true, "age": 47, "date": 936914400000.0, "remarks": "Triumphs difficulties really."}]}, {"title": "Kiss marriages", "type": "role", "children": [{"title": "Sonia Z. Johnston", "type": "person", "state": "h", "age": 62}, {"title": "Lou Q. Carr", "type": "person", "avail": true, "age": 95}, {"title": "Anne W. Wright", "type": "person", "avail": true, "age": 73, "date": 985816800000.0, "remarks": "Neglects employees assuredly."}, {"title": "Anne Lambert", "type": "person", "avail": true, "age": 48}, {"title": "Nathan Q. Murray", "type": "person", "state": "s", "avail": true, "age": 24, "date": 349225200000.0, "remarks": "Speeds youngs unquestionably."}, {"title": "Peter Coleman", "type": "person", "avail": true, "age": 32}, {"title": "Jayden F. Morgan", "type": "person", "age": 45, "date": 1007679600000.0, "remarks": "Selects dots abundantly."}, {"title": "Audrey B. Gibson", "type": "person", "avail": true, "age": 44}, {"title": "Justin Q. Wilkins", "type": "person", "avail": true, "age": 27, "date": 1263423600000.0, "remarks": "Constrains pugs intensely."}, {"title": "Oliver Nash", "type": "person", "avail": true, "age": 40}, {"title": "Joanne J. Reid", "type": "person", "avail": true, "age": 51, "remarks": "Spells alternatives proudly."}]}, {"title": "Owe televisions", "type": "role", "expanded": true, "children": [{"title": "Connor K. Allan", "type": "person", "avail": true, "age": 36, "date": 1482534000000.0}, {"title": "Frances R. Fraser", "type": "person", "avail": true, "age": 51, "date": 1218924000000.0}, {"title": "Julia Rutherford", "type": "person", "avail": true, "age": 60, "date": 338767200000.0, "remarks": "Encourages conversations valiantly."}, {"title": "Trevor Black", "type": "person", "avail": true, "age": 62, "date": 118450800000.0}, {"title": "Kimberly L. Nolan", "type": "person", "state": "h", "avail": true, "age": 88, "date": 1198623600000.0, "remarks": "Rids decisions intelligently."}, {"title": "Edward Y. Campbell", "type": "person", "avail": true, "age": 88, "date": 389829600000.0}, {"title": "Kyle X. Springer", "type": "person", "state": "s", "avail": true, "age": 71, "date": 1061244000000.0}, {"title": "Lou Dyer", "type": "person", "avail": true, "age": 93, "date": 203036400000.0}, {"title": "Frank H. Gill", "type": "person", "avail": true, "age": 92, "date": 1395442800000.0}, {"title": "Jasmine I. Mitchell", "type": "person", "avail": true, "age": 55, "date": 1638486000000.0}, {"title": "Casey O. Langdon", "type": "person", "state": "s", "avail": true, "age": 45, "remarks": "Bows priors agreeably."}, {"title": "Simon Coleman", "type": "person", "avail": true, "age": 97, "date": 302655600000.0, "remarks": "Tramples taps beautifully."}, {"title": "Brian K. Glover", "type": "person", "avail": true, "age": 44, "remarks": "Redoes secretaries upwardly."}, {"title": "Fiona Y. Jackson", "type": "person", "avail": true, "age": 49, "date": 1627768800000.0, "remarks": "Fights bigs heartily."}]}, {"title": "Last campaigns", "type": "role", "children": [{"title": "Daryl B. Dowd", "type": "person", "avail": true, "age": 60, "date": 944607600000.0}, {"title": "Kelly B. Lyman", "type": "person", "avail": true, "age": 65, "remarks": "Tends tricks majestically."}, {"title": "Keith H. Stewart", "type": "person", "avail": true, "age": 71, "date": 547423200000.0}, {"title": "Natalie W. Short", "type": "person", "avail": true, "age": 66, "date": 875138400000.0, "remarks": "Burns sillies infinitely."}, {"title": "Corey Wilkins", "type": "person", "avail": true, "age": 88}, {"title": "Oliver Allan", "type": "person", "state": "h", "age": 33}]}, {"title": "Undo positives", "type": "role", "children": [{"title": "Toby Quinn", "type": "person", "avail": true, "age": 36, "date": 1147989600000.0}]}, {"title": "Sight articles", "type": "role", "children": [{"title": "Adam E. Scott", "type": "person", "avail": true, "age": 31, "date": 1093989600000.0, "remarks": "Curbs alpacas agreeably."}, {"title": "Jesse Y. Blake", "type": "person", "age": 22}, {"title": "Amelia Bond", "type": "person", "avail": true, "age": 83, "date": 863992800000.0, "remarks": "Consoles disciplines dreamily."}, {"title": "Diane Dyer", "type": "person", "avail": true, "age": 24}, {"title": "William MacDonald", "type": "person", "avail": true, "age": 33, "date": 1458342000000.0}, {"title": "Alan J. Wright", "type": "person", "state": "h", "avail": true, "age": 78}, {"title": "Max Z. Duncan", "type": "person", "avail": true, "age": 37, "date": 1180821600000.0}, {"title": "Warren Walker", "type": "person", "state": "s", "avail": true, "age": 27, "remarks": "Incises walruses diligently."}, {"title": "Sam Kelly", "type": "person", "age": 84}, {"title": "Alan Scott", "type": "person", "state": "h", "avail": true, "age": 32}, {"title": "Olivia Vance", "type": "person", "age": 61}]}]}, {"title": "Dept. for Uses and Wars", "type": "department", "children": [{"title": "Dedicate issues", "type": "role", "children": [{"title": "Maria Piper", "type": "person", "age": 44, "date": 435711600000.0}, {"title": "Anne Gill", "type": "person", "avail": true, "age": 70}, {"title": "Phil L. Gibson", "type": "person", "avail": true, "age": 50, "date": 1072652400000.0}, {"title": "Corey Wilson", "type": "person", "avail": true, "age": 49, "date": 752281200000.0}, {"title": "Richard Berry", "type": "person", "age": 78, "date": 83890800000.0}]}, {"title": "Transform departures", "type": "role", "children": [{"title": "Casey M. North", "type": "person", "avail": true, "age": 36, "date": 909788400000.0}, {"title": "Lisa R. Gibson", "type": "person", "avail": true, "age": 59, "remarks": "Sacks dramas merrily."}, {"title": "Mary K. Newman", "type": "person", "age": 95, "date": 2070000000.0, "remarks": "Swallows fruits boldly."}, {"title": "Sarah Wilson", "type": "person", "avail": true, "age": 88, "remarks": "Inflates numbers proudly."}, {"title": "Jamie Lewis", "type": "person", "avail": true, "age": 35, "date": 164070000000.0, "remarks": "Becomes bunches restfully."}, {"title": "Adrian Hughes", "type": "person", "avail": true, "age": 74, "date": 1351461600000.0}, {"title": "Jean A. Clarkson", "type": "person", "avail": true, "age": 47, "date": 1125352800000.0}, {"title": "Diana Campbell", "type": "person", "state": "h", "avail": true, "age": 27, "date": 852850800000.0}, {"title": "Claire E. Mackenzie", "type": "person", "avail": true, "age": 60, "date": 1588456800000.0}, {"title": "Donna Black", "type": "person", "avail": true, "age": 97, "date": 198716400000.0}, {"title": "Gabrielle K. North", "type": "person", "avail": true, "age": 95, "date": 1013468400000.0}, {"title": "Katherine K. Henderson", "type": "person", "state": "s", "avail": true, "age": 55}, {"title": "Joan M. Poole", "type": "person", "state": "s", "avail": true, "age": 53, "date": 1251756000000.0}, {"title": "Karen Rees", "type": "person", "avail": true, "age": 41, "date": 1074898800000.0}, {"title": "Taylor Slater", "type": "person", "avail": true, "age": 72}, {"title": "Jack Vaughan", "type": "person", "avail": true, "age": 51, "date": 1050357600000.0, "remarks": "Sates mouths lovingly."}]}, {"title": "Carve caribous", "type": "role", "expanded": true, "children": [{"title": "Lauren Y. Newman", "type": "person", "avail": true, "age": 60, "date": 227487600000.0}, {"title": "Jean E. Stewart", "type": "person", "avail": true, "age": 22}, {"title": "John F. McDonald", "type": "person", "age": 65, "date": 1129932000000.0}, {"title": "Amy R. Burgess", "type": "person", "avail": true, "age": 89}, {"title": "Lou Q. Scott", "type": "person", "avail": true, "age": 25, "date": 1628892000000.0}, {"title": "Diana E. Hamilton", "type": "person", "state": "h", "avail": true, "age": 89, "date": 527205600000.0}, {"title": "Isaac O. Young", "type": "person", "state": "s", "avail": true, "age": 96, "date": 925423200000.0, "remarks": "Steps hares fervently."}]}, {"title": "Contrast flights", "type": "role", "expanded": true, "children": [{"title": "Vanessa V. Paterson", "type": "person", "avail": true, "age": 92}, {"title": "Piers Y. Harris", "type": "person", "avail": true, "age": 72, "date": 1087336800000.0}, {"title": "Rose Q. Tucker", "type": "person", "state": "h", "avail": true, "age": 57, "date": 1389308400000.0}, {"title": "Rebecca North", "type": "person", "avail": true, "age": 81, "date": 1180908000000.0, "remarks": "Buys switches promptly."}, {"title": "Emily Edmunds", "type": "person", "age": 75, "date": 220489200000.0}, {"title": "Christopher R. Peters", "type": "person", "state": "h", "avail": true, "age": 42}, {"title": "Ryan C. Clark", "type": "person", "avail": true, "age": 40, "date": 1623880800000.0}, {"title": "Emma Wilson", "type": "person", "avail": true, "age": 45, "date": 63154800000.0, "remarks": "Says singles very."}, {"title": "Amanda Tucker", "type": "person", "state": "s", "avail": true, "age": 52, "date": 443574000000.0}, {"title": "Connor Mackay", "type": "person", "state": "h", "avail": true, "age": 36, "date": 350607600000.0}, {"title": "Sue T. Cameron", "type": "person", "state": "h", "avail": true, "age": 89, "date": 1090274400000.0}, {"title": "Kelly Vaughan", "type": "person", "state": "s", "avail": true, "age": 82, "date": 10796400000.0}, {"title": "Angela Oliver", "type": "person", "age": 83, "remarks": "Disuses meets amazingly."}, {"title": "Carl Wallace", "type": "person", "state": "h", "age": 56, "remarks": "Sprays monitors exceptionally."}, {"title": "Gabrielle T. Buckland", "type": "person", "avail": true, "age": 27, "date": 870645600000.0}, {"title": "Michelle O. Smith", "type": "person", "state": "h", "avail": true, "age": 56, "date": 250383600000.0}, {"title": "Kimberly I. Powell", "type": "person", "state": "s", "age": 66, "date": 1117749600000.0, "remarks": "Pleads eggs boldly."}, {"title": "Sarah Rampling", "type": "person", "avail": true, "age": 45}, {"title": "Phil Tucker", "type": "person", "avail": true, "age": 53}]}, {"title": "Astonish presentations", "type": "role", "expanded": true, "children": [{"title": "Irene M. Walker", "type": "person", "avail": true, "age": 39}, {"title": "Mary McDonald", "type": "person", "age": 93, "date": 698108400000.0, "remarks": "Introduces taxes optimistically."}, {"title": "Kevin G. Hemmings", "type": "person", "avail": true, "age": 38, "date": 793407600000.0}, {"title": "Neil Z. Welch", "type": "person", "state": "s", "avail": true, "age": 70, "date": 1394924400000.0, "remarks": "Inquires celebrations joyously."}, {"title": "Harry K. Newman", "type": "person", "state": "s", "avail": true, "age": 42, "remarks": "Copes burns sympathetically."}, {"title": "Megan Slater", "type": "person", "avail": true, "age": 43, "date": 356652000000.0}, {"title": "Karen D. Vance", "type": "person", "avail": true, "age": 26, "date": 1414360800000.0}, {"title": "Adam E. Springer", "type": "person", "avail": true, "age": 98, "date": 910306800000.0}, {"title": "Cameron Jones", "type": "person", "avail": true, "age": 71, "date": 129855600000.0}, {"title": "Phoenix B. Ellison", "type": "person", "avail": true, "age": 36}, {"title": "Ryan Morgan", "type": "person", "avail": true, "age": 21, "date": 324597600000.0}, {"title": "Anthony Buckland", "type": "person", "state": "h", "avail": true, "age": 58, "remarks": "Checks finishes tenderly."}, {"title": "Jesse White", "type": "person", "avail": true, "age": 38}, {"title": "Mary Gibson", "type": "person", "state": "h", "avail": true, "age": 55, "date": 64710000000.0, "remarks": "Conquers families gladly."}, {"title": "Eddie Hamilton", "type": "person", "age": 23, "date": 791420400000.0}]}, {"title": "Carve calendars", "type": "role", "children": [{"title": "Charlie McGrath", "type": "person", "state": "s", "avail": true, "age": 92, "date": 912985200000.0}, {"title": "Amelia H. Mitchell", "type": "person", "avail": true, "age": 71, "remarks": "Glances holidaies deftly."}, {"title": "Nicholas Edmunds", "type": "person", "state": "h", "age": 94, "date": 1538172000000.0}, {"title": "Jack F. Paterson", "type": "person", "avail": true, "age": 84}, {"title": "Anthony Henderson", "type": "person", "avail": true, "age": 50, "date": 1159567200000.0}, {"title": "Alexandra H. Rampling", "type": "person", "state": "h", "avail": true, "age": 70, "date": 584920800000.0, "remarks": "Wastes pages tenderly."}]}, {"title": "Utter falls", "type": "role", "expanded": true, "children": [{"title": "Brian Robertson", "type": "person", "avail": true, "age": 68, "remarks": "Beautifies reserves unfailingly."}, {"title": "Ava P. Dyer", "type": "person", "state": "s", "avail": true, "age": 81, "date": 545522400000.0}, {"title": "Brian D. Lewis", "type": "person", "avail": true, "age": 97, "date": 1097013600000.0, "remarks": "Folds criticisms knowingly."}, {"title": "Andrea Edmunds", "type": "person", "avail": true, "age": 98, "date": 997999200000.0}, {"title": "Toby Carr", "type": "person", "age": 59, "date": 728780400000.0}, {"title": "Jean Payne", "type": "person", "state": "h", "avail": true, "age": 87}, {"title": "Jason F. Lawrence", "type": "person", "state": "s", "avail": true, "age": 91, "date": 228351600000.0}, {"title": "Amy E. Harris", "type": "person", "avail": true, "age": 50, "date": 323046000000.0, "remarks": "Lays stands affirmatively."}, {"title": "Phoenix A. Paige", "type": "person", "avail": true, "age": 26, "date": 1506981600000.0, "remarks": "Breaks cakes usefully."}, {"title": "Alexandra Greene", "type": "person", "avail": true, "age": 90}, {"title": "Pippa Randall", "type": "person", "avail": true, "age": 60}, {"title": "Leah Cameron", "type": "person", "avail": true, "age": 32}, {"title": "Elizabeth Coleman", "type": "person", "state": "s", "avail": true, "age": 87, "date": 271119600000.0, "remarks": "Adds missions efficiently."}]}, {"title": "Reflect structures", "type": "role", "children": [{"title": "Steven J. Turner", "type": "person", "avail": true, "age": 85}, {"title": "Chris Miller", "type": "person", "state": "s", "avail": true, "age": 76, "remarks": "Approves horses charmingly."}, {"title": "Pat T. Arnold", "type": "person", "avail": true, "age": 61, "date": 775692000000.0}, {"title": "Cameron R. Roberts", "type": "person", "avail": true, "age": 47, "date": 668646000000.0, "remarks": "Inputs cods merrily."}, {"title": "Ella Stewart", "type": "person", "avail": true, "age": 73, "remarks": "Inhabits truths sympathetically."}]}, {"title": "Innovate poets", "type": "role", "expanded": true, "children": [{"title": "Jamie Quinn", "type": "person", "avail": true, "age": 96}, {"title": "Dominic Mitchell", "type": "person", "state": "s", "avail": true, "age": 64}, {"title": "Olivia A. Bell", "type": "person", "state": "s", "avail": true, "age": 44}, {"title": "Justin J. Coleman", "type": "person", "avail": true, "age": 96, "date": 1234047600000.0, "remarks": "Refuses calendars gently."}, {"title": "Jason Gray", "type": "person", "state": "s", "avail": true, "age": 47, "date": 370303200000.0, "remarks": "Heaves boats victoriously."}, {"title": "Gabrielle D. Henderson", "type": "person", "avail": true, "age": 74}, {"title": "Alan Henderson", "type": "person", "avail": true, "age": 73}, {"title": "Warren D. Slater", "type": "person", "avail": true, "age": 84, "date": 874015200000.0}, {"title": "Diane Tucker", "type": "person", "avail": true, "age": 53, "date": 948668400000.0, "remarks": "Illustrates bridges unquestionably."}, {"title": "Deirdre F. Kerr", "type": "person", "avail": true, "age": 63, "date": 299977200000.0}, {"title": "Jessica Nash", "type": "person", "state": "s", "avail": true, "age": 21, "remarks": "Matches mortgages deeply."}, {"title": "Joe Bailey", "type": "person", "age": 33}, {"title": "Jessica Forsyth", "type": "person", "avail": true, "age": 61, "date": 1239660000000.0, "remarks": "Greets substances reassuringly."}, {"title": "Leah V. Quinn", "type": "person", "avail": true, "age": 46, "date": 342140400000.0}, {"title": "Lillian Taylor", "type": "person", "state": "h", "age": 88}, {"title": "Sarah Sutherland", "type": "person", "avail": true, "age": 33, "remarks": "Advises successes stylishly."}, {"title": "Frances Quinn", "type": "person", "state": "s", "avail": true, "age": 95, "date": 358207200000.0, "remarks": "Idealizes daughters jubilantly."}, {"title": "Joe Q. Peake", "type": "person", "avail": true, "age": 77, "date": 167094000000.0, "remarks": "Shines wolfs properly."}, {"title": "Sam O. Graham", "type": "person", "avail": true, "age": 95, "remarks": "Stretches philosophies abundantly."}]}, {"title": "Arrest codes", "type": "role", "children": [{"title": "Warren B. Dyer", "type": "person", "state": "s", "age": 95, "date": 1481756400000.0, "remarks": "Chooses tables properly."}, {"title": "Joan N. Rutherford", "type": "person", "state": "s", "avail": true, "age": 50, "date": 1351116000000.0, "remarks": "Boards plants interestingly."}, {"title": "Neil E. Ross", "type": "person", "avail": true, "age": 53, "remarks": "Replies freedoms properly."}, {"title": "Paul Wallace", "type": "person", "state": "s", "avail": true, "age": 92, "date": 1068418800000.0, "remarks": "Sees tries clearly."}, {"title": "Jasmine A. Rees", "type": "person", "state": "h", "avail": true, "age": 75, "date": 1288738800000.0, "remarks": "Expresses bones worthily."}, {"title": "Tracey Bailey", "type": "person", "state": "s", "avail": true, "age": 33, "remarks": "Magnifies taxes enjoyably."}, {"title": "Faith Stewart", "type": "person", "avail": true, "age": 49}, {"title": "Keith North", "type": "person", "avail": true, "age": 72, "date": 1036105200000.0}, {"title": "Adrian M. Dyer", "type": "person", "avail": true, "age": 94}, {"title": "Austin Buckland", "type": "person", "avail": true, "age": 54, "remarks": "Praises turtles jauntily."}, {"title": "Sean L. Jackson", "type": "person", "age": 41, "date": 1424214000000.0}, {"title": "Angela Gray", "type": "person", "avail": true, "age": 94, "date": 1444082400000.0}, {"title": "Natalie N. Taylor", "type": "person", "avail": true, "age": 63, "date": 1436652000000.0}, {"title": "Corey K. Smith", "type": "person", "avail": true, "age": 31}, {"title": "Ryan W. Kelly", "type": "person", "avail": true, "age": 21, "date": 496274400000.0}, {"title": "Sydney Langdon", "type": "person", "avail": true, "age": 86, "remarks": "Seizes grandfathers usefully."}]}]}, {"title": "Dept. for Greens and Angers", "type": "department", "children": [{"title": "Practise properties", "type": "role", "expanded": true}, {"title": "Impede beaches", "type": "role", "children": [{"title": "Dorothy Skinner", "type": "person", "avail": true, "age": 57}, {"title": "Cameron Robertson", "type": "person", "state": "s", "avail": true, "age": 34, "remarks": "Touches questions enthusiastically."}, {"title": "Christian F. Berry", "type": "person", "avail": true, "age": 44, "date": 1146348000000.0, "remarks": "Drills crows infinitely."}, {"title": "James Lambert", "type": "person", "avail": true, "age": 64, "date": 1065132000000.0, "remarks": "Does hurries positively."}, {"title": "Richard Wilkins", "type": "person", "state": "h", "avail": true, "age": 89}]}, {"title": "Kill options", "type": "role", "children": [{"title": "Daryl D. Lyman", "type": "person", "age": 73, "date": 683589600000.0}, {"title": "Sean Taylor", "type": "person", "state": "s", "avail": true, "age": 55, "remarks": "Notices offers diligently."}, {"title": "Lisa Jones", "type": "person", "avail": true, "age": 72, "date": 1539208800000.0}, {"title": "Christopher Walker", "type": "person", "avail": true, "age": 32, "date": 806623200000.0}, {"title": "Jack Hughes", "type": "person", "state": "s", "age": 76}, {"title": "Lauren M. Blake", "type": "person", "state": "h", "avail": true, "age": 58, "date": 1297897200000.0, "remarks": "Founds greens genuinely."}, {"title": "Rebecca Mathis", "type": "person", "avail": true, "age": 34, "remarks": "Breaks classrooms cutely."}, {"title": "Alison E. Duncan", "type": "person", "avail": true, "age": 76, "date": 1028671200000.0}, {"title": "Jean J. Miller", "type": "person", "state": "h", "avail": true, "age": 84, "date": 327016800000.0}, {"title": "Gavin V. Simpson", "type": "person", "avail": true, "age": 81, "date": 471135600000.0}, {"title": "Neil Mathis", "type": "person", "avail": true, "age": 91, "date": 543538800000.0, "remarks": "Belongs familiars substantially."}, {"title": "Gordon Edmunds", "type": "person", "avail": true, "age": 31}, {"title": "Emily J. Vaughan", "type": "person", "avail": true, "age": 91}, {"title": "Steven Young", "type": "person", "state": "s", "avail": true, "age": 93, "date": 1589839200000.0}, {"title": "Evan A. Turner", "type": "person", "age": 31, "remarks": "Prefers tooths unfailingly."}, {"title": "Kelly K. Robertson", "type": "person", "state": "s", "avail": true, "age": 38, "date": 1032386400000.0}, {"title": "Nicola H. Buckland", "type": "person", "avail": true, "age": 61, "date": 604191600000.0, "remarks": "Places equipments lightly."}]}, {"title": "Integrate rents", "type": "role", "expanded": true, "children": [{"title": "Phoenix X. Avery", "type": "person", "avail": true, "age": 92}, {"title": "Sophie A. Parr", "type": "person", "state": "s", "avail": true, "age": 79, "date": 782780400000.0}, {"title": "Kevin N. Marshall", "type": "person", "avail": true, "age": 67, "date": 777765600000.0, "remarks": "Supposes parts restfully."}, {"title": "Matt R. Payne", "type": "person", "avail": true, "age": 32, "date": 398556000000.0}, {"title": "Sydney P. McDonald", "type": "person", "avail": true, "age": 63, "date": 1193871600000.0}, {"title": "Nicholas G. Mathis", "type": "person", "avail": true, "age": 76, "date": 1291417200000.0, "remarks": "Distributes squirrels fashionably."}, {"title": "Victoria X. King", "type": "person", "avail": true, "age": 94}, {"title": "Pat S. Kerr", "type": "person", "avail": true, "age": 61, "date": 39913200000.0}, {"title": "Wanda Z. Sanderson", "type": "person", "state": "s", "avail": true, "age": 24, "date": 370389600000.0}, {"title": "Steven U. Miller", "type": "person", "state": "h", "avail": true, "age": 25, "date": 739231200000.0}, {"title": "Frances Morrison", "type": "person", "state": "h", "avail": true, "age": 21}, {"title": "Leonard S. Watson", "type": "person", "avail": true, "age": 85, "date": 1144360800000.0}]}, {"title": "Maintain clotheses", "type": "role", "expanded": true, "children": [{"title": "Anna W. Wilkins", "type": "person", "avail": true, "age": 29, "date": 634863600000.0, "remarks": "Mashes extents upwardly."}, {"title": "Wanda H. Hart", "type": "person", "state": "s", "avail": true, "age": 70}, {"title": "Carolyn P. Parr", "type": "person", "avail": true, "age": 27, "remarks": "Scales bits blissfully."}, {"title": "Amanda C. Morgan", "type": "person", "avail": true, "age": 80, "date": 1136847600000.0}, {"title": "Lauren P. Grant", "type": "person", "avail": true, "age": 58, "date": 732409200000.0}, {"title": "Nicola B. Berry", "type": "person", "avail": true, "age": 95}, {"title": "Rachel U. James", "type": "person", "avail": true, "age": 83, "date": 755218800000.0}, {"title": "Charlie Nolan", "type": "person", "avail": true, "age": 27}, {"title": "Isaac B. Simpson", "type": "person", "avail": true, "age": 66}]}, {"title": "Sterilize rides", "type": "role", "expanded": true, "children": [{"title": "Harry MacLeod", "type": "person", "avail": true, "age": 48, "date": 298422000000.0}, {"title": "Lucas D. Chapman", "type": "person", "age": 94}, {"title": "Alison King", "type": "person", "state": "s", "avail": true, "age": 40, "remarks": "Impeaches noses lively."}, {"title": "Daryl Nolan", "type": "person", "age": 86, "date": 215046000000.0, "remarks": "Ties notices richly."}, {"title": "Kyle A. Miller", "type": "person", "avail": true, "age": 89}, {"title": "Joshua Greene", "type": "person", "state": "s", "avail": true, "age": 39, "date": 618444000000.0}, {"title": "Colin X. Clark", "type": "person", "avail": true, "age": 60, "date": 1417215600000.0}, {"title": "Luke Vance", "type": "person", "state": "s", "avail": true, "age": 52, "date": 115772400000.0, "remarks": "Varies locks exceptionally."}, {"title": "Blake Paige", "type": "person", "avail": true, "age": 74, "date": 1455663600000.0, "remarks": "Stitches asks poignantly."}, {"title": "Zoe Martin", "type": "person", "avail": true, "age": 95, "remarks": "Writes administrations joyously."}, {"title": "Wendy Burgess", "type": "person", "avail": true, "age": 50, "remarks": "Speaks implements forever."}, {"title": "Lou Hill", "type": "person", "avail": true, "age": 81, "date": 390520800000.0, "remarks": "Surges personalities zestfully."}, {"title": "Steven H. Sanderson", "type": "person", "avail": true, "age": 68, "date": 441414000000.0}, {"title": "Anna Z. Murray", "type": "person", "avail": true, "age": 80, "date": 1216159200000.0, "remarks": "Cribs hands tenderly."}, {"title": "Nicholas F. Ross", "type": "person", "avail": true, "age": 64, "date": 643845600000.0, "remarks": "Favours babies exclusively."}, {"title": "Lisa Mills", "type": "person", "avail": true, "age": 92, "remarks": "Treasures braves infinitely."}, {"title": "Harry I. Randall", "type": "person", "avail": true, "age": 54, "date": 412988400000.0, "remarks": "Convicts discounts carefully."}]}, {"title": "Grade tourists", "type": "role", "expanded": true, "children": [{"title": "Joe Glover", "type": "person", "avail": true, "age": 37}, {"title": "Toby V. Peake", "type": "person", "avail": true, "age": 60, "date": 573778800000.0, "remarks": "Prosecutes damages eagerly."}, {"title": "Thomas L. Turner", "type": "person", "avail": true, "age": 60, "date": 1224540000000.0, "remarks": "Spoils texts believably."}, {"title": "Heather T. Dowd", "type": "person", "avail": true, "age": 80, "date": 842738400000.0}, {"title": "Alexandra McGrath", "type": "person", "avail": true, "age": 86}, {"title": "David S. Lawrence", "type": "person", "avail": true, "age": 64, "remarks": "Indicates sessions lightly."}, {"title": "Joe Lyman", "type": "person", "state": "s", "avail": true, "age": 53, "date": 122770800000.0, "remarks": "Impends laughs luxuriously."}, {"title": "Victoria Carr", "type": "person", "age": 28, "date": 180226800000.0}, {"title": "Joe U. Davidson", "type": "person", "avail": true, "age": 68, "date": 655167600000.0, "remarks": "Supplies kills unselfishly."}, {"title": "Toby W. Dyer", "type": "person", "avail": true, "age": 26}, {"title": "Piers Graham", "type": "person", "avail": true, "age": 44}]}]}, {"title": "Dept. for Singles and Anybodies", "type": "department", "children": [{"title": "Resist trainings", "type": "role", "children": [{"title": "Corey Wilkins", "type": "person", "avail": true, "age": 21, "remarks": "Subscribes rocks gladly."}, {"title": "Rose U. Wilkins", "type": "person", "state": "h", "avail": true, "age": 51, "date": 1379368800000.0}, {"title": "Lisa Metcalfe", "type": "person", "state": "s", "avail": true, "age": 98, "date": 1232146800000.0}, {"title": "Jacob Newman", "type": "person", "avail": true, "age": 77, "remarks": "Decorates swims graciously."}, {"title": "Owen O. Dowd", "type": "person", "avail": true, "age": 41, "remarks": "Varies meals forever."}, {"title": "Alex K. Welch", "type": "person", "age": 72, "date": 620863200000.0}, {"title": "Frances McDonald", "type": "person", "age": 36, "date": 211417200000.0}]}, {"title": "Owe safes", "type": "role", "children": [{"title": "Sarah Hodges", "type": "person", "avail": true, "age": 39, "date": 182732400000.0, "remarks": "Enters shes spectacularly."}, {"title": "Sydney Manning", "type": "person", "state": "h", "avail": true, "age": 36, "date": 1448146800000.0, "remarks": "Sights grasshoppers cleanly."}, {"title": "Rose Y. Lawrence", "type": "person", "avail": true, "age": 94, "date": 1384902000000.0}, {"title": "Nathan Robertson", "type": "person", "age": 79, "remarks": "Courses credits reponsibly."}, {"title": "Alexandra I. Clarkson", "type": "person", "age": 35, "date": 841615200000.0}, {"title": "Liam S. Edmunds", "type": "person", "avail": true, "age": 60, "date": 28422000000.0, "remarks": "Samples indications specially."}, {"title": "Emma D. Blake", "type": "person", "avail": true, "age": 96, "date": 653781600000.0}, {"title": "Rachel H. Terry", "type": "person", "state": "h", "avail": true, "age": 35}, {"title": "Katherine Bower", "type": "person", "avail": true, "age": 29, "date": 515628000000.0, "remarks": "Decorates satisfactions intelligently."}, {"title": "Adam F. Hardacre", "type": "person", "state": "h", "avail": true, "age": 69, "date": 773618400000.0, "remarks": "Pretends frogs admiringly."}, {"title": "Toby Brown", "type": "person", "avail": true, "age": 95, "date": 1215295200000.0, "remarks": "Cycles creams appreciably."}, {"title": "Tim Martin", "type": "person", "avail": true, "age": 38}]}, {"title": "Pollute mules", "type": "role", "children": [{"title": "Stephen W. King", "type": "person", "avail": true, "age": 53}, {"title": "Stewart O. Turner", "type": "person", "state": "h", "avail": true, "age": 71, "date": 1254866400000.0}, {"title": "Rose Fisher", "type": "person", "avail": true, "age": 45, "date": 1065650400000.0}, {"title": "Molly S. Paterson", "type": "person", "avail": true, "age": 50, "date": 1010271600000.0}, {"title": "Daryl V. Black", "type": "person", "avail": true, "age": 87, "date": 163033200000.0}, {"title": "Felicity P. Coleman", "type": "person", "avail": true, "age": 42}, {"title": "James Taylor", "type": "person", "avail": true, "age": 80, "date": 23583600000.0}, {"title": "Phoenix A. Rees", "type": "person", "state": "h", "avail": true, "age": 70, "date": 673135200000.0}, {"title": "Colin G. Randall", "type": "person", "state": "s", "avail": true, "age": 83, "date": 405903600000.0, "remarks": "Decays universities handily."}, {"title": "Kelly W. Mills", "type": "person", "avail": true, "age": 76, "date": 832716000000.0}, {"title": "Jan Powell", "type": "person", "state": "s", "avail": true, "age": 57}, {"title": "Bella N. McGrath", "type": "person", "avail": true, "age": 68}, {"title": "Alison Cameron", "type": "person", "age": 21, "date": 104108400000.0}, {"title": "Emma Churchill", "type": "person", "state": "s", "avail": true, "age": 73, "date": 1272751200000.0}, {"title": "Jack A. Davidson", "type": "person", "avail": true, "age": 43, "date": 1205190000000.0}, {"title": "Piers K. Brown", "type": "person", "avail": true, "age": 23, "date": 1319407200000.0}, {"title": "Una R. McLean", "type": "person", "avail": true, "age": 79, "remarks": "Whispers foxes unselfishly."}, {"title": "Sebastian Parr", "type": "person", "avail": true, "age": 73}, {"title": "Jesse Jones", "type": "person", "avail": true, "age": 46, "remarks": "Wastes horrors intensely."}]}, {"title": "Beseech mammoths", "type": "role", "children": [{"title": "Charles Smith", "type": "person", "avail": true, "age": 91, "remarks": "Drives ropes affirmatively."}, {"title": "Simon Hunter", "type": "person", "state": "h", "avail": true, "age": 44, "date": 954885600000.0}, {"title": "Samantha Peake", "type": "person", "state": "s", "avail": true, "age": 58}, {"title": "Austin Burgess", "type": "person", "avail": true, "age": 59, "date": 63759600000.0, "remarks": "Saddles souths highly."}, {"title": "Daryl Arnold", "type": "person", "state": "s", "age": 67, "date": 230770800000.0, "remarks": "Restrains eyes abundantly."}, {"title": "Christopher Lee", "type": "person", "state": "s", "avail": true, "age": 43}, {"title": "Alan C. MacDonald", "type": "person", "state": "s", "avail": true, "age": 93}, {"title": "Emma I. Lawrence", "type": "person", "avail": true, "age": 42, "remarks": "Lends potentials brightly."}, {"title": "Anne H. Oliver", "type": "person", "avail": true, "age": 26, "date": 1286488800000.0}, {"title": "Neil Poole", "type": "person", "avail": true, "age": 93, "date": 1069887600000.0, "remarks": "Implants steaks upright."}]}, {"title": "Signify yellows", "type": "role", "expanded": true, "children": [{"title": "Jean F. Rees", "type": "person", "state": "s", "avail": true, "age": 45}, {"title": "Fiona Y. Forsyth", "type": "person", "state": "s", "avail": true, "age": 31, "remarks": "Excludes comparisons faithfully."}, {"title": "Benjamin Buckland", "type": "person", "avail": true, "age": 67, "date": 26262000000.0}, {"title": "Dan Hunter", "type": "person", "age": 48, "date": 487980000000.0}, {"title": "Boris Churchill", "type": "person", "state": "h", "avail": true, "age": 81}, {"title": "Brian Nash", "type": "person", "avail": true, "age": 52}, {"title": "Chris Black", "type": "person", "avail": true, "age": 90, "date": 1081720800000.0, "remarks": "Clicks comfortables upright."}, {"title": "Joanne O. Langdon", "type": "person", "avail": true, "age": 27}, {"title": "Simon J. Greene", "type": "person", "state": "h", "avail": true, "age": 73, "date": 1180476000000.0}]}, {"title": "Contrast towers", "type": "role", "children": [{"title": "Charlie Duncan", "type": "person", "state": "h", "avail": true, "age": 53, "remarks": "Shines passengers decently."}, {"title": "Ella F. Gibson", "type": "person", "avail": true, "age": 87, "remarks": "Swears witnesses entirely."}, {"title": "Elizabeth North", "type": "person", "age": 69, "date": 618616800000.0}, {"title": "Blake Hart", "type": "person", "state": "h", "avail": true, "age": 95, "date": 75682800000.0}, {"title": "Tim Grant", "type": "person", "avail": true, "age": 38}, {"title": "Elizabeth R. Simpson", "type": "person", "avail": true, "age": 57, "date": 402274800000.0}, {"title": "Chloe Tucker", "type": "person", "avail": true, "age": 79, "remarks": "Contradicts airports generously."}, {"title": "Charlie Mitchell", "type": "person", "avail": true, "age": 72}, {"title": "Alexandra D. Short", "type": "person", "avail": true, "age": 62}, {"title": "Frank Z. Fraser", "type": "person", "avail": true, "age": 79}, {"title": "Rose Gill", "type": "person", "avail": true, "age": 86, "date": 1446591600000.0, "remarks": "Arises educations explicitly."}, {"title": "Owen Paterson", "type": "person", "avail": true, "age": 91}]}, {"title": "Fight trades", "type": "role", "expanded": true, "children": [{"title": "Abigail W. Greene", "type": "person", "avail": true, "age": 36}, {"title": "Yvonne Q. Vaughan", "type": "person", "state": "s", "avail": true, "age": 81, "date": 1423436400000.0}, {"title": "Andy Y. McGrath", "type": "person", "state": "s", "avail": true, "age": 79, "date": 1526508000000.0}, {"title": "Alex J. Gibson", "type": "person", "avail": true, "age": 74, "date": 1418770800000.0}, {"title": "Dorothy Roberts", "type": "person", "avail": true, "age": 86, "remarks": "Verifies asses explicitly."}, {"title": "Toby Parsons", "type": "person", "avail": true, "age": 60, "date": 231980400000.0}, {"title": "Hannah North", "type": "person", "avail": true, "age": 49, "date": 184806000000.0}, {"title": "Sophie Q. Bell", "type": "person", "avail": true, "age": 98, "date": 1153605600000.0}, {"title": "Hannah U. Bailey", "type": "person", "avail": true, "age": 71, "date": 111020400000.0}, {"title": "Brandon J. Clarkson", "type": "person", "state": "h", "avail": true, "age": 35, "date": 1037746800000.0}, {"title": "Jean O. Dickens", "type": "person", "avail": true, "age": 97, "remarks": "Prepares spends thoroughly."}, {"title": "Colin Langdon", "type": "person", "age": 95, "date": 1660255200000.0}, {"title": "Phoenix Morrison", "type": "person", "avail": true, "age": 65, "date": 780706800000.0, "remarks": "Blushes ages carefully."}, {"title": "Sophie E. Bower", "type": "person", "state": "h", "avail": true, "age": 25}, {"title": "Edward E. Mitchell", "type": "person", "state": "s", "avail": true, "age": 34, "date": 542070000000.0}]}]}, {"title": "Dept. for Schools and Standards", "type": "department", "children": [{"title": "Fight messages", "type": "role", "children": [{"title": "Blake Paige", "type": "person", "avail": true, "age": 76, "remarks": "Swims wifes fluently."}, {"title": "Max G. Metcalfe", "type": "person", "avail": true, "age": 42}, {"title": "Jessica I. Martin", "type": "person", "avail": true, "age": 51, "date": 659574000000.0}, {"title": "Victor C. Roberts", "type": "person", "avail": true, "age": 79, "date": 1599948000000.0}, {"title": "Lauren Hamilton", "type": "person", "avail": true, "age": 77, "date": 1228258800000.0}, {"title": "Jessica W. Bell", "type": "person", "avail": true, "age": 94, "date": 316825200000.0}, {"title": "Pat Paterson", "type": "person", "avail": true, "age": 61, "date": 721609200000.0}, {"title": "Toby Poole", "type": "person", "avail": true, "age": 83, "date": 1659996000000.0, "remarks": "Admits joins perfectly."}, {"title": "Frances Z. Parr", "type": "person", "state": "s", "age": 52}, {"title": "Frances Randall", "type": "person", "avail": true, "age": 44, "date": 365983200000.0}, {"title": "Casey Gibson", "type": "person", "state": "s", "avail": true, "age": 72, "remarks": "Sanctions routines passionately."}, {"title": "Emma Parsons", "type": "person", "avail": true, "age": 50, "date": 303692400000.0, "remarks": "Wanders mixtures remarkably."}, {"title": "Austin Gray", "type": "person", "avail": true, "age": 24, "date": 904255200000.0}, {"title": "Colin F. Wilkins", "type": "person", "avail": true, "age": 72, "date": 1446418800000.0, "remarks": "Hinders teaches well."}]}, {"title": "Eye grasses", "type": "role", "expanded": true, "children": [{"title": "Eric Hemmings", "type": "person", "avail": true, "age": 37, "date": 1368914400000.0, "remarks": "Weaves lows boldly."}, {"title": "Olivia G. Cornish", "type": "person", "avail": true, "age": 65, "date": 1144792800000.0}, {"title": "Max M. Hart", "type": "person", "avail": true, "age": 61, "date": 1486594800000.0}, {"title": "Edward Short", "type": "person", "avail": true, "age": 84, "date": 313455600000.0}, {"title": "Irene R. Simpson", "type": "person", "avail": true, "age": 54}, {"title": "Daryl X. Paterson", "type": "person", "avail": true, "age": 63}]}, {"title": "Saw insides", "type": "role", "expanded": true, "children": [{"title": "Daryl Kerr", "type": "person", "state": "h", "avail": true, "age": 53, "date": 1233788400000.0, "remarks": "Counsels generals efficiently."}, {"title": "Jane I. Rampling", "type": "person", "avail": true, "age": 21, "date": 1291330800000.0}, {"title": "Steven U. Ferguson", "type": "person", "avail": true, "age": 40, "date": 1621461600000.0, "remarks": "Scams categories healthily."}, {"title": "Corey Fisher", "type": "person", "state": "s", "avail": true, "age": 35, "date": 1285192800000.0}, {"title": "Stephanie Rampling", "type": "person", "avail": true, "age": 25, "date": 165625200000.0}, {"title": "Amelia F. Roberts", "type": "person", "state": "h", "avail": true, "age": 79, "date": 1166569200000.0}]}, {"title": "Imagine tensions", "type": "role", "children": [{"title": "Luke P. Bond", "type": "person", "avail": true, "age": 67}, {"title": "Julia R. Clarkson", "type": "person", "avail": true, "age": 26, "date": 751590000000.0}, {"title": "Eric Ince", "type": "person", "avail": true, "age": 31}, {"title": "Toby K. Hodges", "type": "person", "avail": true, "age": 70}, {"title": "Jan E. Johnston", "type": "person", "avail": true, "age": 89, "date": 1469052000000.0}, {"title": "Charlie G. Ross", "type": "person", "avail": true, "age": 29}, {"title": "Rose N. Blake", "type": "person", "state": "h", "avail": true, "age": 52, "date": 1148076000000.0}, {"title": "Lillian E. Knox", "type": "person", "avail": true, "age": 21, "date": 1576882800000.0}, {"title": "Alison I. Welch", "type": "person", "avail": true, "age": 23}, {"title": "Toby North", "type": "person", "avail": true, "age": 41, "date": 284511600000.0}]}, {"title": "Incise dogs", "type": "role", "children": [{"title": "Wanda B. May", "type": "person", "age": 72, "date": 1010271600000.0}, {"title": "Keith Bower", "type": "person", "avail": true, "age": 51, "date": 948063600000.0}, {"title": "Una Piper", "type": "person", "state": "s", "avail": true, "age": 97, "date": 486252000000.0, "remarks": "Glitters policies amazingly."}, {"title": "Victor J. Scott", "type": "person", "age": 43, "date": 1457910000000.0, "remarks": "Pours prompts rapidly."}, {"title": "Matt B. Rampling", "type": "person", "avail": true, "age": 70, "remarks": "Retires softs ethically."}, {"title": "Molly Q. Nash", "type": "person", "avail": true, "age": 59, "date": 1531519200000.0}, {"title": "Sydney Stewart", "type": "person", "avail": true, "age": 85, "date": 440118000000.0}, {"title": "Steven D. Gill", "type": "person", "state": "s", "avail": true, "age": 21}, {"title": "Sydney N. Piper", "type": "person", "avail": true, "age": 27, "date": 582847200000.0}]}, {"title": "Recur creams", "type": "role", "children": [{"title": "Joshua P. Poole", "type": "person", "age": 97, "remarks": "Tests crickets jubilantly."}]}, {"title": "March wonders", "type": "role", "children": [{"title": "Elizabeth Lee", "type": "person", "avail": true, "age": 51}, {"title": "Taylor U. Vaughan", "type": "person", "avail": true, "age": 57}, {"title": "Gavin Peake", "type": "person", "avail": true, "age": 91, "date": 186620400000.0}, {"title": "Anne K. Hart", "type": "person", "state": "h", "avail": true, "age": 89, "date": 163465200000.0, "remarks": "Controls readings extensively."}, {"title": "Brian Hughes", "type": "person", "avail": true, "age": 64, "remarks": "Ponders grandmothers luckily."}]}, {"title": "Rewind reasons", "type": "role", "children": [{"title": "Edward I. Taylor", "type": "person", "avail": true, "age": 93, "date": 633394800000.0, "remarks": "Greets cities delightfully."}, {"title": "Elizabeth Q. Bower", "type": "person", "state": "h", "avail": true, "age": 83, "date": 543970800000.0}, {"title": "Christian Underwood", "type": "person", "avail": true, "age": 50, "date": 225068400000.0}]}, {"title": "Donate regulars", "type": "role", "expanded": true, "children": [{"title": "Jennifer I. Manning", "type": "person", "state": "h", "avail": true, "age": 56, "date": 889052400000.0}, {"title": "Karen T. Chapman", "type": "person", "avail": true, "age": 63, "date": 1140044400000.0}, {"title": "Taylor Mackay", "type": "person", "avail": true, "age": 46}, {"title": "Thomas Ogden", "type": "person", "avail": true, "age": 41, "date": 1520809200000.0, "remarks": "Imprisons leads reliably."}, {"title": "Blake Carr", "type": "person", "state": "s", "avail": true, "age": 26, "remarks": "Scorches instances diligently."}, {"title": "Charlie J. Hodges", "type": "person", "avail": true, "age": 92, "date": 702424800000.0}]}, {"title": "Quit editors", "type": "role", "children": [{"title": "Lucas K. North", "type": "person", "state": "h", "avail": true, "age": 76, "date": 424994400000.0}, {"title": "Adrian Piper", "type": "person", "avail": true, "age": 51, "date": 295138800000.0}, {"title": "Jayden Hill", "type": "person", "state": "s", "avail": true, "age": 93, "date": 1248300000000.0, "remarks": "Damps hosts explicitly."}, {"title": "Ruth Wilson", "type": "person", "avail": true, "age": 33, "date": 505263600000.0}, {"title": "Neil Tucker", "type": "person", "state": "h", "avail": true, "age": 24}, {"title": "Adam Dickens", "type": "person", "avail": true, "age": 44, "date": 1088114400000.0, "remarks": "Guards illegals happily."}, {"title": "Sean Alsop", "type": "person", "avail": true, "age": 69, "date": 650757600000.0}, {"title": "Kylie D. Powell", "type": "person", "avail": true, "age": 58}, {"title": "Yvonne X. King", "type": "person", "age": 52, "date": 1590703200000.0, "remarks": "Teaches tops respectably."}, {"title": "Paul Mitchell", "type": "person", "avail": true, "age": 72, "date": 1337205600000.0}, {"title": "Heather Campbell", "type": "person", "avail": true, "age": 50, "date": 67734000000.0}, {"title": "Sam E. Dyer", "type": "person", "avail": true, "age": 30}, {"title": "Anne F. Mitchell", "type": "person", "avail": true, "age": 28, "remarks": "Constitutes natures earnestly."}, {"title": "Frances P. Gill", "type": "person", "avail": true, "age": 69, "date": 186361200000.0}, {"title": "Angela J. Anderson", "type": "person", "avail": true, "age": 95}, {"title": "Sarah B. McDonald", "type": "person", "avail": true, "age": 96}, {"title": "Kimberly Peters", "type": "person", "avail": true, "age": 94, "date": 697503600000.0}, {"title": "Carl M. May", "type": "person", "avail": true, "age": 34, "remarks": "Implodes stars assuredly."}, {"title": "Gordon T. McDonald", "type": "person", "avail": true, "age": 49, "date": 1083880800000.0}]}, {"title": "Crave horrors", "type": "role", "children": [{"title": "Andrea D. Newman", "type": "person", "state": "s", "avail": true, "age": 45}, {"title": "Jonathan B. Vaughan", "type": "person", "avail": true, "age": 26}]}]}, {"title": "Dept. for Doors and Backgrounds", "type": "department", "children": [{"title": "Reply lifts", "type": "role", "children": [{"title": "Kimberly Cornish", "type": "person", "avail": true, "age": 61, "date": 243558000000.0, "remarks": "Shaves slides amazingly."}, {"title": "Molly Z. Ball", "type": "person", "avail": true, "age": 45, "date": 1403388000000.0, "remarks": "Retches jumps helpfully."}, {"title": "Fiona L. Short", "type": "person", "avail": true, "age": 33}, {"title": "Madeleine W. Vaughan", "type": "person", "avail": true, "age": 39, "remarks": "Adjusts designs delicately."}, {"title": "Jacob Turner", "type": "person", "avail": true, "age": 47, "date": 615592800000.0}, {"title": "Stephen M. Mills", "type": "person", "avail": true, "age": 90, "date": 1399586400000.0, "remarks": "Fetches amounts early."}]}, {"title": "Persuade natives", "type": "role", "expanded": true, "children": [{"title": "Frances J. Blake", "type": "person", "state": "s", "avail": true, "age": 89, "remarks": "Cools midnights suitably."}, {"title": "Toby J. Morgan", "type": "person", "age": 60}, {"title": "Jane McDonald", "type": "person", "avail": true, "age": 64, "date": 986162400000.0}, {"title": "Una Robertson", "type": "person", "state": "s", "avail": true, "age": 70, "date": 647733600000.0, "remarks": "Flaps structures fluently."}, {"title": "Bobbie O. Watson", "type": "person", "state": "h", "avail": true, "age": 28, "remarks": "States brothers extraordinarily."}, {"title": "Gordon Lawrence", "type": "person", "avail": true, "age": 29, "date": 1221861600000.0}, {"title": "Ryan K. Dickens", "type": "person", "state": "s", "avail": true, "age": 21, "remarks": "Shears levels interestingly."}, {"title": "Michelle Howard", "type": "person", "state": "s", "avail": true, "age": 92, "date": 823129200000.0}, {"title": "Theresa Nash", "type": "person", "state": "h", "avail": true, "age": 29, "date": 733788000000.0}, {"title": "Max McDonald", "type": "person", "state": "h", "avail": true, "age": 82, "date": 1511218800000.0, "remarks": "Improves histories intently."}, {"title": "Emma A. Black", "type": "person", "avail": true, "age": 23, "date": 197938800000.0, "remarks": "Hustles beginnings happily."}, {"title": "Brandon L. Turner", "type": "person", "state": "s", "avail": true, "age": 40, "remarks": "Scolds angers deeply."}, {"title": "Andy Y. Ross", "type": "person", "avail": true, "age": 77, "date": 519775200000.0, "remarks": "Inflames tests skillfully."}]}, {"title": "Rest arrivals", "type": "role", "expanded": true, "children": [{"title": "Jonathan B. McDonald", "type": "person", "avail": true, "age": 81}, {"title": "Jesse Berry", "type": "person", "avail": true, "age": 62}, {"title": "Sydney Harris", "type": "person", "avail": true, "age": 67, "date": 586130400000.0}, {"title": "Irene Johnston", "type": "person", "avail": true, "age": 61, "date": 787791600000.0}, {"title": "Jesse N. Reid", "type": "person", "avail": true, "age": 32, "date": 643327200000.0}, {"title": "Nicholas Peters", "type": "person", "avail": true, "age": 46}, {"title": "Heather Abraham", "type": "person", "avail": true, "age": 43, "date": 848185200000.0}, {"title": "Christopher Q. Watson", "type": "person", "avail": true, "age": 37, "remarks": "Smoothes obligations brilliantly."}, {"title": "Julian B. Marshall", "type": "person", "avail": true, "age": 23, "date": 156812400000.0}, {"title": "Felicity M. Sharp", "type": "person", "avail": true, "age": 49}, {"title": "Connor Ellison", "type": "person", "avail": true, "age": 68, "remarks": "Scarps guies fortunately."}]}, {"title": "Help rows", "type": "role", "children": [{"title": "Angela A. Coleman", "type": "person", "avail": true, "age": 75}, {"title": "Ian Paige", "type": "person", "avail": true, "age": 37, "date": 1026770400000.0}, {"title": "Maria Wilson", "type": "person", "avail": true, "age": 98, "date": 426808800000.0}]}, {"title": "Breed girls", "type": "role", "expanded": true, "children": [{"title": "James Metcalfe", "type": "person", "avail": true, "age": 61, "remarks": "Waxes grandmothers upwardly."}, {"title": "Grace McGrath", "type": "person", "avail": true, "age": 30}, {"title": "Joshua Reid", "type": "person", "state": "h", "avail": true, "age": 38}, {"title": "Abigail H. Hunter", "type": "person", "avail": true, "age": 33, "date": 1429308000000.0}, {"title": "Felicity F. Rees", "type": "person", "avail": true, "age": 85, "remarks": "Frees mights significantly."}, {"title": "Jesse Graham", "type": "person", "avail": true, "age": 33, "date": 1652738400000.0}, {"title": "Frances U. Reid", "type": "person", "state": "h", "avail": true, "age": 24, "date": 210898800000.0}, {"title": "Julia I. Walsh", "type": "person", "state": "s", "avail": true, "age": 65, "date": 737589600000.0, "remarks": "Utters requirements very."}, {"title": "Jack Y. Davidson", "type": "person", "avail": true, "age": 62, "date": 1566252000000.0, "remarks": "Scorches hamsters immeasurably."}, {"title": "Tim Hemmings", "type": "person", "avail": true, "age": 57, "date": 1591308000000.0, "remarks": "Sways treats healthily."}, {"title": "Carolyn Young", "type": "person", "avail": true, "age": 44}, {"title": "Oliver Randall", "type": "person", "state": "s", "avail": true, "age": 71, "date": 1361919600000.0}, {"title": "Dylan Gibson", "type": "person", "avail": true, "age": 38, "date": 1131058800000.0}, {"title": "Pippa H. May", "type": "person", "avail": true, "age": 68}]}, {"title": "Scald acts", "type": "role", "children": [{"title": "Tim L. Miller", "type": "person", "avail": true, "age": 31, "date": 1585864800000.0, "remarks": "Boasts farms easily."}, {"title": "Pippa Stewart", "type": "person", "avail": true, "age": 31, "date": 718758000000.0}, {"title": "Charlie Young", "type": "person", "age": 69, "date": 827017200000.0, "remarks": "Looks futures reliably."}, {"title": "Claire Avery", "type": "person", "avail": true, "age": 63, "date": 1360018800000.0}, {"title": "Matt Johnston", "type": "person", "state": "h", "avail": true, "age": 68, "date": 1500156000000.0}, {"title": "Frances Underwood", "type": "person", "avail": true, "age": 30, "date": 1623276000000.0}, {"title": "Chris A. Walker", "type": "person", "avail": true, "age": 45}, {"title": "Caroline Paterson", "type": "person", "avail": true, "age": 41}, {"title": "Phil Z. Morgan", "type": "person", "avail": true, "age": 50}, {"title": "Peter Peake", "type": "person", "state": "s", "avail": true, "age": 74, "date": 122598000000.0}, {"title": "Jamie Scott", "type": "person", "avail": true, "age": 39, "date": 645832800000.0}]}, {"title": "Correct beautifuls", "type": "role", "children": [{"title": "Matt Q. Duncan", "type": "person", "state": "h", "avail": true, "age": 90, "date": 1204844400000.0}, {"title": "Angela Taylor", "type": "person", "state": "s", "avail": true, "age": 82}, {"title": "Sonia Wilson", "type": "person", "state": "h", "avail": true, "age": 98, "date": 1093125600000.0, "remarks": "Contrasts efforts explicitly."}, {"title": "Emma S. Reid", "type": "person", "avail": true, "age": 42}, {"title": "Victoria D. Butler", "type": "person", "state": "h", "avail": true, "age": 46}, {"title": "Eddie Q. Newman", "type": "person", "avail": true, "age": 44, "date": 7686000000.0}, {"title": "Wanda F. King", "type": "person", "avail": true, "age": 45, "date": 410310000000.0}, {"title": "Sue W. Jackson", "type": "person", "avail": true, "age": 57}, {"title": "Jan P. Brown", "type": "person", "state": "s", "age": 58, "date": 432856800000.0}, {"title": "Nicholas T. Sutherland", "type": "person", "state": "s", "avail": true, "age": 21, "date": 1125525600000.0}, {"title": "Rebecca Rampling", "type": "person", "state": "s", "age": 42, "date": 1195945200000.0}, {"title": "Penelope Grant", "type": "person", "avail": true, "age": 89, "date": 703893600000.0, "remarks": "Tears menus usefully."}, {"title": "Eddie Z. Duncan", "type": "person", "avail": true, "age": 42}, {"title": "Michael Reid", "type": "person", "avail": true, "age": 51, "date": 1288998000000.0}, {"title": "Michelle Hughes", "type": "person", "avail": true, "age": 27, "date": 1144620000000.0}, {"title": "Connor S. Ball", "type": "person", "state": "h", "avail": true, "age": 70}, {"title": "Jan K. Scott", "type": "person", "state": "h", "avail": true, "age": 53, "date": 1236726000000.0}, {"title": "Max Abraham", "type": "person", "avail": true, "age": 48, "remarks": "Hopes girlfriends elegantly."}, {"title": "Frances B. McLean", "type": "person", "state": "s", "avail": true, "age": 97, "remarks": "Complains visuals rapidly."}]}, {"title": "Change chains", "type": "role", "expanded": true, "children": [{"title": "Kylie Buckland", "type": "person", "state": "h", "age": 35}, {"title": "Kelly W. Lewis", "type": "person", "avail": true, "age": 37, "date": 1092520800000.0}]}]}]} ================================================ FILE: docs/assets/json/tree_department_M_t_c.json ================================================ {"types":{"department":{"icon":"bi bi-diagram-3","colspan":true},"role":{"icon":"bi bi-microsoft-teams","colspan":true},"person":{"icon":"bi bi-person"}},"columns":[{"title":"Title","id":"*","width":"250px","sortable":true},{"title":"Age","id":"age","width":"50px","html":"","classes":"wb-helper-end","sortable":true},{"title":"Date","id":"date","width":"100px","html":"","sortable":true},{"title":"Mood","id":"mood","width":"70px","html":"\n","sortable":true},{"title":"Remarks","id":"remarks","width":"300px","html":"","sortable":true},{"title":"#1","id":"state_1","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#2","id":"state_2","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#3","id":"state_3","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#4","id":"state_4","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#5","id":"state_5","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#6","id":"state_6","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#7","id":"state_7","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#8","id":"state_8","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#9","id":"state_9","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#10","id":"state_10","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#11","id":"state_11","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#12","id":"state_12","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#13","id":"state_13","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#14","id":"state_14","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#15","id":"state_15","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#16","id":"state_16","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#17","id":"state_17","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#18","id":"state_18","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#19","id":"state_19","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#20","id":"state_20","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#21","id":"state_21","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#22","id":"state_22","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#23","id":"state_23","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#24","id":"state_24","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#25","id":"state_25","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#26","id":"state_26","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#27","id":"state_27","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#28","id":"state_28","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#29","id":"state_29","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#30","id":"state_30","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#31","id":"state_31","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#32","id":"state_32","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#33","id":"state_33","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#34","id":"state_34","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#35","id":"state_35","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#36","id":"state_36","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#37","id":"state_37","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#38","id":"state_38","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#39","id":"state_39","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#40","id":"state_40","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#41","id":"state_41","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#42","id":"state_42","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#43","id":"state_43","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#44","id":"state_44","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#45","id":"state_45","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#46","id":"state_46","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#47","id":"state_47","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#48","id":"state_48","width":"30px","classes":"wb-helper-center","html":"","sortable":false},{"title":"#49","id":"state_49","width":"30px","classes":"wb-helper-center","html":"","sortable":false}],"children":[{"type":"department","title":"Dept. for Carries and Koalas","children":[{"type":"role","title":"Construe persons","children":[{"type":"person","title":"Karen Lee","state":"h","avail":true,"age":64,"date":1500940800000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_3":true,"state_7":true,"state_12":true,"state_13":true,"state_23":true,"state_24":true,"state_27":true,"state_41":true,"state_44":true,"state_47":true,"state_48":true},{"type":"person","title":"Jonathan A. Mitchell","state":"h","avail":true,"age":97,"date":472953600000.0,"state_2":true,"state_5":true,"state_9":true,"state_11":true,"state_20":true,"state_28":true,"state_29":true,"state_34":true,"state_42":true,"state_48":true},{"type":"person","title":"Faith U. Powell","avail":true,"age":34,"state_20":true,"state_32":true,"state_34":true,"state_35":true,"state_46":true,"state_48":true,"state_49":true},{"type":"person","title":"Stephen Knox","avail":true,"age":63,"state_5":true,"state_15":true,"state_21":true,"state_23":true,"state_26":true,"state_32":true,"state_40":true,"state_42":true,"state_45":true,"state_47":true},{"type":"person","title":"Joseph H. Smith","state":"h","avail":true,"age":90,"state_5":true,"state_8":true,"state_9":true,"state_11":true,"state_14":true,"state_24":true,"state_27":true,"state_30":true,"state_31":true,"state_32":true,"state_40":true,"state_44":true,"state_46":true},{"type":"person","title":"Christopher Lee","state":"s","avail":true,"age":32,"date":469152000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_6":true,"state_7":true,"state_13":true,"state_18":true,"state_22":true,"state_31":true,"state_33":true,"state_38":true,"state_42":true},{"type":"person","title":"Karen Peake","avail":true,"age":42,"date":1650240000000.0,"state_4":true,"state_11":true,"state_21":true,"state_32":true,"state_39":true,"state_41":true,"state_45":true},{"type":"person","title":"Richard Wallace","age":50,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_14":true,"state_18":true,"state_19":true,"state_26":true,"state_39":true,"state_43":true,"state_44":true,"state_48":true},{"type":"person","title":"Bernadette H. Berry","state":"s","avail":true,"age":40,"date":367372800000.0,"state_2":true,"state_4":true,"state_5":true,"state_7":true,"state_9":true,"state_12":true,"state_27":true,"state_32":true,"state_37":true,"state_41":true,"state_47":true}]},{"type":"role","title":"Imagine secretaries","children":[{"type":"person","title":"Pat I. Short","age":86,"date":1180828800000.0,"state_7":true,"state_15":true,"state_18":true,"state_20":true,"state_25":true,"state_31":true,"state_37":true,"state_41":true,"state_42":true,"state_45":true,"state_46":true,"state_50":true},{"type":"person","title":"Phil McLean","state":"s","avail":true,"age":72,"date":764035200000.0,"state_1":true,"state_2":true,"state_9":true,"state_10":true,"state_12":true,"state_26":true,"state_29":true,"state_31":true,"state_37":true,"state_40":true,"state_41":true},{"type":"person","title":"Joan J. Watson","avail":true,"age":31,"date":832118400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_1":true,"state_7":true,"state_10":true,"state_16":true,"state_20":true,"state_21":true,"state_22":true,"state_23":true,"state_35":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Nicholas Vaughan","state":"s","avail":true,"age":29,"state_12":true,"state_17":true,"state_21":true,"state_23":true,"state_26":true,"state_49":true},{"type":"person","title":"Sean E. Young","avail":true,"age":45,"date":903052800000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_2":true,"state_3":true,"state_6":true,"state_7":true,"state_11":true,"state_25":true,"state_29":true,"state_44":true,"state_45":true,"state_47":true},{"type":"person","title":"Natalie Poole","state":"h","avail":true,"age":48,"date":469065600000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_1":true,"state_4":true,"state_6":true,"state_11":true,"state_12":true,"state_13":true,"state_23":true,"state_29":true,"state_33":true,"state_40":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Liam E. Mathis","state":"h","avail":true,"age":79,"state_2":true,"state_7":true,"state_8":true,"state_10":true,"state_13":true,"state_21":true,"state_24":true,"state_28":true,"state_36":true,"state_37":true,"state_40":true,"state_42":true,"state_47":true},{"type":"person","title":"Leonard X. Brown","age":30,"date":719798400000.0,"state_1":true,"state_2":true,"state_6":true,"state_28":true,"state_31":true,"state_43":true},{"type":"person","title":"Jayden Kelly","avail":true,"age":29,"date":1718150400000.0,"state_4":true,"state_5":true,"state_23":true,"state_28":true,"state_38":true,"state_40":true,"state_43":true,"state_45":true,"state_50":true},{"type":"person","title":"Kelly Grant","avail":true,"age":71,"date":692323200000.0,"state_12":true,"state_15":true,"state_24":true,"state_27":true,"state_35":true,"state_37":true,"state_41":true},{"type":"person","title":"Thomas Ince","age":38,"date":308534400000.0,"state_1":true,"state_7":true,"state_14":true,"state_27":true,"state_33":true,"state_37":true,"state_50":true},{"type":"person","title":"Austin C. May","avail":true,"age":64,"date":1616716800000.0,"state_9":true,"state_16":true,"state_19":true,"state_20":true,"state_21":true,"state_26":true,"state_31":true,"state_36":true,"state_37":true,"state_45":true,"state_50":true},{"type":"person","title":"Elizabeth Black","avail":true,"age":61,"state_3":true,"state_4":true,"state_7":true,"state_26":true,"state_33":true,"state_38":true},{"type":"person","title":"Evan Hamilton","state":"s","avail":true,"age":56,"state_2":true,"state_4":true,"state_9":true,"state_16":true,"state_17":true,"state_22":true,"state_23":true,"state_27":true,"state_30":true,"state_38":true},{"type":"person","title":"Madeleine Rees","avail":true,"age":30,"date":394761600000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_5":true,"state_8":true,"state_16":true,"state_23":true,"state_34":true,"state_35":true,"state_43":true,"state_48":true}]},{"type":"role","title":"Chew insurances","children":[{"type":"person","title":"Madeleine W. Paterson","age":53,"date":1215388800000.0,"state_12":true,"state_13":true,"state_17":true,"state_21":true,"state_26":true,"state_34":true,"state_42":true,"state_44":true},{"type":"person","title":"Jason Kelly","avail":true,"age":58,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_4":true,"state_16":true,"state_19":true,"state_20":true,"state_27":true,"state_28":true,"state_32":true,"state_33":true,"state_39":true,"state_40":true,"state_43":true,"state_45":true},{"type":"person","title":"Hannah Howard","state":"s","avail":true,"age":85,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_7":true,"state_10":true,"state_12":true,"state_18":true,"state_20":true,"state_23":true,"state_24":true,"state_25":true,"state_34":true,"state_39":true,"state_40":true,"state_43":true,"state_45":true},{"type":"person","title":"Kevin Hardacre","avail":true,"age":88,"state_1":true,"state_11":true,"state_13":true,"state_20":true,"state_21":true,"state_24":true,"state_28":true,"state_34":true,"state_37":true,"state_40":true,"state_50":true}]},{"type":"role","title":"Forget gos","children":[{"type":"person","title":"Brian Murray","avail":true,"age":21,"date":1686096000000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_1":true,"state_4":true,"state_5":true,"state_19":true,"state_20":true,"state_38":true,"state_39":true,"state_43":true},{"type":"person","title":"Jayden Martin","avail":true,"age":86,"date":1311724800000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_2":true,"state_5":true,"state_6":true,"state_18":true,"state_20":true,"state_26":true,"state_35":true,"state_36":true,"state_41":true,"state_43":true,"state_45":true,"state_48":true},{"type":"person","title":"Dan Vaughan","avail":true,"age":82,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_2":true,"state_5":true,"state_30":true,"state_40":true,"state_41":true,"state_48":true,"state_49":true},{"type":"person","title":"Adrian Johnston","avail":true,"age":48,"date":1696377600000.0,"state_15":true,"state_16":true,"state_20":true,"state_29":true,"state_39":true,"state_42":true,"state_45":true},{"type":"person","title":"Pippa Nash","avail":true,"age":60,"state_6":true,"state_8":true,"state_14":true,"state_19":true,"state_24":true,"state_26":true,"state_28":true,"state_29":true,"state_37":true,"state_40":true,"state_44":true,"state_46":true},{"type":"person","title":"Jayden H. Mackay","avail":true,"age":27,"date":1205193600000.0,"state_2":true,"state_16":true,"state_19":true,"state_20":true,"state_22":true,"state_23":true,"state_26":true,"state_37":true,"state_39":true},{"type":"person","title":"Daryl Manning","state":"s","avail":true,"age":48,"date":76896000000.0,"state_5":true,"state_9":true,"state_11":true,"state_24":true,"state_25":true,"state_26":true,"state_35":true,"state_40":true,"state_46":true,"state_48":true},{"type":"person","title":"Andy Lewis","avail":true,"age":31,"date":109641600000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_4":true,"state_6":true,"state_9":true,"state_10":true,"state_15":true,"state_17":true,"state_19":true,"state_34":true,"state_36":true,"state_39":true,"state_46":true},{"type":"person","title":"Katherine T. Abraham","avail":true,"age":70,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_46":true},{"type":"person","title":"Angela P. Poole","state":"h","avail":true,"age":21,"date":1494979200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_3":true,"state_7":true,"state_12":true,"state_15":true,"state_18":true,"state_19":true,"state_20":true,"state_23":true,"state_27":true,"state_30":true,"state_39":true,"state_45":true,"state_47":true},{"type":"person","title":"Oliver Dowd","age":54,"date":1678752000000.0,"state_1":true,"state_3":true,"state_7":true,"state_8":true,"state_10":true,"state_16":true,"state_34":true,"state_39":true,"state_41":true,"state_42":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Eddie O. Robertson","avail":true,"age":35,"date":580867200000.0,"state_12":true,"state_15":true,"state_19":true,"state_20":true,"state_22":true,"state_28":true,"state_43":true,"state_45":true,"state_46":true,"state_48":true},{"type":"person","title":"Christopher Morrison","avail":true,"age":47,"date":825811200000.0,"state_4":true,"state_8":true,"state_12":true,"state_13":true,"state_15":true,"state_16":true,"state_19":true,"state_20":true,"state_24":true,"state_27":true,"state_29":true,"state_34":true,"state_46":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Richard Lawrence","avail":true,"age":61,"date":1331769600000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_7":true,"state_12":true,"state_13":true,"state_16":true,"state_17":true,"state_21":true,"state_26":true,"state_32":true,"state_40":true,"state_45":true},{"type":"person","title":"Melanie Rampling","avail":true,"age":26,"date":934588800000.0,"state_1":true,"state_3":true,"state_7":true,"state_8":true,"state_15":true,"state_20":true,"state_25":true,"state_35":true,"state_44":true,"state_45":true,"state_46":true},{"type":"person","title":"Nicholas T. Fisher","state":"s","age":85,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_2":true,"state_8":true,"state_9":true,"state_25":true,"state_28":true,"state_47":true,"state_49":true},{"type":"person","title":"Toby Hudson","state":"s","avail":true,"age":45,"date":946339200000.0,"state_7":true,"state_8":true,"state_12":true,"state_22":true,"state_36":true,"state_38":true,"state_42":true,"state_48":true}]},{"type":"role","title":"Prevent airlines","children":[{"type":"person","title":"Phoenix P. Forsyth","avail":true,"age":86,"state_2":true,"state_8":true,"state_10":true,"state_12":true,"state_13":true,"state_15":true,"state_17":true,"state_19":true,"state_32":true,"state_33":true,"state_34":true,"state_35":true,"state_49":true},{"type":"person","title":"Irene R. Tucker","state":"s","avail":true,"age":31,"date":899596800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_2":true,"state_3":true,"state_4":true,"state_7":true,"state_8":true,"state_10":true,"state_12":true,"state_21":true,"state_23":true,"state_25":true,"state_30":true,"state_32":true,"state_39":true,"state_44":true,"state_50":true},{"type":"person","title":"Jamie T. Reid","avail":true,"age":22,"date":569808000000.0,"state_10":true,"state_15":true,"state_16":true,"state_24":true,"state_32":true,"state_39":true},{"type":"person","title":"Rose Springer","state":"h","avail":true,"age":80,"state_6":true,"state_9":true,"state_19":true,"state_27":true,"state_28":true,"state_35":true,"state_38":true,"state_39":true,"state_40":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Caroline Arnold","avail":true,"age":80,"state_4":true,"state_8":true,"state_10":true,"state_12":true,"state_25":true,"state_41":true,"state_45":true,"state_47":true},{"type":"person","title":"Mary Q. Watson","state":"s","avail":true,"age":56,"state_3":true,"state_5":true,"state_19":true,"state_22":true,"state_30":true,"state_32":true,"state_33":true,"state_34":true,"state_35":true,"state_44":true,"state_49":true,"state_50":true},{"type":"person","title":"Charles Bower","avail":true,"age":42,"date":1082678400000.0,"state_1":true,"state_2":true,"state_4":true,"state_9":true,"state_13":true,"state_18":true,"state_20":true,"state_21":true,"state_30":true,"state_34":true,"state_39":true,"state_43":true,"state_44":true,"state_47":true,"state_50":true},{"type":"person","title":"Chris McLean","avail":true,"age":39,"date":1481587200000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_3":true,"state_6":true,"state_7":true,"state_12":true,"state_13":true,"state_14":true,"state_15":true,"state_19":true,"state_21":true,"state_27":true,"state_29":true,"state_30":true,"state_31":true,"state_32":true,"state_36":true,"state_38":true,"state_39":true,"state_44":true,"state_49":true},{"type":"person","title":"Evan E. Jackson","avail":true,"age":46,"date":1489795200000.0,"state_13":true,"state_23":true,"state_29":true,"state_38":true,"state_43":true,"state_46":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Emily Hemmings","avail":true,"age":59,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_9":true,"state_11":true,"state_17":true,"state_20":true,"state_28":true,"state_31":true,"state_33":true,"state_35":true,"state_36":true,"state_45":true},{"type":"person","title":"Casey Russell","state":"s","avail":true,"age":28,"date":626227200000.0,"state_12":true,"state_13":true,"state_23":true,"state_28":true,"state_32":true,"state_35":true,"state_36":true,"state_37":true,"state_39":true,"state_49":true,"state_50":true},{"type":"person","title":"Isaac C. Bell","state":"s","avail":true,"age":84,"date":1701129600000.0,"state_1":true,"state_10":true,"state_14":true,"state_18":true,"state_32":true,"state_33":true,"state_38":true,"state_39":true,"state_40":true,"state_41":true,"state_47":true},{"type":"person","title":"Brian Wright","state":"s","age":49,"date":322790400000.0,"state_4":true,"state_12":true,"state_14":true,"state_21":true,"state_23":true,"state_29":true,"state_36":true,"state_38":true,"state_43":true,"state_44":true,"state_50":true},{"type":"person","title":"Katherine Howard","state":"s","avail":true,"age":49,"date":105148800000.0,"state_7":true,"state_15":true,"state_20":true,"state_22":true,"state_23":true,"state_27":true,"state_28":true,"state_30":true,"state_32":true,"state_34":true,"state_35":true,"state_36":true,"state_44":true},{"type":"person","title":"Anne Parr","avail":true,"age":47,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_10":true,"state_13":true,"state_19":true,"state_22":true,"state_25":true,"state_26":true,"state_39":true,"state_43":true,"state_44":true},{"type":"person","title":"Chloe O. Paige","avail":true,"age":34,"state_1":true,"state_2":true,"state_7":true,"state_8":true,"state_12":true,"state_15":true,"state_19":true,"state_20":true,"state_21":true,"state_23":true,"state_24":true,"state_30":true,"state_31":true,"state_33":true,"state_34":true,"state_39":true,"state_45":true},{"type":"person","title":"Jan Lewis","avail":true,"age":27,"state_16":true,"state_21":true,"state_23":true,"state_26":true,"state_29":true,"state_33":true,"state_35":true,"state_37":true,"state_38":true,"state_40":true,"state_42":true,"state_44":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Sam S. Watson","avail":true,"age":65,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_2":true,"state_5":true,"state_7":true,"state_9":true,"state_30":true,"state_32":true,"state_39":true,"state_45":true},{"type":"person","title":"Lillian Peake","avail":true,"age":35,"date":1334966400000.0,"state_3":true,"state_14":true,"state_18":true,"state_23":true,"state_26":true,"state_35":true,"state_36":true,"state_37":true,"state_39":true,"state_40":true,"state_42":true,"state_43":true},{"type":"person","title":"Corey D. Payne","avail":true,"age":73,"date":1380499200000.0,"state_4":true,"state_12":true,"state_27":true,"state_34":true,"state_39":true,"state_40":true,"state_42":true}]},{"type":"role","title":"Scald sons","children":[{"type":"person","title":"Sydney H. Lawrence","avail":true,"age":91,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_9":true,"state_13":true,"state_19":true,"state_24":true,"state_25":true,"state_28":true,"state_31":true,"state_37":true,"state_41":true,"state_44":true,"state_46":true},{"type":"person","title":"Alex H. Thomson","avail":true,"age":62,"date":1456012800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_1":true,"state_3":true,"state_8":true,"state_16":true,"state_17":true,"state_18":true,"state_25":true,"state_26":true,"state_29":true,"state_42":true},{"type":"person","title":"Joan Russell","avail":true,"age":46,"state_16":true,"state_17":true,"state_18":true,"state_19":true,"state_27":true,"state_37":true,"state_40":true},{"type":"person","title":"Kelly E. Nash","avail":true,"age":83,"date":1572307200000.0,"state_9":true,"state_11":true,"state_15":true,"state_16":true,"state_20":true,"state_21":true,"state_23":true,"state_24":true,"state_26":true,"state_33":true,"state_36":true,"state_40":true,"state_42":true,"state_43":true,"state_44":true}]},{"type":"role","title":"Ferry lifes","children":[{"type":"person","title":"Colin L. Hill","avail":true,"age":59,"date":595641600000.0,"state_6":true,"state_16":true,"state_17":true,"state_22":true,"state_24":true,"state_26":true,"state_28":true,"state_29":true,"state_33":true,"state_38":true,"state_40":true,"state_45":true},{"type":"person","title":"Matt Sutherland","avail":true,"age":27,"date":342403200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_7":true,"state_10":true,"state_15":true,"state_17":true,"state_23":true,"state_31":true,"state_34":true,"state_43":true,"state_46":true,"state_49":true},{"type":"person","title":"Alex Black","avail":true,"age":52,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_3":true,"state_15":true,"state_21":true,"state_29":true,"state_31":true,"state_32":true,"state_35":true,"state_39":true,"state_40":true,"state_49":true},{"type":"person","title":"Casey C. Hughes","state":"h","avail":true,"age":64,"date":801619200000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_19":true,"state_20":true,"state_24":true,"state_32":true,"state_33":true,"state_34":true,"state_40":true,"state_41":true,"state_47":true,"state_49":true},{"type":"person","title":"Kelly Lyman","avail":true,"age":58,"date":1265932800000.0,"state_5":true,"state_8":true,"state_10":true,"state_13":true,"state_14":true,"state_17":true,"state_20":true,"state_21":true,"state_25":true,"state_38":true,"state_39":true,"state_43":true,"state_48":true},{"type":"person","title":"Victoria S. Allan","avail":true,"age":35,"date":603763200000.0,"state_11":true,"state_14":true,"state_16":true,"state_18":true,"state_20":true,"state_21":true,"state_23":true,"state_25":true,"state_29":true,"state_30":true,"state_38":true,"state_46":true},{"type":"person","title":"Felicity McGrath","avail":true,"age":69,"date":868406400000.0,"state_5":true,"state_11":true,"state_15":true,"state_23":true,"state_35":true,"state_36":true,"state_47":true},{"type":"person","title":"Dylan Wallace","avail":true,"age":63,"state_1":true,"state_4":true,"state_7":true,"state_9":true,"state_19":true,"state_24":true,"state_31":true,"state_43":true,"state_47":true},{"type":"person","title":"Benjamin W. Bell","state":"h","avail":true,"age":72,"date":691200000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_7":true,"state_29":true,"state_35":true,"state_37":true,"state_40":true,"state_46":true,"state_50":true},{"type":"person","title":"Andy L. Clarkson","age":22,"date":1369094400000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_13":true,"state_21":true,"state_23":true,"state_28":true,"state_30":true,"state_41":true},{"type":"person","title":"Charles Slater","avail":true,"age":54,"date":1568505600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_3":true,"state_4":true,"state_10":true,"state_15":true,"state_16":true,"state_39":true,"state_41":true,"state_45":true,"state_46":true,"state_47":true},{"type":"person","title":"Emily L. Poole","state":"h","avail":true,"age":66,"date":378000000000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_3":true,"state_4":true,"state_10":true,"state_17":true,"state_23":true,"state_27":true,"state_30":true,"state_40":true,"state_43":true,"state_47":true,"state_48":true},{"type":"person","title":"Alexander Slater","avail":true,"age":84,"date":1100044800000.0,"state_14":true,"state_19":true,"state_24":true,"state_38":true,"state_39":true,"state_41":true,"state_43":true,"state_47":true},{"type":"person","title":"Evan N. Thomson","avail":true,"age":24,"date":846633600000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_2":true,"state_12":true,"state_13":true,"state_16":true,"state_27":true,"state_29":true,"state_32":true},{"type":"person","title":"Frank Young","avail":true,"age":51,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_6":true,"state_14":true,"state_22":true,"state_28":true,"state_33":true,"state_37":true,"state_39":true,"state_40":true,"state_41":true,"state_47":true},{"type":"person","title":"Blake J. Underwood","state":"s","avail":true,"age":82,"state_3":true,"state_18":true,"state_29":true,"state_36":true,"state_47":true,"state_49":true},{"type":"person","title":"Phoenix A. Arnold","age":84,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_12":true,"state_19":true,"state_32":true,"state_38":true,"state_39":true,"state_43":true},{"type":"person","title":"Emily F. Langdon","avail":true,"age":36,"state_2":true,"state_6":true,"state_13":true,"state_18":true,"state_21":true,"state_23":true,"state_25":true,"state_27":true,"state_35":true,"state_39":true,"state_43":true,"state_44":true},{"type":"person","title":"Emma X. Glover","avail":true,"age":40,"date":370224000000.0,"state_5":true,"state_16":true,"state_27":true,"state_36":true,"state_41":true,"state_47":true},{"type":"person","title":"Thomas Mills","avail":true,"age":82,"date":1087344000000.0,"state_1":true,"state_3":true,"state_7":true,"state_13":true,"state_21":true,"state_29":true}]},{"type":"role","title":"Chat reindeers","children":[{"type":"person","title":"Claire Cornish","avail":true,"age":74,"state_11":true,"state_15":true,"state_17":true,"state_24":true,"state_27":true,"state_43":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Claire Ogden","avail":true,"age":97,"state_1":true,"state_4":true,"state_12":true,"state_17":true,"state_20":true,"state_39":true,"state_40":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Robert McDonald","state":"h","avail":true,"age":65,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_10":true,"state_22":true,"state_23":true,"state_27":true,"state_42":true,"state_45":true,"state_47":true},{"type":"person","title":"Michael P. Glover","avail":true,"age":26,"date":867196800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_12":true,"state_16":true,"state_20":true,"state_23":true,"state_32":true,"state_44":true,"state_46":true,"state_49":true},{"type":"person","title":"Corey C. Hunter","age":45,"date":1678579200000.0,"state_1":true,"state_7":true,"state_10":true,"state_14":true,"state_18":true,"state_22":true,"state_23":true,"state_25":true,"state_30":true,"state_33":true,"state_35":true,"state_36":true,"state_37":true,"state_40":true,"state_42":true,"state_44":true},{"type":"person","title":"Victor M. Gibson","avail":true,"age":82,"date":1431907200000.0,"state_2":true,"state_3":true,"state_7":true,"state_11":true,"state_12":true,"state_13":true,"state_21":true,"state_26":true,"state_27":true,"state_29":true,"state_30":true,"state_35":true,"state_40":true,"state_42":true,"state_43":true},{"type":"person","title":"Edward Cameron","avail":true,"age":24,"date":803260800000.0,"state_1":true,"state_6":true,"state_17":true,"state_21":true,"state_22":true,"state_23":true,"state_24":true,"state_25":true,"state_36":true,"state_38":true,"state_46":true,"state_47":true,"state_48":true},{"type":"person","title":"Jane Powell","age":80,"state_5":true,"state_7":true,"state_9":true,"state_10":true,"state_14":true,"state_16":true,"state_17":true,"state_20":true,"state_22":true,"state_26":true,"state_38":true,"state_40":true,"state_46":true,"state_49":true}]}]},{"type":"department","title":"Dept. for Investments and Supports","children":[{"type":"role","title":"Scarp listens","children":[{"type":"person","title":"Sarah Ross","avail":true,"age":66,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_2":true,"state_7":true,"state_11":true,"state_12":true,"state_14":true,"state_23":true,"state_25":true,"state_30":true,"state_37":true,"state_40":true,"state_49":true},{"type":"person","title":"Bobbie Walker","state":"h","avail":true,"age":44,"state_3":true,"state_5":true,"state_7":true,"state_19":true,"state_22":true,"state_24":true,"state_26":true,"state_30":true,"state_31":true,"state_33":true,"state_35":true,"state_38":true,"state_45":true,"state_46":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Angela T. Carr","avail":true,"age":88,"state_2":true,"state_7":true,"state_28":true,"state_30":true,"state_32":true,"state_35":true,"state_36":true},{"type":"person","title":"Harry Springer","state":"s","avail":true,"age":71,"date":1596067200000.0,"state_12":true,"state_18":true,"state_28":true,"state_32":true,"state_36":true,"state_37":true,"state_38":true,"state_40":true,"state_47":true},{"type":"person","title":"Carl McDonald","avail":true,"age":55,"date":1501372800000.0,"state_1":true,"state_10":true,"state_12":true,"state_23":true,"state_28":true,"state_44":true},{"type":"person","title":"Alan Z. Davies","avail":true,"age":88,"date":759801600000.0,"state_1":true,"state_4":true,"state_6":true,"state_10":true,"state_12":true,"state_13":true,"state_18":true,"state_20":true,"state_21":true,"state_22":true,"state_25":true,"state_28":true,"state_32":true,"state_40":true,"state_41":true},{"type":"person","title":"Dylan Cameron","avail":true,"age":48,"date":787449600000.0,"state_6":true,"state_12":true,"state_13":true,"state_25":true,"state_27":true,"state_36":true,"state_39":true,"state_40":true,"state_42":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Sydney Z. Fraser","state":"h","avail":true,"age":32,"date":822528000000.0,"state_3":true,"state_5":true,"state_10":true,"state_14":true,"state_17":true,"state_19":true,"state_24":true,"state_30":true,"state_33":true,"state_43":true,"state_48":true},{"type":"person","title":"Claire W. Mackenzie","state":"h","avail":true,"age":76,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_8":true,"state_9":true,"state_12":true,"state_16":true,"state_18":true,"state_20":true,"state_28":true,"state_30":true,"state_32":true,"state_39":true,"state_42":true,"state_46":true},{"type":"person","title":"Claire M. Pullman","state":"s","avail":true,"age":54,"date":590284800000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_3":true,"state_10":true,"state_13":true,"state_15":true,"state_20":true,"state_27":true,"state_39":true,"state_44":true},{"type":"person","title":"Isaac Lambert","state":"h","avail":true,"age":22,"date":1359590400000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_8":true,"state_14":true,"state_29":true,"state_37":true,"state_43":true,"state_44":true,"state_48":true},{"type":"person","title":"Joe Paterson","avail":true,"age":77,"date":1560988800000.0,"state_13":true,"state_16":true,"state_28":true,"state_32":true,"state_36":true,"state_41":true,"state_43":true,"state_49":true},{"type":"person","title":"Irene Gill","state":"h","avail":true,"age":35,"state_27":true,"state_47":true}]},{"type":"role","title":"Invite packs","children":[{"type":"person","title":"Stephanie Vaughan","state":"s","avail":true,"age":73,"date":758073600000.0,"state_17":true,"state_20":true,"state_22":true,"state_23":true,"state_25":true,"state_28":true,"state_29":true,"state_30":true,"state_33":true,"state_40":true,"state_41":true,"state_49":true},{"type":"person","title":"Edward James","state":"h","avail":true,"age":70,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_3":true,"state_4":true,"state_11":true,"state_13":true,"state_14":true,"state_15":true,"state_16":true,"state_42":true},{"type":"person","title":"Connor Dyer","avail":true,"age":29,"date":602985600000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_1":true,"state_9":true,"state_15":true,"state_17":true,"state_21":true,"state_22":true,"state_26":true,"state_34":true,"state_38":true,"state_39":true,"state_40":true,"state_47":true},{"type":"person","title":"Rachel Hill","state":"s","avail":true,"age":41,"date":1020902400000.0,"state_5":true,"state_9":true,"state_10":true,"state_16":true,"state_23":true,"state_25":true,"state_36":true,"state_37":true,"state_38":true,"state_41":true,"state_44":true,"state_45":true,"state_49":true},{"type":"person","title":"Sebastian Mathis","avail":true,"age":23,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_4":true,"state_10":true,"state_15":true,"state_20":true,"state_22":true,"state_33":true,"state_36":true,"state_42":true,"state_44":true,"state_46":true,"state_49":true},{"type":"person","title":"Jean Robertson","avail":true,"age":70,"date":631929600000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_1":true,"state_9":true,"state_27":true,"state_34":true},{"type":"person","title":"Colin Taylor","state":"s","age":22,"date":879984000000.0,"state_7":true,"state_10":true,"state_23":true,"state_26":true,"state_32":true,"state_42":true,"state_44":true,"state_45":true,"state_46":true},{"type":"person","title":"Faith W. Glover","avail":true,"age":92,"state_1":true,"state_8":true,"state_19":true,"state_20":true,"state_34":true,"state_40":true,"state_42":true,"state_43":true},{"type":"person","title":"Joshua F. Fisher","avail":true,"age":98,"state_5":true,"state_11":true,"state_13":true,"state_19":true,"state_24":true,"state_33":true,"state_40":true,"state_42":true,"state_44":true,"state_46":true}]},{"type":"role","title":"Connect tears","children":[{"type":"person","title":"Tracey C. Stewart","avail":true,"age":83,"state_2":true,"state_25":true,"state_26":true,"state_30":true,"state_34":true,"state_41":true,"state_42":true,"state_45":true,"state_46":true,"state_48":true,"state_49":true},{"type":"person","title":"Harry Welch","state":"s","avail":true,"age":90,"state_15":true,"state_16":true,"state_24":true,"state_29":true,"state_30":true,"state_36":true,"state_37":true,"state_42":true,"state_44":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Fiona Scott","avail":true,"age":90,"date":1051315200000.0,"state_19":true,"state_32":true,"state_41":true,"state_47":true,"state_50":true},{"type":"person","title":"Jean G. Cornish","avail":true,"age":61,"date":34300800000.0,"state_3":true,"state_5":true,"state_6":true,"state_7":true,"state_8":true,"state_18":true,"state_23":true,"state_35":true,"state_42":true},{"type":"person","title":"Blake Metcalfe","state":"h","avail":true,"age":30,"date":590198400000.0,"state_1":true,"state_5":true,"state_10":true,"state_16":true,"state_25":true,"state_32":true,"state_33":true,"state_34":true,"state_41":true,"state_45":true,"state_48":true},{"type":"person","title":"Joe Grant","avail":true,"age":36,"date":23760000000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_8":true,"state_22":true,"state_24":true,"state_29":true,"state_30":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Samantha X. Underwood","state":"h","avail":true,"age":65,"date":505958400000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_3":true,"state_8":true,"state_19":true,"state_21":true,"state_22":true,"state_28":true,"state_32":true,"state_37":true,"state_38":true,"state_47":true},{"type":"person","title":"Kevin T. Hemmings","avail":true,"age":24,"date":4924800000.0,"state_6":true,"state_15":true,"state_16":true,"state_18":true,"state_23":true,"state_31":true,"state_35":true,"state_40":true,"state_41":true,"state_48":true},{"type":"person","title":"Luke Welch","avail":true,"age":50,"state_1":true,"state_2":true,"state_5":true,"state_12":true,"state_14":true,"state_15":true,"state_16":true,"state_24":true,"state_33":true,"state_36":true,"state_42":true,"state_43":true,"state_45":true,"state_47":true},{"type":"person","title":"Nathan S. Graham","state":"s","avail":true,"age":25,"date":957830400000.0,"state_8":true,"state_9":true,"state_12":true,"state_16":true,"state_17":true,"state_19":true,"state_21":true,"state_24":true,"state_30":true,"state_44":true},{"type":"person","title":"Theresa Nash","avail":true,"age":70,"date":564192000000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_4":true,"state_22":true,"state_23":true,"state_26":true,"state_40":true,"state_43":true,"state_44":true,"state_45":true,"state_47":true},{"type":"person","title":"Eddie N. Vaughan","avail":true,"age":59,"date":1237420800000.0,"state_6":true,"state_17":true,"state_46":true,"state_48":true},{"type":"person","title":"Emma Paterson","avail":true,"age":87,"date":1460764800000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_3":true,"state_10":true,"state_23":true,"state_30":true,"state_42":true},{"type":"person","title":"Rose Dowd","state":"s","avail":true,"age":54,"date":1574812800000.0,"state_1":true,"state_4":true,"state_17":true,"state_21":true,"state_28":true,"state_29":true,"state_32":true,"state_39":true,"state_46":true},{"type":"person","title":"Taylor I. Pullman","avail":true,"age":42,"date":289440000000.0,"state_2":true,"state_3":true,"state_7":true,"state_31":true,"state_33":true,"state_38":true,"state_41":true,"state_43":true,"state_48":true,"state_49":true},{"type":"person","title":"Bernadette Metcalfe","avail":true,"age":26,"state_1":true,"state_10":true,"state_12":true,"state_13":true,"state_15":true,"state_22":true,"state_33":true,"state_47":true}]},{"type":"role","title":"Sleep beings","children":[{"type":"person","title":"Charles Davies","age":85,"state_2":true,"state_3":true,"state_6":true,"state_8":true,"state_14":true,"state_19":true,"state_23":true,"state_36":true,"state_37":true,"state_41":true,"state_43":true,"state_49":true},{"type":"person","title":"Toby Payne","age":29,"date":757296000000.0,"state_24":true,"state_33":true,"state_37":true,"state_50":true},{"type":"person","title":"Taylor E. Paige","avail":true,"age":57,"date":1179619200000.0,"state_2":true,"state_3":true,"state_4":true,"state_6":true,"state_19":true,"state_23":true,"state_27":true,"state_33":true},{"type":"person","title":"Toby X. Parsons","state":"h","avail":true,"age":64,"date":174096000000.0,"state_3":true,"state_10":true,"state_12":true,"state_22":true,"state_26":true,"state_29":true,"state_32":true,"state_39":true,"state_41":true,"state_43":true},{"type":"person","title":"Bobbie R. Quinn","avail":true,"age":68,"date":973641600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_6":true,"state_7":true,"state_9":true,"state_11":true,"state_22":true,"state_26":true,"state_34":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Katherine Oliver","avail":true,"age":73,"state_10":true,"state_14":true,"state_17":true,"state_19":true,"state_20":true,"state_25":true,"state_26":true,"state_30":true,"state_45":true,"state_46":true,"state_49":true},{"type":"person","title":"Frank R. Morrison","avail":true,"age":91,"date":768268800000.0,"state_1":true,"state_10":true,"state_15":true,"state_32":true,"state_45":true},{"type":"person","title":"Sarah Hodges","avail":true,"age":30,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_1":true,"state_2":true,"state_11":true,"state_15":true,"state_16":true,"state_18":true,"state_30":true,"state_31":true,"state_42":true,"state_49":true},{"type":"person","title":"Colin Churchill","state":"h","avail":true,"age":28,"date":996451200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_15":true,"state_18":true,"state_23":true,"state_26":true,"state_27":true,"state_31":true,"state_33":true,"state_41":true,"state_42":true,"state_43":true,"state_46":true,"state_48":true},{"type":"person","title":"Alexander Y. Vaughan","age":84,"date":211161600000.0,"state_15":true,"state_24":true,"state_28":true,"state_32":true,"state_33":true,"state_36":true,"state_37":true,"state_42":true,"state_48":true,"state_50":true},{"type":"person","title":"Alison Lyman","state":"s","avail":true,"age":92,"date":1153526400000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_1":true,"state_3":true,"state_8":true,"state_9":true,"state_10":true,"state_11":true,"state_12":true,"state_16":true,"state_30":true,"state_32":true,"state_37":true,"state_41":true,"state_50":true},{"type":"person","title":"Eddie Turner","avail":true,"age":79,"date":1268697600000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_3":true,"state_8":true,"state_9":true,"state_10":true,"state_14":true,"state_17":true,"state_23":true,"state_28":true,"state_30":true,"state_32":true,"state_41":true},{"type":"person","title":"Lillian J. Fisher","avail":true,"age":59,"date":960595200000.0,"state_1":true,"state_3":true,"state_5":true,"state_8":true,"state_21":true,"state_29":true,"state_35":true,"state_47":true,"state_48":true},{"type":"person","title":"Ella Ball","state":"s","age":40,"state_2":true,"state_3":true,"state_4":true,"state_6":true,"state_12":true,"state_13":true,"state_14":true,"state_15":true,"state_17":true,"state_18":true,"state_23":true,"state_24":true,"state_25":true,"state_31":true,"state_34":true,"state_47":true},{"type":"person","title":"Pat Hardacre","state":"h","age":45,"state_3":true,"state_6":true,"state_9":true,"state_13":true,"state_15":true,"state_24":true,"state_30":true,"state_36":true,"state_41":true,"state_47":true,"state_49":true},{"type":"person","title":"Cameron Greene","avail":true,"age":77,"date":1170288000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_3":true,"state_6":true,"state_21":true,"state_29":true,"state_34":true,"state_37":true,"state_46":true,"state_48":true},{"type":"person","title":"Charlie V. Bell","state":"h","avail":true,"age":51,"state_1":true,"state_3":true,"state_4":true,"state_6":true,"state_12":true,"state_18":true,"state_24":true,"state_32":true,"state_40":true,"state_43":true,"state_45":true,"state_47":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Stephanie Hill","avail":true,"age":57,"state_4":true,"state_10":true,"state_11":true,"state_26":true,"state_29":true,"state_30":true,"state_34":true,"state_35":true,"state_44":true,"state_49":true,"state_50":true},{"type":"person","title":"Natalie P. Dickens","avail":true,"age":39,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_6":true,"state_8":true,"state_12":true,"state_18":true,"state_21":true,"state_31":true,"state_35":true,"state_36":true,"state_39":true,"state_42":true,"state_45":true,"state_46":true}]},{"type":"role","title":"Shock routines","children":[{"type":"person","title":"Jack L. Walsh","state":"s","age":87,"date":1204675200000.0,"state_1":true,"state_14":true,"state_18":true,"state_25":true,"state_29":true,"state_30":true,"state_35":true,"state_40":true,"state_47":true,"state_49":true}]},{"type":"role","title":"Strew visuals"},{"type":"role","title":"Cause uncles","children":[{"type":"person","title":"Lisa C. Morrison","avail":true,"age":84,"state_2":true,"state_5":true,"state_9":true,"state_10":true,"state_22":true,"state_25":true,"state_28":true,"state_30":true,"state_38":true,"state_39":true,"state_50":true},{"type":"person","title":"Chris S. Coleman","state":"h","avail":true,"age":54,"state_1":true,"state_4":true,"state_12":true,"state_19":true,"state_26":true,"state_34":true,"state_40":true,"state_42":true,"state_44":true,"state_46":true,"state_48":true},{"type":"person","title":"Carolyn Black","age":68,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_1":true,"state_2":true,"state_3":true,"state_9":true,"state_16":true,"state_29":true,"state_30":true,"state_31":true,"state_42":true,"state_48":true},{"type":"person","title":"Amanda O. Lambert","avail":true,"age":56,"date":511488000000.0,"state_8":true,"state_29":true,"state_33":true,"state_49":true},{"type":"person","title":"Alison S. Edmunds","avail":true,"age":84,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_6":true,"state_7":true,"state_11":true,"state_28":true,"state_31":true,"state_36":true,"state_41":true,"state_45":true},{"type":"person","title":"Eric Johnston","avail":true,"age":64,"date":1327104000000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_9":true,"state_20":true,"state_26":true,"state_38":true,"state_46":true,"state_50":true},{"type":"person","title":"Felicity Y. Bailey","state":"h","avail":true,"age":45,"date":723859200000.0,"state_5":true,"state_8":true,"state_13":true,"state_30":true,"state_31":true,"state_32":true,"state_37":true,"state_40":true,"state_42":true},{"type":"person","title":"Blake V. Rees","avail":true,"age":77,"date":467510400000.0,"state_1":true,"state_2":true,"state_10":true,"state_15":true,"state_19":true,"state_29":true,"state_30":true,"state_32":true,"state_45":true},{"type":"person","title":"Kyle Morrison","state":"s","avail":true,"age":76,"state_7":true,"state_10":true,"state_11":true,"state_17":true,"state_24":true,"state_25":true,"state_28":true,"state_31":true,"state_35":true,"state_40":true,"state_43":true},{"type":"person","title":"Bobbie Wilson","avail":true,"age":32,"state_2":true,"state_3":true,"state_13":true,"state_15":true,"state_20":true,"state_27":true,"state_28":true,"state_29":true,"state_30":true,"state_31":true,"state_34":true,"state_35":true,"state_47":true},{"type":"person","title":"Jesse Carr","avail":true,"age":51,"state_1":true,"state_10":true,"state_15":true,"state_17":true,"state_27":true,"state_28":true,"state_30":true,"state_36":true,"state_41":true,"state_49":true},{"type":"person","title":"Jean N. Churchill","state":"h","avail":true,"age":65,"date":1503532800000.0,"state_1":true,"state_3":true,"state_8":true,"state_16":true,"state_20":true,"state_21":true,"state_22":true,"state_25":true,"state_48":true},{"type":"person","title":"Pippa King","state":"h","avail":true,"age":93,"date":1192492800000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_2":true,"state_18":true,"state_29":true,"state_36":true,"state_38":true,"state_45":true,"state_48":true},{"type":"person","title":"Sam U. Glover","avail":true,"age":24,"state_3":true,"state_4":true,"state_13":true,"state_16":true,"state_19":true,"state_21":true,"state_25":true,"state_27":true,"state_29":true,"state_37":true,"state_38":true,"state_39":true,"state_44":true,"state_47":true,"state_50":true},{"type":"person","title":"Lou McLean","avail":true,"age":60,"date":232848000000.0,"state_1":true,"state_3":true,"state_4":true,"state_8":true,"state_9":true,"state_12":true,"state_28":true,"state_37":true,"state_42":true,"state_44":true,"state_50":true}]},{"type":"role","title":"Crib indications","children":[{"type":"person","title":"Phil U. Ince","avail":true,"age":63,"date":366422400000.0,"state_2":true,"state_11":true,"state_12":true,"state_17":true,"state_18":true,"state_23":true,"state_32":true,"state_33":true,"state_36":true,"state_41":true}]},{"type":"role","title":"Finish designers","children":[{"type":"person","title":"Phoenix Arnold","state":"s","avail":true,"age":76,"date":535248000000.0,"state_5":true,"state_9":true,"state_11":true,"state_20":true,"state_25":true,"state_27":true,"state_28":true,"state_37":true,"state_42":true,"state_47":true}]},{"type":"role","title":"Diminish processes","children":[{"type":"person","title":"Joshua V. Dyer","state":"h","avail":true,"age":93,"date":386467200000.0,"state_3":true,"state_5":true,"state_7":true,"state_8":true,"state_10":true,"state_11":true,"state_12":true,"state_16":true,"state_17":true,"state_19":true,"state_20":true,"state_21":true,"state_23":true,"state_27":true,"state_33":true,"state_36":true,"state_37":true,"state_40":true,"state_49":true,"state_50":true},{"type":"person","title":"Lou B. Black","avail":true,"age":27,"date":1721260800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_2":true,"state_8":true,"state_9":true,"state_13":true,"state_20":true,"state_23":true,"state_27":true,"state_38":true,"state_48":true},{"type":"person","title":"Kimberly I. Taylor","avail":true,"age":93,"date":1530748800000.0,"state_2":true,"state_6":true,"state_12":true,"state_16":true,"state_21":true,"state_32":true,"state_36":true,"state_38":true,"state_40":true,"state_45":true,"state_50":true},{"type":"person","title":"Andrew Piper","avail":true,"age":81,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_11":true,"state_22":true,"state_24":true,"state_32":true,"state_36":true,"state_44":true},{"type":"person","title":"Zoe Carr","age":88,"date":1155254400000.0,"state_7":true,"state_11":true,"state_15":true,"state_16":true,"state_18":true,"state_20":true,"state_22":true,"state_23":true,"state_37":true,"state_40":true,"state_42":true},{"type":"person","title":"Penelope Cornish","avail":true,"age":77,"date":1317168000000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_1":true,"state_2":true,"state_4":true,"state_10":true,"state_14":true,"state_17":true,"state_28":true,"state_44":true,"state_48":true},{"type":"person","title":"Irene Welch","state":"h","avail":true,"age":78,"date":1654646400000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_7":true,"state_16":true,"state_21":true,"state_30":true,"state_39":true},{"type":"person","title":"Chloe V. Lawrence","state":"h","avail":true,"age":78,"state_1":true,"state_5":true,"state_9":true,"state_10":true,"state_13":true,"state_15":true,"state_27":true,"state_28":true,"state_30":true,"state_36":true},{"type":"person","title":"Frank Smith","avail":true,"age":29,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_7":true,"state_9":true,"state_12":true,"state_17":true,"state_34":true,"state_35":true,"state_41":true,"state_45":true},{"type":"person","title":"Rachel Fisher","avail":true,"age":92,"date":36201600000.0,"state_7":true,"state_10":true,"state_14":true,"state_16":true,"state_20":true,"state_26":true,"state_31":true,"state_47":true},{"type":"person","title":"Victoria Clark","state":"h","avail":true,"age":54,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_7":true,"state_9":true,"state_10":true,"state_16":true,"state_19":true,"state_25":true,"state_26":true,"state_27":true,"state_39":true,"state_40":true,"state_44":true},{"type":"person","title":"Kelly Dickens","avail":true,"age":83,"date":381456000000.0,"state_9":true,"state_14":true,"state_17":true,"state_19":true,"state_23":true,"state_32":true,"state_38":true,"state_41":true,"state_46":true},{"type":"person","title":"Lily Short","state":"s","avail":true,"age":77,"date":436060800000.0,"state_2":true,"state_4":true,"state_10":true,"state_22":true,"state_27":true,"state_36":true,"state_41":true,"state_42":true},{"type":"person","title":"Casey Payne","avail":true,"age":54,"date":1581638400000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_2":true,"state_4":true,"state_9":true,"state_10":true,"state_14":true,"state_17":true,"state_18":true,"state_25":true,"state_27":true,"state_31":true,"state_34":true,"state_38":true,"state_44":true},{"type":"person","title":"Nicholas Rampling","avail":true,"age":27,"date":555984000000.0,"state_1":true,"state_12":true,"state_13":true,"state_24":true,"state_30":true,"state_33":true,"state_37":true,"state_41":true,"state_48":true},{"type":"person","title":"Wendy A. Hudson","avail":true,"age":29,"date":1133222400000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_18":true,"state_23":true,"state_27":true,"state_33":true,"state_35":true,"state_49":true}]},{"type":"role","title":"Glow repeats","children":[{"type":"person","title":"Frank Davies","state":"s","avail":true,"age":61,"date":958953600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_6":true,"state_30":true,"state_31":true,"state_34":true,"state_36":true,"state_46":true},{"type":"person","title":"Hannah Avery","avail":true,"age":28,"date":694310400000.0,"state_3":true,"state_14":true,"state_19":true,"state_21":true,"state_25":true,"state_32":true,"state_39":true},{"type":"person","title":"Lisa V. Poole","avail":true,"age":47,"date":378086400000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_2":true,"state_3":true,"state_6":true,"state_7":true,"state_17":true,"state_19":true,"state_20":true,"state_22":true,"state_28":true,"state_29":true,"state_30":true,"state_31":true,"state_36":true,"state_39":true,"state_44":true,"state_48":true},{"type":"person","title":"Alex Dowd","state":"s","avail":true,"age":73,"state_6":true,"state_13":true,"state_14":true,"state_20":true,"state_24":true,"state_29":true,"state_34":true,"state_42":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Liam Blake","avail":true,"age":34,"date":615859200000.0,"state_6":true,"state_16":true,"state_23":true,"state_24":true,"state_25":true,"state_32":true,"state_34":true,"state_41":true,"state_46":true,"state_48":true,"state_49":true},{"type":"person","title":"Julia Ogden","avail":true,"age":60,"date":832118400000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_8":true,"state_13":true,"state_23":true,"state_32":true,"state_35":true,"state_37":true,"state_39":true,"state_50":true},{"type":"person","title":"Bella Bailey","age":51,"state_7":true,"state_19":true,"state_21":true,"state_27":true,"state_32":true,"state_41":true,"state_42":true,"state_43":true,"state_45":true,"state_48":true},{"type":"person","title":"Madeleine Z. Russell","state":"s","avail":true,"age":90,"date":1571788800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_4":true,"state_5":true,"state_7":true,"state_9":true,"state_11":true,"state_15":true,"state_17":true,"state_18":true,"state_22":true,"state_24":true,"state_31":true,"state_36":true,"state_40":true,"state_42":true,"state_45":true},{"type":"person","title":"Eddie F. Gill","state":"h","avail":true,"age":71,"state_8":true,"state_9":true,"state_13":true,"state_14":true,"state_19":true,"state_31":true,"state_38":true,"state_44":true,"state_48":true},{"type":"person","title":"Samantha Piper","avail":true,"age":52,"state_5":true,"state_7":true,"state_8":true,"state_16":true,"state_41":true,"state_45":true}]},{"type":"role","title":"Apologize burns"}]},{"type":"department","title":"Dept. for Distributions and Mights","children":[{"type":"role","title":"Patch internationals","children":[{"type":"person","title":"Charles S. Duncan","avail":true,"age":76,"date":188179200000.0,"state_2":true,"state_6":true,"state_8":true,"state_13":true,"state_16":true,"state_17":true,"state_18":true,"state_29":true,"state_37":true,"state_42":true},{"type":"person","title":"Penelope Sharp","state":"s","avail":true,"age":54,"date":1422748800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_11":true,"state_24":true,"state_25":true,"state_26":true,"state_28":true,"state_29":true,"state_32":true,"state_45":true},{"type":"person","title":"Sean X. Berry","avail":true,"age":28,"date":970790400000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_7":true,"state_13":true,"state_16":true,"state_21":true,"state_24":true,"state_37":true},{"type":"person","title":"Samantha Henderson","state":"h","avail":true,"age":32,"state_9":true,"state_36":true},{"type":"person","title":"Jamie W. Hart","state":"h","avail":true,"age":83,"state_5":true,"state_7":true,"state_21":true,"state_23":true,"state_27":true,"state_28":true,"state_31":true,"state_36":true,"state_37":true,"state_44":true,"state_48":true,"state_50":true},{"type":"person","title":"Sonia P. Wilson","avail":true,"age":82,"state_2":true,"state_3":true,"state_10":true,"state_12":true,"state_18":true,"state_23":true,"state_24":true,"state_26":true,"state_34":true},{"type":"person","title":"Chloe N. Randall","avail":true,"age":63,"state_9":true,"state_10":true,"state_20":true,"state_29":true,"state_34":true,"state_43":true,"state_47":true},{"type":"person","title":"Pat Arnold","avail":true,"age":54,"date":1553731200000.0,"state_7":true,"state_13":true,"state_22":true,"state_23":true,"state_24":true,"state_32":true,"state_35":true,"state_49":true,"state_50":true},{"type":"person","title":"Phoenix V. Langdon","state":"h","avail":true,"age":57,"date":1496620800000.0,"state_12":true,"state_14":true,"state_15":true,"state_17":true,"state_18":true,"state_19":true,"state_27":true,"state_30":true,"state_32":true,"state_36":true,"state_39":true}]},{"type":"role","title":"Depend peoples","children":[{"type":"person","title":"Olivia Hemmings","avail":true,"age":25,"state_2":true,"state_13":true,"state_19":true,"state_23":true,"state_24":true,"state_26":true,"state_43":true,"state_44":true,"state_46":true},{"type":"person","title":"Piers Thomson","avail":true,"age":86,"date":705888000000.0,"state_5":true,"state_6":true,"state_27":true,"state_32":true,"state_39":true,"state_48":true},{"type":"person","title":"Charlie Murray","avail":true,"age":33,"state_1":true,"state_10":true,"state_17":true,"state_21":true,"state_25":true,"state_29":true,"state_32":true,"state_36":true,"state_41":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Justin Q. Wallace","age":91,"state_7":true,"state_15":true,"state_17":true,"state_18":true,"state_21":true,"state_22":true,"state_27":true,"state_37":true,"state_45":true}]},{"type":"role","title":"Disuse mixes","children":[{"type":"person","title":"Jessica Jackson","avail":true,"age":90,"date":1340409600000.0,"state_8":true,"state_10":true,"state_14":true,"state_17":true,"state_33":true,"state_39":true,"state_43":true,"state_45":true},{"type":"person","title":"John I. Ball","state":"h","avail":true,"age":72,"date":1059782400000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_5":true,"state_6":true,"state_13":true,"state_14":true,"state_18":true,"state_23":true,"state_35":true,"state_38":true,"state_40":true,"state_50":true},{"type":"person","title":"Alex Forsyth","avail":true,"age":39,"state_9":true,"state_14":true,"state_18":true,"state_25":true,"state_26":true,"state_28":true,"state_33":true,"state_35":true,"state_47":true},{"type":"person","title":"Tracey Welch","avail":true,"age":66,"date":1585094400000.0,"state_2":true,"state_4":true,"state_8":true,"state_9":true,"state_10":true,"state_13":true,"state_36":true,"state_39":true,"state_48":true,"state_49":true},{"type":"person","title":"Luke Churchill","avail":true,"age":85,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_3":true,"state_8":true,"state_10":true,"state_20":true,"state_24":true,"state_27":true,"state_29":true,"state_35":true,"state_39":true,"state_40":true,"state_43":true},{"type":"person","title":"Rachel N. Fraser","age":61,"date":415843200000.0,"state_2":true,"state_9":true,"state_12":true,"state_13":true,"state_23":true,"state_24":true,"state_29":true,"state_33":true,"state_39":true,"state_40":true,"state_42":true,"state_44":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Sonia K. Poole","avail":true,"age":26,"date":614131200000.0,"state_14":true,"state_17":true,"state_19":true,"state_26":true,"state_27":true,"state_32":true,"state_35":true,"state_39":true,"state_40":true,"state_41":true,"state_43":true},{"type":"person","title":"Lou Lyman","avail":true,"age":26,"date":1466035200000.0,"state_10":true,"state_17":true,"state_20":true,"state_21":true,"state_26":true,"state_31":true,"state_43":true},{"type":"person","title":"Tim Lambert","avail":true,"age":62,"date":169862400000.0,"state_5":true,"state_6":true,"state_26":true,"state_36":true,"state_39":true,"state_40":true,"state_42":true},{"type":"person","title":"Julian Parsons","state":"h","avail":true,"age":97,"state_2":true,"state_4":true,"state_11":true,"state_17":true,"state_19":true,"state_21":true,"state_25":true,"state_29":true,"state_36":true,"state_37":true,"state_42":true,"state_44":true,"state_45":true,"state_48":true},{"type":"person","title":"Jayden S. Powell","state":"s","avail":true,"age":29,"date":361584000000.0,"state_9":true,"state_11":true,"state_16":true,"state_37":true,"state_48":true},{"type":"person","title":"Lauren Clark","avail":true,"age":60,"state_1":true,"state_5":true,"state_11":true,"state_14":true,"state_25":true,"state_26":true,"state_35":true,"state_39":true,"state_40":true,"state_41":true,"state_43":true},{"type":"person","title":"Andrew L. Nolan","avail":true,"age":84,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_2":true,"state_8":true,"state_10":true,"state_16":true,"state_17":true,"state_23":true,"state_24":true,"state_32":true,"state_39":true,"state_42":true,"state_47":true,"state_48":true},{"type":"person","title":"Ella Martin","avail":true,"age":48,"date":1182124800000.0,"state_9":true,"state_15":true,"state_16":true,"state_20":true,"state_25":true,"state_26":true,"state_27":true,"state_28":true,"state_29":true,"state_36":true,"state_39":true,"state_42":true},{"type":"person","title":"Phoenix Lawrence","avail":true,"age":62,"date":328233600000.0,"state_2":true,"state_3":true,"state_4":true,"state_8":true,"state_12":true,"state_15":true,"state_18":true,"state_23":true,"state_26":true,"state_33":true,"state_34":true,"state_39":true,"state_41":true,"state_42":true,"state_45":true,"state_47":true},{"type":"person","title":"Joan May","state":"s","avail":true,"age":47,"date":1373846400000.0,"state_1":true,"state_5":true,"state_6":true,"state_14":true,"state_15":true,"state_17":true,"state_28":true,"state_29":true,"state_32":true,"state_36":true,"state_37":true,"state_43":true,"state_44":true,"state_45":true,"state_48":true},{"type":"person","title":"Ruth Vance","avail":true,"age":97,"date":873244800000.0,"state_3":true,"state_4":true,"state_8":true,"state_11":true,"state_13":true,"state_31":true,"state_33":true,"state_35":true,"state_39":true,"state_50":true},{"type":"person","title":"Amelia S. Terry","avail":true,"age":41,"date":328838400000.0,"state_2":true,"state_12":true,"state_15":true,"state_20":true,"state_31":true,"state_35":true,"state_39":true,"state_42":true}]},{"type":"role","title":"Conclude calendars","children":[{"type":"person","title":"Emma V. Rampling","avail":true,"age":83,"date":434764800000.0,"state_6":true,"state_12":true,"state_15":true,"state_20":true,"state_37":true,"state_48":true},{"type":"person","title":"David G. Cameron","age":46,"date":740016000000.0,"state_1":true,"state_7":true,"state_11":true,"state_21":true,"state_24":true,"state_36":true,"state_40":true,"state_42":true,"state_48":true},{"type":"person","title":"Bernadette Abraham","avail":true,"age":67,"state_2":true,"state_8":true,"state_11":true,"state_12":true,"state_16":true,"state_35":true,"state_47":true},{"type":"person","title":"Jesse J. Young","avail":true,"age":73,"date":470534400000.0,"state_2":true,"state_4":true,"state_6":true,"state_16":true,"state_20":true,"state_22":true,"state_28":true,"state_48":true},{"type":"person","title":"Bernadette C. Peters","avail":true,"age":81,"date":937353600000.0,"state_22":true,"state_27":true,"state_30":true,"state_37":true,"state_42":true,"state_47":true},{"type":"person","title":"Harry Alsop","state":"h","avail":true,"age":42,"state_2":true,"state_3":true,"state_46":true},{"type":"person","title":"Samantha Bower","state":"h","avail":true,"age":40,"state_1":true,"state_2":true,"state_9":true,"state_14":true,"state_23":true,"state_26":true,"state_28":true,"state_30":true,"state_32":true,"state_33":true},{"type":"person","title":"Samantha D. Parr","avail":true,"age":97,"state_7":true,"state_9":true,"state_14":true,"state_15":true,"state_16":true,"state_17":true,"state_27":true,"state_37":true,"state_48":true},{"type":"person","title":"Dylan Dyer","avail":true,"age":40,"state_2":true,"state_3":true,"state_9":true,"state_16":true,"state_21":true,"state_22":true,"state_31":true,"state_43":true}]},{"type":"role","title":"Spring balances","children":[{"type":"person","title":"Corey M. Cornish","state":"s","avail":true,"age":46,"state_11":true,"state_13":true,"state_19":true,"state_21":true,"state_25":true,"state_28":true,"state_34":true,"state_36":true,"state_39":true,"state_41":true,"state_42":true,"state_46":true},{"type":"person","title":"Amelia I. Harris","avail":true,"age":54,"date":1594425600000.0,"state_9":true,"state_10":true,"state_19":true,"state_23":true,"state_26":true,"state_27":true,"state_29":true,"state_38":true,"state_44":true,"state_47":true},{"type":"person","title":"Bobbie McGrath","age":63,"date":549072000000.0,"state_1":true,"state_9":true,"state_13":true,"state_17":true,"state_21":true,"state_28":true,"state_29":true,"state_35":true,"state_39":true,"state_40":true}]},{"type":"role","title":"Knit functions","children":[{"type":"person","title":"Carolyn McGrath","state":"s","avail":true,"age":36,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_7":true,"state_8":true,"state_9":true,"state_11":true,"state_13":true,"state_15":true,"state_27":true,"state_31":true,"state_35":true,"state_37":true,"state_46":true,"state_50":true},{"type":"person","title":"Sydney Morgan","avail":true,"age":88,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_17":true,"state_18":true,"state_22":true,"state_25":true,"state_27":true,"state_31":true,"state_32":true,"state_37":true,"state_42":true,"state_49":true},{"type":"person","title":"Cameron Dickens","avail":true,"age":56,"state_11":true,"state_15":true,"state_19":true,"state_20":true,"state_26":true,"state_31":true,"state_43":true,"state_44":true,"state_50":true},{"type":"person","title":"Mary T. McDonald","avail":true,"age":58,"date":1724889600000.0,"state_14":true,"state_17":true,"state_23":true,"state_30":true,"state_32":true,"state_46":true,"state_48":true},{"type":"person","title":"Wanda F. Hart","avail":true,"age":83,"date":1590624000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_9":true,"state_17":true,"state_18":true,"state_22":true,"state_42":true,"state_43":true,"state_46":true},{"type":"person","title":"Dylan Ross","age":38,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_2":true,"state_11":true,"state_12":true,"state_13":true,"state_14":true,"state_20":true,"state_23":true,"state_29":true,"state_32":true,"state_33":true,"state_37":true,"state_44":true,"state_47":true},{"type":"person","title":"Ella Lambert","state":"s","avail":true,"age":41,"date":70588800000.0,"state_7":true,"state_9":true,"state_11":true,"state_14":true,"state_16":true,"state_19":true,"state_20":true,"state_22":true,"state_27":true,"state_31":true,"state_44":true}]},{"type":"role","title":"Send surveies","children":[{"type":"person","title":"Kimberly Cornish","state":"h","avail":true,"age":44,"state_16":true,"state_20":true,"state_22":true,"state_26":true,"state_28":true,"state_39":true},{"type":"person","title":"Rachel C. Clark","state":"h","age":53,"date":45100800000.0,"state_6":true,"state_8":true,"state_11":true,"state_12":true,"state_20":true,"state_22":true,"state_24":true,"state_26":true,"state_29":true,"state_36":true,"state_40":true,"state_49":true},{"type":"person","title":"Chris C. Hardacre","avail":true,"age":71,"date":648259200000.0,"state_2":true,"state_5":true,"state_6":true,"state_15":true,"state_24":true,"state_25":true,"state_26":true,"state_37":true,"state_43":true,"state_46":true,"state_49":true},{"type":"person","title":"Eric M. Marshall","state":"s","avail":true,"age":81,"date":537926400000.0,"state_1":true,"state_3":true,"state_12":true,"state_16":true,"state_17":true,"state_18":true,"state_34":true,"state_35":true,"state_39":true,"state_42":true,"state_50":true},{"type":"person","title":"Melanie Thomson","avail":true,"age":38,"date":1126828800000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_3":true,"state_4":true,"state_23":true,"state_27":true,"state_29":true,"state_30":true},{"type":"person","title":"Lily Roberts","state":"s","avail":true,"age":53,"date":599097600000.0,"state_2":true,"state_6":true,"state_8":true,"state_9":true,"state_22":true,"state_31":true,"state_43":true,"state_47":true},{"type":"person","title":"Carolyn Y. Taylor","age":66,"date":989107200000.0,"state_2":true,"state_3":true,"state_4":true,"state_5":true,"state_8":true,"state_13":true,"state_25":true,"state_26":true,"state_29":true,"state_30":true,"state_40":true,"state_46":true,"state_50":true},{"type":"person","title":"Grace G. Peake","avail":true,"age":27,"date":1224374400000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_2":true,"state_3":true,"state_8":true,"state_11":true,"state_15":true,"state_18":true,"state_26":true,"state_28":true,"state_38":true,"state_40":true,"state_46":true,"state_48":true},{"type":"person","title":"Jack Y. Lawrence","age":43,"date":702518400000.0,"state_2":true,"state_6":true,"state_13":true,"state_16":true,"state_18":true,"state_28":true,"state_32":true,"state_37":true,"state_39":true,"state_41":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Justin A. Howard","avail":true,"age":51,"date":1687996800000.0,"state_7":true,"state_8":true,"state_14":true,"state_21":true,"state_25":true,"state_26":true,"state_28":true,"state_30":true,"state_43":true,"state_44":true,"state_47":true,"state_50":true},{"type":"person","title":"Wendy G. Clark","avail":true,"age":84,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_15":true,"state_20":true,"state_36":true,"state_39":true},{"type":"person","title":"Jane Allan","avail":true,"age":27,"date":391651200000.0,"state_3":true,"state_7":true,"state_14":true,"state_15":true,"state_20":true,"state_21":true,"state_26":true,"state_30":true,"state_33":true,"state_42":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Cameron L. Alsop","state":"s","avail":true,"age":83,"date":791337600000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_4":true,"state_5":true,"state_17":true,"state_23":true,"state_26":true,"state_27":true,"state_32":true,"state_38":true,"state_39":true,"state_45":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Alex L. Payne","state":"h","avail":true,"age":38,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_6":true,"state_9":true,"state_12":true,"state_15":true,"state_17":true,"state_22":true,"state_23":true,"state_43":true,"state_49":true}]},{"type":"role","title":"Cash airlines","children":[{"type":"person","title":"Paul S. Lawrence","state":"s","avail":true,"age":84,"state_1":true,"state_16":true,"state_21":true,"state_23":true,"state_24":true,"state_29":true,"state_32":true,"state_35":true,"state_36":true,"state_42":true,"state_46":true,"state_47":true},{"type":"person","title":"Victoria Z. MacLeod","avail":true,"age":40,"state_1":true,"state_5":true,"state_13":true,"state_14":true,"state_22":true,"state_24":true,"state_27":true,"state_28":true,"state_38":true,"state_39":true,"state_42":true},{"type":"person","title":"Wanda Campbell","state":"s","avail":true,"age":88,"date":283564800000.0,"state_5":true,"state_9":true,"state_21":true,"state_34":true,"state_35":true,"state_36":true,"state_42":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Colin Y. Parr","state":"h","avail":true,"age":70,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_13":true,"state_14":true,"state_15":true,"state_18":true,"state_19":true,"state_23":true,"state_29":true,"state_37":true,"state_38":true,"state_41":true,"state_42":true,"state_45":true},{"type":"person","title":"Boris R. Henderson","avail":true,"age":28,"date":318816000000.0,"state_6":true,"state_10":true,"state_13":true,"state_21":true,"state_27":true,"state_31":true,"state_34":true}]},{"type":"role","title":"Gaze escapes","children":[{"type":"person","title":"Eddie E. MacDonald","avail":true,"age":73,"date":729043200000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_5":true,"state_12":true,"state_13":true,"state_18":true,"state_25":true,"state_36":true,"state_39":true,"state_43":true,"state_47":true,"state_49":true},{"type":"person","title":"Jayden Brown","avail":true,"age":69,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_9":true,"state_19":true,"state_22":true,"state_26":true,"state_28":true,"state_37":true,"state_39":true,"state_43":true,"state_49":true},{"type":"person","title":"Jake Z. Henderson","avail":true,"age":94,"date":162259200000.0,"state_5":true,"state_9":true,"state_16":true,"state_18":true,"state_24":true,"state_26":true,"state_32":true,"state_36":true,"state_37":true,"state_39":true,"state_44":true,"state_46":true,"state_47":true,"state_50":true},{"type":"person","title":"Keith K. Lewis","avail":true,"age":87,"date":1304726400000.0,"state_3":true,"state_4":true,"state_12":true,"state_13":true,"state_16":true,"state_20":true,"state_21":true,"state_31":true,"state_33":true,"state_40":true,"state_43":true,"state_46":true,"state_49":true,"state_50":true}]},{"type":"role","title":"Reuse cycles","children":[{"type":"person","title":"Joseph M. Poole","state":"h","avail":true,"age":56,"state_2":true,"state_4":true,"state_6":true,"state_18":true,"state_29":true,"state_36":true},{"type":"person","title":"Joanne A. Allan","state":"s","avail":true,"age":54,"date":421545600000.0,"state_1":true,"state_3":true,"state_7":true,"state_9":true,"state_15":true,"state_21":true,"state_22":true,"state_24":true,"state_28":true,"state_33":true,"state_39":true,"state_41":true,"state_42":true,"state_44":true,"state_47":true},{"type":"person","title":"Felicity J. Walsh","avail":true,"age":43,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_8":true,"state_12":true,"state_21":true,"state_44":true,"state_50":true},{"type":"person","title":"Una Gray","avail":true,"age":73,"date":599184000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_8":true,"state_11":true,"state_21":true,"state_39":true,"state_41":true,"state_45":true},{"type":"person","title":"Maria R. Blake","avail":true,"age":74,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_11":true,"state_21":true,"state_24":true,"state_25":true,"state_27":true,"state_37":true,"state_40":true,"state_41":true},{"type":"person","title":"Lucas Berry","state":"s","avail":true,"age":44,"date":379382400000.0,"state_7":true,"state_8":true,"state_15":true,"state_19":true,"state_34":true,"state_38":true,"state_39":true,"state_40":true,"state_43":true,"state_44":true,"state_45":true},{"type":"person","title":"Caroline Miller","avail":true,"age":70,"date":718416000000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_2":true,"state_14":true,"state_24":true,"state_25":true,"state_31":true,"state_39":true,"state_48":true},{"type":"person","title":"Lou Manning","avail":true,"age":48,"date":956102400000.0,"state_1":true,"state_7":true,"state_26":true,"state_31":true,"state_33":true,"state_36":true,"state_43":true,"state_48":true,"state_49":true},{"type":"person","title":"Faith X. Hemmings","avail":true,"age":76,"state_11":true,"state_17":true,"state_19":true,"state_27":true,"state_29":true,"state_30":true,"state_35":true,"state_36":true,"state_37":true,"state_39":true,"state_44":true,"state_50":true},{"type":"person","title":"Casey Q. Abraham","state":"s","avail":true,"age":27,"state_5":true,"state_6":true,"state_15":true,"state_16":true,"state_24":true,"state_31":true,"state_32":true,"state_34":true,"state_36":true,"state_39":true,"state_44":true,"state_45":true,"state_48":true,"state_49":true},{"type":"person","title":"Isaac B. Simpson","avail":true,"age":26,"date":1235088000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_4":true,"state_11":true,"state_19":true,"state_26":true,"state_35":true,"state_36":true,"state_40":true},{"type":"person","title":"Max Q. Wilson","avail":true,"age":27,"date":146534400000.0,"state_7":true,"state_15":true,"state_17":true,"state_20":true,"state_26":true,"state_31":true,"state_34":true,"state_38":true,"state_42":true,"state_44":true,"state_46":true},{"type":"person","title":"Lou Y. Chapman","avail":true,"age":71,"date":491443200000.0,"state_9":true,"state_13":true,"state_15":true,"state_19":true,"state_20":true,"state_27":true,"state_28":true,"state_29":true,"state_34":true,"state_41":true},{"type":"person","title":"Sam Fraser","age":29,"date":1700179200000.0,"state_3":true,"state_8":true,"state_12":true,"state_13":true,"state_18":true,"state_24":true,"state_30":true},{"type":"person","title":"Gabrielle K. Pullman","avail":true,"age":69,"state_5":true,"state_6":true,"state_11":true,"state_20":true,"state_27":true,"state_28":true,"state_33":true,"state_34":true,"state_36":true,"state_37":true,"state_50":true},{"type":"person","title":"Daryl Rutherford","state":"s","avail":true,"age":46,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_3":true,"state_12":true,"state_13":true,"state_14":true,"state_19":true,"state_32":true,"state_38":true,"state_41":true,"state_46":true}]},{"type":"role","title":"Spill wins","children":[{"type":"person","title":"Evan Paige","avail":true,"age":33,"date":1711929600000.0,"state_3":true,"state_7":true,"state_12":true,"state_15":true,"state_20":true,"state_37":true},{"type":"person","title":"Natalie A. Roberts","state":"s","avail":true,"age":78,"state_7":true,"state_9":true,"state_12":true,"state_13":true,"state_17":true,"state_18":true,"state_33":true,"state_35":true,"state_37":true,"state_39":true,"state_45":true,"state_47":true,"state_50":true},{"type":"person","title":"Benjamin I. Greene","state":"h","avail":true,"age":26,"state_3":true,"state_4":true,"state_5":true,"state_10":true,"state_12":true,"state_19":true,"state_21":true,"state_28":true,"state_47":true,"state_49":true},{"type":"person","title":"Julian Smith","state":"h","avail":true,"age":51,"date":62294400000.0,"state_12":true,"state_15":true,"state_19":true,"state_21":true,"state_24":true,"state_26":true,"state_30":true,"state_36":true,"state_40":true,"state_43":true,"state_49":true},{"type":"person","title":"Chris O. Knox","state":"h","avail":true,"age":22,"date":62380800000.0,"state_10":true,"state_11":true,"state_14":true,"state_26":true,"state_40":true,"state_49":true},{"type":"person","title":"Amanda K. Davies","state":"s","avail":true,"age":44,"date":29894400000.0,"state_7":true,"state_9":true,"state_14":true,"state_15":true,"state_21":true,"state_22":true,"state_27":true,"state_31":true,"state_35":true,"state_36":true,"state_41":true,"state_43":true},{"type":"person","title":"William S. McLean","avail":true,"age":81,"date":882057600000.0,"state_10":true,"state_13":true,"state_18":true,"state_21":true,"state_36":true,"state_38":true,"state_41":true,"state_43":true},{"type":"person","title":"Diane X. Glover","avail":true,"age":70,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_6":true,"state_9":true,"state_12":true,"state_24":true,"state_28":true,"state_38":true,"state_49":true},{"type":"person","title":"Irene F. Burgess","avail":true,"age":48,"state_14":true,"state_17":true,"state_25":true,"state_26":true,"state_29":true,"state_32":true,"state_33":true,"state_38":true,"state_41":true,"state_42":true,"state_46":true,"state_50":true},{"type":"person","title":"Jason J. Abraham","avail":true,"age":59,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_7":true,"state_8":true,"state_17":true,"state_31":true,"state_34":true,"state_37":true},{"type":"person","title":"Bernadette Springer","avail":true,"age":35,"state_13":true,"state_16":true,"state_21":true,"state_22":true,"state_30":true,"state_31":true,"state_40":true,"state_42":true},{"type":"person","title":"Brian W. Carr","avail":true,"age":71,"date":420768000000.0,"state_3":true,"state_13":true,"state_28":true},{"type":"person","title":"Brian K. Chapman","avail":true,"age":57,"date":1339632000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_1":true,"state_18":true,"state_21":true,"state_30":true,"state_49":true},{"type":"person","title":"Kelly L. Paige","state":"h","avail":true,"age":69,"state_6":true,"state_8":true,"state_13":true,"state_17":true,"state_23":true,"state_24":true,"state_30":true}]},{"type":"role","title":"Misuse foots","children":[{"type":"person","title":"Trevor Y. MacLeod","avail":true,"age":86,"date":5097600000.0,"state_1":true,"state_2":true,"state_4":true,"state_6":true,"state_12":true,"state_19":true,"state_22":true,"state_26":true,"state_27":true,"state_37":true,"state_39":true,"state_41":true,"state_46":true,"state_47":true},{"type":"person","title":"Casey Y. Oliver","avail":true,"age":74,"date":54518400000.0,"state_4":true,"state_5":true,"state_11":true,"state_13":true,"state_17":true,"state_23":true,"state_25":true,"state_34":true,"state_40":true},{"type":"person","title":"Michelle Churchill","avail":true,"age":72,"date":760492800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_5":true,"state_6":true,"state_7":true,"state_8":true,"state_26":true,"state_27":true,"state_31":true,"state_33":true,"state_43":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Wendy Carr","state":"s","avail":true,"age":89,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_4":true,"state_14":true,"state_15":true,"state_17":true,"state_18":true,"state_19":true,"state_21":true,"state_24":true,"state_26":true,"state_27":true,"state_29":true,"state_30":true,"state_34":true,"state_39":true,"state_47":true,"state_48":true},{"type":"person","title":"Frances Gill","state":"s","age":46,"state_16":true,"state_17":true,"state_18":true,"state_20":true,"state_22":true,"state_23":true,"state_24":true,"state_26":true,"state_40":true,"state_46":true},{"type":"person","title":"Dylan U. Young","avail":true,"age":43,"date":1338508800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_3":true,"state_4":true,"state_6":true,"state_21":true,"state_28":true,"state_30":true,"state_33":true,"state_42":true,"state_49":true}]}]},{"type":"department","title":"Dept. for Folds and Hedgehogs","children":[{"type":"role","title":"String companies","children":[{"type":"person","title":"Eric Oliver","state":"s","avail":true,"age":42,"date":637027200000.0,"state_4":true,"state_17":true,"state_18":true,"state_20":true,"state_32":true,"state_38":true,"state_41":true,"state_42":true,"state_43":true},{"type":"person","title":"Chris O. Bailey","state":"h","avail":true,"age":44,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_1":true,"state_10":true,"state_20":true,"state_21":true,"state_25":true,"state_31":true,"state_35":true,"state_40":true,"state_43":true},{"type":"person","title":"Pippa F. Greene","avail":true,"age":79,"date":505612800000.0,"state_4":true,"state_11":true,"state_24":true,"state_26":true,"state_30":true,"state_36":true,"state_42":true,"state_45":true,"state_49":true},{"type":"person","title":"Max K. Vaughan","state":"s","avail":true,"age":69,"date":471744000000.0,"state_1":true,"state_5":true,"state_7":true,"state_9":true,"state_10":true,"state_14":true,"state_17":true,"state_20":true,"state_27":true,"state_34":true,"state_36":true,"state_38":true,"state_42":true,"state_46":true},{"type":"person","title":"Madeleine Wilkins","avail":true,"age":81,"date":1564704000000.0,"state_3":true,"state_4":true,"state_5":true,"state_7":true,"state_14":true,"state_22":true,"state_25":true,"state_26":true,"state_31":true,"state_38":true,"state_39":true,"state_41":true,"state_44":true},{"type":"person","title":"Joshua Wright","age":96,"date":214444800000.0,"state_1":true,"state_8":true,"state_15":true,"state_17":true,"state_18":true,"state_22":true,"state_23":true,"state_29":true,"state_37":true,"state_39":true,"state_40":true},{"type":"person","title":"Kyle I. Allan","state":"h","avail":true,"age":63,"date":1404345600000.0,"state_6":true,"state_12":true,"state_18":true,"state_20":true,"state_32":true,"state_35":true,"state_36":true,"state_42":true,"state_44":true},{"type":"person","title":"Lucas F. Manning","avail":true,"age":77,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_2":true,"state_4":true,"state_6":true,"state_9":true,"state_10":true,"state_12":true,"state_29":true,"state_30":true,"state_31":true,"state_35":true,"state_38":true,"state_41":true,"state_48":true,"state_49":true},{"type":"person","title":"Chris S. Simpson","avail":true,"age":76,"state_1":true,"state_2":true,"state_19":true,"state_24":true,"state_25":true,"state_28":true,"state_30":true,"state_33":true,"state_39":true,"state_40":true}]},{"type":"role","title":"Indulge zebras","children":[{"type":"person","title":"Anna F. Hill","avail":true,"age":27,"date":710726400000.0,"state_2":true,"state_29":true,"state_36":true,"state_37":true,"state_39":true,"state_41":true},{"type":"person","title":"Hannah Burgess","state":"h","avail":true,"age":55,"date":307324800000.0,"state_19":true,"state_20":true,"state_22":true,"state_26":true,"state_29":true,"state_33":true,"state_39":true},{"type":"person","title":"Colin Thomson","avail":true,"age":27,"state_2":true,"state_8":true,"state_9":true,"state_17":true,"state_21":true,"state_27":true,"state_28":true,"state_30":true,"state_39":true,"state_40":true,"state_43":true,"state_46":true,"state_48":true},{"type":"person","title":"Kelly Graham","avail":true,"age":56,"state_1":true,"state_6":true,"state_18":true,"state_20":true,"state_28":true,"state_35":true,"state_44":true,"state_46":true,"state_48":true,"state_50":true},{"type":"person","title":"Abigail Johnston","avail":true,"age":76,"date":495158400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_2":true,"state_3":true,"state_5":true,"state_7":true,"state_9":true,"state_14":true,"state_17":true,"state_18":true,"state_31":true,"state_40":true,"state_41":true},{"type":"person","title":"Jacob Wilkins","avail":true,"age":48,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_4":true,"state_6":true,"state_8":true,"state_9":true,"state_13":true,"state_22":true,"state_23":true,"state_24":true,"state_29":true,"state_40":true,"state_44":true},{"type":"person","title":"Penelope G. Hamilton","avail":true,"age":87,"date":1281657600000.0,"state_2":true,"state_4":true,"state_15":true,"state_25":true,"state_36":true,"state_38":true,"state_39":true,"state_40":true,"state_42":true,"state_44":true},{"type":"person","title":"Jesse Brown","state":"s","avail":true,"age":75,"state_1":true,"state_2":true,"state_20":true,"state_25":true,"state_36":true,"state_40":true,"state_41":true,"state_44":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Joseph Burgess","avail":true,"age":55,"date":483148800000.0,"state_7":true,"state_9":true,"state_24":true,"state_27":true,"state_28":true,"state_30":true,"state_35":true,"state_37":true,"state_43":true,"state_45":true,"state_49":true}]},{"type":"role","title":"Click sells","children":[{"type":"person","title":"Rachel H. Stewart","state":"h","avail":true,"age":70,"date":190339200000.0,"state_2":true,"state_6":true,"state_12":true,"state_17":true,"state_18":true,"state_23":true,"state_25":true,"state_31":true,"state_46":true,"state_47":true,"state_48":true},{"type":"person","title":"Michelle F. Piper","state":"h","avail":true,"age":39,"date":1572393600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_12":true,"state_16":true,"state_17":true,"state_31":true,"state_33":true,"state_35":true,"state_38":true,"state_43":true,"state_48":true},{"type":"person","title":"Andy S. Walker","avail":true,"age":50,"state_5":true,"state_7":true,"state_11":true,"state_26":true,"state_34":true,"state_36":true,"state_37":true,"state_39":true,"state_40":true,"state_43":true,"state_45":true,"state_46":true,"state_47":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Emma D. Scott","avail":true,"age":57,"date":170985600000.0,"state_5":true,"state_9":true,"state_10":true,"state_17":true,"state_27":true,"state_28":true,"state_31":true,"state_36":true,"state_40":true},{"type":"person","title":"Jason Johnston","state":"s","avail":true,"age":31,"state_2":true,"state_4":true,"state_8":true,"state_9":true,"state_11":true,"state_12":true,"state_13":true,"state_16":true,"state_21":true,"state_22":true,"state_26":true,"state_28":true,"state_31":true},{"type":"person","title":"Deirdre Davies","state":"s","avail":true,"age":64,"date":380764800000.0,"state_2":true,"state_11":true,"state_13":true,"state_14":true,"state_15":true,"state_16":true,"state_18":true,"state_20":true,"state_22":true,"state_23":true,"state_24":true,"state_27":true,"state_29":true,"state_46":true,"state_47":true},{"type":"person","title":"Rebecca Pullman","avail":true,"age":47,"state_1":true,"state_2":true,"state_5":true,"state_6":true,"state_9":true,"state_12":true,"state_13":true,"state_15":true,"state_17":true,"state_18":true,"state_19":true,"state_26":true,"state_27":true,"state_29":true,"state_31":true,"state_40":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Trevor M. Tucker","avail":true,"age":34,"date":1167609600000.0,"state_4":true,"state_11":true,"state_21":true,"state_22":true,"state_37":true,"state_41":true,"state_43":true,"state_50":true},{"type":"person","title":"Una C. Dowd","avail":true,"age":78,"state_2":true,"state_14":true,"state_16":true,"state_18":true,"state_20":true,"state_21":true,"state_22":true,"state_24":true,"state_35":true,"state_36":true,"state_40":true,"state_41":true,"state_43":true,"state_46":true},{"type":"person","title":"Sam A. Newman","state":"s","avail":true,"age":34,"state_6":true,"state_7":true,"state_9":true,"state_11":true,"state_12":true,"state_21":true,"state_22":true,"state_24":true,"state_26":true,"state_31":true,"state_42":true,"state_45":true,"state_50":true},{"type":"person","title":"Kelly Y. Powell","state":"h","avail":true,"age":63,"date":292032000000.0,"state_1":true,"state_3":true,"state_4":true,"state_5":true,"state_7":true,"state_9":true,"state_21":true,"state_22":true,"state_23":true,"state_24":true,"state_31":true,"state_37":true,"state_38":true,"state_43":true,"state_46":true,"state_48":true},{"type":"person","title":"Joseph Abraham","age":51,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_14":true,"state_17":true,"state_21":true,"state_35":true,"state_40":true,"state_42":true,"state_44":true},{"type":"person","title":"Bernadette Young","avail":true,"age":31,"date":647395200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_11":true,"state_13":true,"state_18":true,"state_20":true,"state_22":true,"state_26":true,"state_29":true,"state_30":true,"state_33":true,"state_43":true,"state_46":true},{"type":"person","title":"Chris A. Hughes","avail":true,"age":95,"date":325641600000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_5":true,"state_7":true,"state_10":true,"state_11":true,"state_12":true,"state_14":true,"state_16":true,"state_32":true,"state_35":true,"state_36":true,"state_38":true},{"type":"person","title":"Leonard Paterson","state":"h","avail":true,"age":59,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_10":true,"state_14":true,"state_19":true,"state_35":true,"state_36":true,"state_37":true,"state_39":true,"state_43":true,"state_44":true,"state_45":true,"state_48":true},{"type":"person","title":"Chris L. Turner","avail":true,"age":37,"date":610070400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_5":true,"state_10":true,"state_11":true,"state_12":true,"state_18":true,"state_19":true,"state_20":true,"state_23":true,"state_24":true,"state_26":true,"state_27":true,"state_30":true,"state_37":true,"state_40":true}]},{"type":"role","title":"Digest combines","children":[{"type":"person","title":"Sebastian Q. Hardacre","avail":true,"age":68,"date":745891200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_1":true,"state_20":true,"state_28":true,"state_33":true,"state_34":true,"state_37":true,"state_46":true,"state_47":true,"state_48":true},{"type":"person","title":"Casey S. Oliver","state":"h","avail":true,"age":72,"date":966211200000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_7":true,"state_13":true,"state_18":true,"state_20":true,"state_21":true,"state_22":true,"state_23":true,"state_25":true,"state_30":true,"state_31":true,"state_32":true,"state_34":true,"state_35":true,"state_40":true,"state_47":true,"state_50":true},{"type":"person","title":"Cameron A. Wright","avail":true,"age":60,"date":1115510400000.0,"state_6":true,"state_10":true,"state_22":true,"state_25":true,"state_32":true,"state_37":true,"state_40":true,"state_41":true,"state_43":true,"state_49":true},{"type":"person","title":"Sydney Wilkins","avail":true,"age":86,"state_3":true,"state_5":true,"state_23":true,"state_24":true,"state_28":true,"state_41":true,"state_50":true}]},{"type":"role","title":"Discover promises","children":[{"type":"person","title":"Emma N. Sharp","avail":true,"age":92,"date":915926400000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_6":true,"state_8":true,"state_9":true,"state_15":true,"state_16":true,"state_19":true,"state_22":true,"state_23":true,"state_26":true,"state_43":true,"state_50":true},{"type":"person","title":"Charlie Morrison","state":"s","avail":true,"age":59,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_6":true,"state_17":true,"state_19":true,"state_20":true,"state_27":true,"state_28":true,"state_41":true,"state_49":true},{"type":"person","title":"Jamie Arnold","avail":true,"age":52,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_13":true,"state_14":true,"state_15":true,"state_19":true,"state_21":true,"state_24":true,"state_28":true,"state_45":true,"state_49":true},{"type":"person","title":"Stephanie Hodges","avail":true,"age":84,"date":324172800000.0,"state_9":true,"state_10":true,"state_11":true,"state_14":true,"state_23":true,"state_33":true,"state_36":true,"state_39":true},{"type":"person","title":"Jasmine Chapman","state":"s","avail":true,"age":31,"date":626227200000.0,"state_3":true,"state_7":true,"state_10":true,"state_13":true,"state_15":true,"state_18":true,"state_23":true,"state_25":true,"state_32":true,"state_34":true,"state_37":true,"state_38":true,"state_48":true},{"type":"person","title":"Elizabeth O. Duncan","avail":true,"age":61,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_7":true,"state_8":true,"state_15":true,"state_19":true,"state_23":true,"state_30":true,"state_37":true,"state_48":true},{"type":"person","title":"Paul N. Quinn","state":"s","avail":true,"age":63,"date":1279497600000.0,"state_13":true,"state_20":true,"state_23":true,"state_31":true,"state_32":true,"state_33":true,"state_34":true,"state_35":true,"state_37":true,"state_40":true,"state_41":true,"state_44":true,"state_49":true,"state_50":true},{"type":"person","title":"Harry Graham","avail":true,"age":65,"date":680313600000.0,"state_1":true,"state_11":true,"state_19":true,"state_24":true,"state_32":true,"state_39":true,"state_47":true,"state_48":true},{"type":"person","title":"Alan Jones","avail":true,"age":26,"state_2":true,"state_8":true,"state_10":true,"state_15":true,"state_17":true,"state_19":true,"state_21":true,"state_28":true,"state_31":true,"state_32":true,"state_43":true},{"type":"person","title":"Joseph Henderson","avail":true,"age":91,"date":925689600000.0,"state_11":true,"state_12":true,"state_24":true,"state_25":true,"state_28":true,"state_41":true,"state_44":true},{"type":"person","title":"Edward Baker","avail":true,"age":44,"date":281750400000.0,"state_4":true,"state_22":true,"state_23":true,"state_25":true,"state_30":true,"state_37":true,"state_40":true,"state_41":true},{"type":"person","title":"Adrian V. Parsons","state":"s","avail":true,"age":23,"date":139536000000.0,"state_2":true,"state_3":true,"state_8":true,"state_39":true,"state_44":true},{"type":"person","title":"Virginia Parr","avail":true,"age":77,"date":845251200000.0,"state_1":true,"state_4":true,"state_11":true,"state_12":true,"state_20":true,"state_28":true,"state_30":true,"state_41":true,"state_42":true,"state_43":true},{"type":"person","title":"Fiona Terry","avail":true,"age":74,"state_18":true,"state_19":true,"state_26":true,"state_37":true,"state_40":true,"state_42":true,"state_44":true},{"type":"person","title":"Sarah Roberts","avail":true,"age":71,"date":469843200000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_8":true,"state_9":true,"state_10":true,"state_12":true,"state_19":true,"state_25":true,"state_26":true,"state_33":true,"state_34":true,"state_38":true,"state_41":true,"state_45":true,"state_49":true},{"type":"person","title":"Zoe Pullman","avail":true,"age":90,"date":1200009600000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_3":true,"state_5":true,"state_12":true,"state_14":true,"state_17":true,"state_19":true,"state_22":true,"state_30":true,"state_33":true,"state_39":true,"state_43":true,"state_50":true},{"type":"person","title":"Chloe V. Edmunds","avail":true,"age":24,"date":1130457600000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_12":true,"state_17":true,"state_21":true,"state_23":true,"state_26":true,"state_31":true,"state_36":true,"state_40":true,"state_45":true,"state_48":true,"state_49":true},{"type":"person","title":"Jason K. Burgess","avail":true,"age":26,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_5":true,"state_8":true,"state_11":true,"state_12":true,"state_14":true,"state_18":true,"state_19":true,"state_23":true,"state_26":true,"state_28":true,"state_37":true,"state_38":true,"state_41":true,"state_45":true,"state_50":true},{"type":"person","title":"Jennifer Turner","avail":true,"age":35,"state_10":true,"state_14":true,"state_15":true,"state_21":true,"state_26":true,"state_34":true,"state_38":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Simon Grant","avail":true,"age":70,"date":1208217600000.0,"state_5":true,"state_9":true,"state_20":true,"state_30":true,"state_31":true,"state_38":true,"state_47":true},{"type":"person","title":"Frank Q. Sanderson","avail":true,"age":96,"state_3":true,"state_4":true,"state_7":true,"state_11":true,"state_12":true,"state_13":true,"state_18":true,"state_33":true,"state_36":true,"state_42":true}]},{"type":"role","title":"Argue futures","children":[{"type":"person","title":"Jayden Q. Wallace","avail":true,"age":27,"date":1254960000000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_16":true,"state_19":true,"state_25":true,"state_28":true,"state_31":true,"state_34":true,"state_36":true,"state_42":true,"state_48":true},{"type":"person","title":"Andy Graham","state":"s","age":48,"date":884995200000.0,"state_2":true,"state_4":true,"state_7":true,"state_32":true,"state_34":true,"state_38":true,"state_40":true,"state_41":true,"state_42":true,"state_43":true,"state_47":true},{"type":"person","title":"Dan W. Hill","state":"h","avail":true,"age":92,"state_7":true,"state_11":true,"state_14":true,"state_19":true,"state_20":true,"state_23":true,"state_24":true,"state_26":true,"state_27":true,"state_29":true,"state_32":true,"state_34":true,"state_43":true,"state_44":true,"state_50":true},{"type":"person","title":"Heather Piper","avail":true,"age":44,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_14":true,"state_17":true,"state_21":true,"state_24":true,"state_29":true,"state_30":true,"state_34":true},{"type":"person","title":"Adrian B. Bower","state":"s","avail":true,"age":80,"state_2":true,"state_3":true,"state_9":true,"state_23":true,"state_27":true,"state_29":true,"state_31":true,"state_32":true,"state_37":true,"state_44":true},{"type":"person","title":"Amy E. Ogden","avail":true,"age":22,"date":666489600000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_12":true,"state_17":true,"state_20":true,"state_22":true,"state_23":true,"state_24":true,"state_31":true,"state_37":true,"state_38":true,"state_43":true,"state_45":true,"state_48":true,"state_50":true},{"type":"person","title":"James Russell","avail":true,"age":92,"date":352857600000.0,"state_8":true,"state_17":true,"state_18":true,"state_20":true,"state_23":true,"state_28":true,"state_41":true},{"type":"person","title":"Stephanie S. Rees","avail":true,"age":67,"state_2":true,"state_5":true,"state_6":true,"state_16":true,"state_19":true,"state_22":true,"state_25":true,"state_26":true,"state_39":true,"state_43":true,"state_48":true},{"type":"person","title":"Charlie Hunter","avail":true,"age":43,"date":1084665600000.0,"state_11":true,"state_12":true,"state_16":true,"state_20":true,"state_21":true,"state_25":true,"state_27":true,"state_31":true,"state_32":true,"state_33":true,"state_37":true,"state_39":true,"state_44":true,"state_45":true,"state_48":true,"state_50":true},{"type":"person","title":"Bernadette T. Lyman","avail":true,"age":73,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_8":true,"state_20":true,"state_23":true,"state_29":true,"state_35":true,"state_38":true,"state_40":true,"state_48":true},{"type":"person","title":"Faith Sanderson","avail":true,"age":73,"date":258940800000.0,"state_1":true,"state_8":true,"state_9":true,"state_10":true,"state_11":true,"state_13":true,"state_18":true,"state_33":true,"state_34":true,"state_39":true,"state_47":true,"state_48":true},{"type":"person","title":"Anna Abraham","avail":true,"age":90,"state_11":true,"state_12":true,"state_22":true,"state_24":true,"state_31":true,"state_32":true,"state_47":true},{"type":"person","title":"Jean Greene","state":"s","avail":true,"age":68,"date":1465776000000.0,"state_3":true,"state_13":true,"state_14":true,"state_19":true,"state_27":true,"state_29":true,"state_35":true,"state_47":true},{"type":"person","title":"Austin Bell","age":81,"state_1":true,"state_2":true,"state_3":true,"state_9":true,"state_10":true,"state_14":true,"state_19":true,"state_26":true,"state_28":true,"state_35":true,"state_36":true,"state_37":true,"state_38":true,"state_40":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Jonathan Murray","avail":true,"age":34,"date":585705600000.0,"state_7":true,"state_8":true,"state_11":true,"state_29":true,"state_37":true,"state_38":true,"state_47":true},{"type":"person","title":"Anthony Russell","state":"s","avail":true,"age":63,"date":1080950400000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_3":true,"state_5":true,"state_9":true,"state_11":true,"state_14":true,"state_29":true,"state_40":true,"state_42":true,"state_48":true},{"type":"person","title":"Kimberly Allan","avail":true,"age":90,"date":468374400000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_5":true,"state_14":true,"state_24":true,"state_26":true,"state_29":true,"state_33":true,"state_36":true,"state_39":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Trevor Z. Manning","avail":true,"age":66,"state_11":true,"state_29":true,"state_50":true},{"type":"person","title":"Sophie C. Randall","avail":true,"age":92,"date":199756800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_3":true,"state_7":true,"state_10":true,"state_14":true,"state_17":true,"state_18":true,"state_22":true,"state_34":true,"state_40":true,"state_41":true,"state_45":true,"state_46":true,"state_48":true,"state_50":true},{"type":"person","title":"Sydney H. Mills","state":"h","avail":true,"age":92,"state_2":true,"state_3":true,"state_4":true,"state_12":true,"state_13":true,"state_15":true,"state_17":true,"state_18":true,"state_19":true,"state_20":true,"state_22":true,"state_24":true,"state_25":true,"state_26":true,"state_40":true,"state_43":true,"state_49":true,"state_50":true},{"type":"person","title":"Irene Vance","avail":true,"age":75,"state_1":true,"state_4":true,"state_10":true,"state_11":true,"state_17":true,"state_18":true,"state_20":true,"state_21":true,"state_26":true,"state_31":true,"state_37":true,"state_42":true,"state_47":true,"state_48":true}]},{"type":"role","title":"Inculcate camps"},{"type":"role","title":"Inflame recovers","children":[{"type":"person","title":"Claire U. Greene","avail":true,"age":60,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_12":true,"state_19":true,"state_22":true,"state_25":true,"state_30":true,"state_38":true,"state_46":true}]},{"type":"role","title":"Swear oxes","children":[{"type":"person","title":"Eddie Alsop","avail":true,"age":56,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_13":true,"state_14":true,"state_18":true,"state_22":true,"state_23":true,"state_37":true,"state_38":true,"state_44":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Keith D. Underwood","avail":true,"age":74,"date":1336003200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_8":true,"state_10":true,"state_16":true,"state_18":true,"state_20":true,"state_22":true,"state_28":true,"state_33":true,"state_39":true},{"type":"person","title":"Liam O. Parr","avail":true,"age":22,"date":1130544000000.0,"state_8":true,"state_14":true,"state_28":true,"state_29":true,"state_34":true,"state_35":true,"state_47":true,"state_49":true},{"type":"person","title":"Anthony V. Baker","state":"h","avail":true,"age":57,"date":312595200000.0,"state_5":true,"state_12":true,"state_23":true,"state_25":true,"state_27":true,"state_29":true,"state_31":true,"state_32":true,"state_37":true,"state_45":true,"state_49":true},{"type":"person","title":"Rachel U. Skinner","avail":true,"age":56,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_2":true,"state_7":true,"state_9":true,"state_18":true,"state_24":true,"state_30":true,"state_32":true,"state_41":true,"state_44":true,"state_46":true,"state_50":true},{"type":"person","title":"Samantha E. Underwood","avail":true,"age":80,"date":1671148800000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_3":true,"state_4":true,"state_20":true,"state_25":true,"state_31":true,"state_34":true,"state_41":true,"state_46":true},{"type":"person","title":"Frank V. Ferguson","avail":true,"age":90,"state_8":true,"state_29":true,"state_33":true,"state_34":true,"state_35":true,"state_43":true,"state_46":true},{"type":"person","title":"Gordon Parr","avail":true,"age":70,"state_6":true,"state_10":true,"state_15":true,"state_23":true,"state_26":true,"state_29":true,"state_36":true,"state_44":true,"state_50":true}]},{"type":"role","title":"Mew anywheres","children":[{"type":"person","title":"Olivia Miller","avail":true,"age":63,"state_1":true,"state_13":true,"state_14":true,"state_15":true,"state_16":true,"state_22":true,"state_23":true,"state_26":true,"state_27":true,"state_30":true,"state_32":true}]}]},{"type":"department","title":"Dept. for Listens and Battles","children":[{"type":"role","title":"Fail constants","children":[{"type":"person","title":"Steven H. Davidson","avail":true,"age":22,"date":1396051200000.0,"state_2":true,"state_19":true,"state_22":true,"state_23":true,"state_25":true,"state_26":true,"state_36":true,"state_37":true,"state_43":true},{"type":"person","title":"Samantha G. Oliver","state":"h","avail":true,"age":43,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_9":true,"state_13":true,"state_23":true,"state_24":true,"state_47":true},{"type":"person","title":"Phoenix Greene","state":"s","avail":true,"age":87,"date":1103414400000.0,"state_7":true,"state_8":true,"state_10":true,"state_13":true,"state_19":true,"state_24":true,"state_26":true,"state_35":true,"state_41":true,"state_42":true,"state_44":true},{"type":"person","title":"Madeleine May","avail":true,"age":82,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_5":true,"state_7":true,"state_8":true,"state_17":true,"state_19":true,"state_25":true,"state_28":true,"state_29":true,"state_32":true,"state_38":true,"state_41":true},{"type":"person","title":"Connor I. McGrath","avail":true,"age":35,"date":419040000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_4":true,"state_6":true,"state_8":true,"state_15":true,"state_23":true,"state_24":true,"state_26":true,"state_28":true,"state_29":true,"state_30":true,"state_33":true,"state_36":true,"state_37":true,"state_40":true,"state_41":true,"state_44":true,"state_48":true,"state_50":true},{"type":"person","title":"Wendy C. Nash","avail":true,"age":79,"date":1266364800000.0,"state_7":true,"state_8":true,"state_13":true,"state_20":true,"state_25":true,"state_33":true,"state_38":true,"state_41":true,"state_42":true},{"type":"person","title":"Joe D. Hart","age":57,"date":973296000000.0,"state_1":true,"state_14":true,"state_23":true,"state_34":true,"state_35":true,"state_46":true,"state_48":true},{"type":"person","title":"Peter F. McDonald","avail":true,"age":36,"state_2":true,"state_6":true,"state_8":true,"state_9":true,"state_12":true,"state_14":true,"state_18":true,"state_26":true,"state_28":true,"state_50":true},{"type":"person","title":"Carl Murray","avail":true,"age":89,"state_3":true,"state_7":true,"state_13":true,"state_22":true,"state_32":true,"state_37":true,"state_40":true,"state_41":true,"state_42":true},{"type":"person","title":"Simon X. Paterson","state":"s","avail":true,"age":42,"date":213235200000.0,"state_2":true,"state_3":true,"state_5":true,"state_11":true,"state_19":true,"state_22":true,"state_24":true,"state_38":true,"state_45":true,"state_49":true}]},{"type":"role","title":"Crush choices","children":[{"type":"person","title":"Jennifer G. Churchill","state":"h","avail":true,"age":26,"date":974246400000.0,"state_3":true,"state_14":true,"state_15":true,"state_20":true,"state_22":true,"state_23":true,"state_28":true,"state_32":true,"state_39":true,"state_50":true},{"type":"person","title":"Frances Marshall","avail":true,"age":70,"date":1384992000000.0,"state_3":true,"state_14":true,"state_29":true,"state_36":true,"state_45":true,"state_48":true},{"type":"person","title":"Daryl Alsop","state":"s","avail":true,"age":49,"state_10":true,"state_14":true,"state_21":true,"state_33":true,"state_39":true,"state_43":true,"state_46":true,"state_50":true},{"type":"person","title":"Matt R. Hart","avail":true,"age":79,"date":230342400000.0,"state_2":true,"state_3":true,"state_11":true,"state_21":true,"state_22":true,"state_26":true,"state_28":true,"state_29":true,"state_32":true,"state_45":true,"state_49":true,"state_50":true},{"type":"person","title":"Kelly Wright","state":"h","avail":true,"age":45,"date":1216512000000.0,"state_5":true,"state_8":true,"state_19":true,"state_21":true,"state_28":true,"state_29":true,"state_35":true,"state_37":true,"state_38":true,"state_47":true},{"type":"person","title":"Emily Anderson","avail":true,"age":85,"date":453945600000.0,"state_6":true,"state_24":true,"state_25":true,"state_26":true,"state_30":true,"state_36":true,"state_39":true,"state_43":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Nathan Graham","avail":true,"age":47,"state_8":true,"state_22":true,"state_26":true,"state_31":true,"state_35":true,"state_37":true,"state_47":true}]},{"type":"role","title":"Limp resolutions","children":[{"type":"person","title":"Carl F. Johnston","state":"h","avail":true,"age":67,"date":734832000000.0,"state_1":true,"state_4":true,"state_10":true,"state_13":true,"state_17":true,"state_20":true,"state_24":true,"state_34":true,"state_35":true,"state_43":true,"state_48":true},{"type":"person","title":"Frances F. Sutherland","state":"s","avail":true,"age":66,"state_18":true,"state_23":true,"state_24":true,"state_25":true,"state_29":true,"state_34":true,"state_36":true,"state_37":true,"state_39":true,"state_41":true},{"type":"person","title":"Corey G. Vance","avail":true,"age":66,"date":329616000000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_1":true,"state_2":true,"state_7":true,"state_15":true,"state_16":true,"state_22":true,"state_29":true,"state_33":true,"state_37":true,"state_42":true,"state_44":true,"state_45":true},{"type":"person","title":"Jamie Lawrence","avail":true,"age":69,"state_14":true,"state_17":true,"state_26":true,"state_39":true,"state_41":true},{"type":"person","title":"Gordon K. Morgan","state":"h","avail":true,"age":33,"state_9":true,"state_20":true,"state_28":true,"state_34":true,"state_37":true,"state_40":true,"state_41":true,"state_48":true},{"type":"person","title":"Una Bower","avail":true,"age":81,"date":41040000000.0,"state_1":true,"state_9":true,"state_11":true,"state_12":true,"state_13":true,"state_18":true,"state_19":true,"state_20":true,"state_25":true,"state_29":true,"state_33":true,"state_48":true},{"type":"person","title":"Joseph C. Gill","state":"s","avail":true,"age":81,"state_2":true,"state_3":true,"state_8":true,"state_19":true,"state_23":true,"state_28":true,"state_30":true,"state_32":true,"state_37":true},{"type":"person","title":"Natalie Buckland","state":"h","avail":true,"age":97,"date":60652800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_2":true,"state_5":true,"state_6":true,"state_7":true,"state_11":true,"state_20":true,"state_21":true,"state_23":true,"state_29":true,"state_44":true,"state_45":true,"state_46":true,"state_47":true,"state_48":true},{"type":"person","title":"Virginia Greene","state":"s","age":63,"state_6":true,"state_9":true,"state_10":true,"state_12":true,"state_13":true,"state_20":true,"state_27":true,"state_28":true,"state_29":true,"state_38":true,"state_43":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Felicity Brown","state":"h","avail":true,"age":43,"state_7":true,"state_11":true,"state_15":true,"state_20":true,"state_24":true,"state_28":true,"state_34":true,"state_37":true,"state_39":true,"state_44":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Donna Lewis","avail":true,"age":85,"date":452736000000.0,"state_6":true,"state_15":true,"state_18":true,"state_20":true,"state_23":true,"state_24":true,"state_26":true,"state_28":true,"state_29":true,"state_30":true,"state_33":true,"state_37":true,"state_39":true,"state_46":true},{"type":"person","title":"Christopher Hudson","avail":true,"age":68,"state_2":true,"state_7":true,"state_16":true,"state_17":true,"state_21":true,"state_26":true,"state_28":true,"state_31":true,"state_38":true,"state_43":true,"state_46":true,"state_47":true},{"type":"person","title":"Pat Paige","state":"s","avail":true,"age":54,"state_5":true,"state_6":true,"state_18":true,"state_19":true,"state_21":true,"state_25":true,"state_34":true,"state_44":true},{"type":"person","title":"Austin Sutherland","avail":true,"age":21,"date":576892800000.0,"state_1":true,"state_2":true,"state_4":true,"state_5":true,"state_25":true,"state_26":true,"state_28":true,"state_40":true,"state_41":true,"state_42":true,"state_43":true,"state_49":true,"state_50":true}]},{"type":"role","title":"Decorate increases","children":[{"type":"person","title":"Virginia May","avail":true,"age":42,"state_4":true,"state_7":true,"state_16":true,"state_32":true,"state_36":true,"state_42":true,"state_45":true},{"type":"person","title":"Austin M. Wright","state":"s","avail":true,"age":85,"date":837216000000.0,"state_1":true,"state_7":true,"state_17":true,"state_20":true,"state_21":true,"state_31":true,"state_35":true,"state_36":true,"state_42":true},{"type":"person","title":"Gabrielle Springer","avail":true,"age":73,"date":810604800000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_3":true,"state_4":true,"state_10":true,"state_14":true,"state_15":true,"state_20":true,"state_23":true,"state_24":true,"state_34":true,"state_36":true,"state_37":true,"state_41":true,"state_43":true,"state_46":true,"state_50":true},{"type":"person","title":"Rebecca T. Reid","avail":true,"age":27,"date":211680000000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_1":true,"state_3":true,"state_12":true,"state_14":true,"state_16":true,"state_17":true,"state_19":true,"state_33":true,"state_37":true,"state_38":true,"state_41":true,"state_46":true,"state_48":true},{"type":"person","title":"Anne Piper","avail":true,"age":35,"date":581904000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_4":true,"state_6":true,"state_22":true,"state_24":true,"state_32":true,"state_38":true,"state_46":true},{"type":"person","title":"Boris McGrath","state":"s","avail":true,"age":61,"state_1":true,"state_12":true,"state_17":true,"state_18":true,"state_20":true,"state_21":true,"state_24":true,"state_46":true},{"type":"person","title":"Justin Q. Lawrence","avail":true,"age":47,"date":1330128000000.0,"state_8":true,"state_10":true,"state_11":true,"state_19":true,"state_20":true,"state_31":true,"state_32":true,"state_34":true,"state_37":true,"state_38":true},{"type":"person","title":"Dorothy Berry","state":"h","avail":true,"age":68,"date":679795200000.0,"state_1":true,"state_9":true,"state_16":true,"state_17":true,"state_19":true,"state_21":true,"state_22":true,"state_30":true,"state_41":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Abigail J. Ferguson","avail":true,"age":76,"date":340675200000.0,"state_2":true,"state_3":true,"state_4":true,"state_11":true,"state_13":true,"state_16":true,"state_17":true,"state_20":true,"state_21":true,"state_25":true,"state_26":true,"state_28":true,"state_35":true,"state_40":true,"state_43":true,"state_44":true,"state_49":true},{"type":"person","title":"Trevor Sharp","avail":true,"age":32,"date":1436918400000.0,"state_4":true,"state_7":true,"state_11":true,"state_14":true,"state_16":true,"state_20":true,"state_24":true,"state_27":true,"state_33":true,"state_41":true},{"type":"person","title":"Casey A. Thomson","avail":true,"age":58,"state_1":true,"state_5":true,"state_9":true,"state_11":true,"state_13":true,"state_16":true,"state_18":true,"state_24":true,"state_25":true,"state_26":true,"state_30":true,"state_34":true,"state_43":true},{"type":"person","title":"Una V. Hemmings","avail":true,"age":87,"state_8":true,"state_16":true,"state_23":true,"state_34":true,"state_40":true,"state_42":true,"state_48":true,"state_50":true},{"type":"person","title":"Molly B. Butler","avail":true,"age":33,"date":1084838400000.0,"state_2":true,"state_4":true,"state_6":true,"state_11":true,"state_13":true,"state_14":true,"state_16":true,"state_26":true,"state_27":true,"state_36":true,"state_38":true,"state_47":true},{"type":"person","title":"Sebastian W. Walsh","avail":true,"age":82,"date":231638400000.0,"state_7":true,"state_11":true,"state_18":true,"state_26":true,"state_28":true,"state_32":true,"state_47":true,"state_49":true}]},{"type":"role","title":"Digest acts","children":[{"type":"person","title":"Alexandra Hemmings","avail":true,"age":21,"date":282873600000.0,"state_1":true,"state_8":true,"state_11":true,"state_23":true,"state_26":true,"state_41":true,"state_45":true,"state_50":true},{"type":"person","title":"Carolyn H. Carr","age":41,"date":1023753600000.0,"state_7":true,"state_16":true,"state_20":true,"state_24":true,"state_34":true,"state_40":true,"state_43":true},{"type":"person","title":"Dorothy Ogden","age":40,"date":283046400000.0,"state_4":true,"state_13":true,"state_17":true,"state_27":true,"state_36":true,"state_38":true,"state_41":true,"state_43":true,"state_45":true,"state_46":true,"state_49":true},{"type":"person","title":"Amy King","avail":true,"age":85,"date":921628800000.0,"state_11":true,"state_13":true,"state_16":true,"state_19":true,"state_32":true,"state_41":true},{"type":"person","title":"Justin N. Hughes","avail":true,"age":81,"date":1332201600000.0,"state_7":true,"state_11":true,"state_13":true,"state_14":true,"state_19":true,"state_20":true,"state_21":true,"state_23":true,"state_26":true,"state_28":true,"state_35":true,"state_38":true,"state_39":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Bobbie D. Bailey","age":40,"date":952214400000.0,"state_14":true,"state_15":true,"state_19":true,"state_23":true,"state_26":true,"state_28":true,"state_34":true,"state_35":true,"state_36":true,"state_37":true,"state_49":true,"state_50":true},{"type":"person","title":"Jayden Mathis","avail":true,"age":47,"date":1372636800000.0,"state_26":true,"state_33":true,"state_34":true,"state_36":true,"state_38":true,"state_48":true},{"type":"person","title":"Eddie D. Randall","state":"h","avail":true,"age":69,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_3":true,"state_4":true,"state_7":true,"state_9":true,"state_10":true,"state_17":true,"state_19":true,"state_26":true,"state_29":true,"state_31":true,"state_43":true,"state_46":true,"state_50":true},{"type":"person","title":"Adrian Henderson","avail":true,"age":33,"date":562982400000.0,"state_7":true,"state_9":true,"state_11":true,"state_20":true,"state_28":true,"state_31":true,"state_43":true,"state_50":true}]},{"type":"role","title":"Contrast representatives","children":[{"type":"person","title":"Gabrielle N. Payne","age":24,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_6":true,"state_7":true,"state_10":true,"state_12":true,"state_35":true,"state_42":true,"state_45":true,"state_47":true},{"type":"person","title":"Kimberly Thomson","state":"h","avail":true,"age":23,"date":450403200000.0,"state_1":true,"state_13":true,"state_15":true,"state_25":true,"state_31":true,"state_42":true,"state_45":true,"state_46":true},{"type":"person","title":"Bernadette O. James","state":"h","avail":true,"age":22,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_10":true,"state_22":true,"state_32":true,"state_36":true,"state_38":true,"state_42":true,"state_48":true,"state_49":true},{"type":"person","title":"Abigail Wilkins","state":"s","avail":true,"age":45,"state_2":true,"state_7":true,"state_8":true,"state_9":true,"state_11":true,"state_22":true,"state_23":true,"state_24":true,"state_32":true,"state_38":true},{"type":"person","title":"Rebecca I. Dowd","age":42,"state_1":true,"state_5":true,"state_8":true,"state_10":true,"state_13":true,"state_14":true,"state_15":true,"state_25":true,"state_26":true,"state_36":true,"state_39":true,"state_50":true},{"type":"person","title":"Sue Lawrence","state":"h","avail":true,"age":44,"date":1555977600000.0,"state_11":true,"state_16":true,"state_19":true,"state_20":true,"state_21":true,"state_22":true,"state_27":true,"state_29":true,"state_32":true,"state_34":true,"state_36":true,"state_41":true,"state_42":true,"state_45":true,"state_46":true,"state_48":true,"state_50":true},{"type":"person","title":"Emily Dyer","state":"h","avail":true,"age":40,"date":16934400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_4":true,"state_13":true,"state_17":true,"state_18":true,"state_23":true,"state_33":true,"state_34":true,"state_35":true,"state_44":true},{"type":"person","title":"Frances Hudson","avail":true,"age":83,"date":415324800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_1":true,"state_6":true,"state_14":true,"state_22":true,"state_24":true,"state_29":true,"state_30":true,"state_32":true,"state_39":true,"state_40":true},{"type":"person","title":"Ian A. Avery","state":"h","avail":true,"age":42,"date":538358400000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_4":true,"state_6":true,"state_11":true,"state_19":true,"state_21":true,"state_26":true,"state_29":true,"state_31":true,"state_33":true,"state_46":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Cameron L. Gray","avail":true,"age":66,"state_3":true,"state_4":true,"state_5":true,"state_7":true,"state_8":true,"state_14":true,"state_19":true,"state_21":true,"state_22":true,"state_25":true,"state_28":true,"state_33":true,"state_36":true,"state_39":true,"state_40":true,"state_44":true,"state_45":true},{"type":"person","title":"Frank Young","avail":true,"age":77,"state_3":true,"state_7":true,"state_8":true,"state_15":true,"state_16":true,"state_27":true,"state_30":true,"state_32":true,"state_33":true,"state_34":true,"state_39":true,"state_40":true,"state_50":true},{"type":"person","title":"Jan T. Marshall","avail":true,"age":39,"state_4":true,"state_18":true,"state_19":true,"state_27":true,"state_29":true,"state_39":true,"state_42":true},{"type":"person","title":"Anna Fisher","avail":true,"age":30,"date":80524800000.0,"state_1":true,"state_3":true,"state_4":true,"state_8":true,"state_10":true,"state_32":true,"state_43":true,"state_46":true,"state_48":true,"state_50":true},{"type":"person","title":"Wanda V. Terry","avail":true,"age":39,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_3":true,"state_9":true,"state_10":true,"state_11":true,"state_13":true,"state_14":true,"state_15":true,"state_18":true,"state_19":true,"state_23":true,"state_24":true,"state_25":true,"state_34":true,"state_38":true,"state_40":true,"state_42":true},{"type":"person","title":"Leonard Chapman","avail":true,"age":93,"state_7":true,"state_8":true,"state_9":true,"state_23":true,"state_27":true,"state_30":true,"state_32":true,"state_33":true,"state_38":true,"state_41":true,"state_42":true,"state_46":true},{"type":"person","title":"Casey B. Baker","state":"h","age":34,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_2":true,"state_3":true,"state_16":true,"state_20":true,"state_22":true,"state_23":true,"state_24":true,"state_25":true,"state_30":true,"state_37":true,"state_38":true,"state_43":true,"state_47":true,"state_50":true},{"type":"person","title":"Christian Watson","avail":true,"age":83,"state_22":true,"state_26":true,"state_27":true,"state_32":true,"state_33":true,"state_38":true,"state_40":true,"state_44":true,"state_45":true},{"type":"person","title":"Una J. Grant","avail":true,"age":47,"state_8":true,"state_11":true,"state_15":true,"state_18":true,"state_22":true,"state_23":true,"state_26":true,"state_35":true,"state_39":true,"state_41":true,"state_45":true,"state_49":true},{"type":"person","title":"Corey I. Payne","age":72,"date":968544000000.0,"state_5":true,"state_15":true,"state_20":true,"state_27":true,"state_30":true,"state_36":true,"state_37":true,"state_44":true},{"type":"person","title":"Cameron P. Metcalfe","state":"h","avail":true,"age":86,"state_2":true,"state_3":true,"state_13":true,"state_14":true,"state_21":true,"state_22":true,"state_29":true,"state_30":true,"state_33":true,"state_44":true,"state_47":true,"state_48":true},{"type":"person","title":"Abigail F. Davies","state":"h","avail":true,"age":64,"date":1188172800000.0,"state_2":true,"state_6":true,"state_10":true,"state_12":true,"state_17":true,"state_23":true,"state_26":true,"state_27":true,"state_33":true,"state_37":true,"state_40":true,"state_41":true,"state_42":true,"state_47":true}]},{"type":"role","title":"Idolize releases","children":[{"type":"person","title":"Luke Clarkson","avail":true,"age":72,"date":398131200000.0,"state_4":true,"state_5":true,"state_10":true,"state_16":true,"state_27":true,"state_37":true,"state_41":true,"state_44":true,"state_50":true},{"type":"person","title":"Kyle Z. Oliver","avail":true,"age":60,"date":701740800000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_7":true,"state_17":true,"state_20":true,"state_23":true,"state_28":true,"state_29":true,"state_31":true,"state_32":true,"state_37":true,"state_38":true,"state_41":true,"state_49":true,"state_50":true},{"type":"person","title":"Justin F. Graham","state":"s","avail":true,"age":73,"date":1664496000000.0,"state_6":true,"state_10":true,"state_19":true,"state_26":true,"state_32":true,"state_41":true,"state_45":true,"state_49":true},{"type":"person","title":"Kevin K. May","avail":true,"age":90,"state_2":true,"state_7":true,"state_13":true,"state_18":true,"state_24":true,"state_26":true,"state_29":true,"state_31":true,"state_38":true,"state_41":true,"state_44":true,"state_47":true},{"type":"person","title":"John A. McGrath","avail":true,"age":45,"date":886896000000.0,"state_1":true,"state_16":true,"state_23":true,"state_25":true,"state_29":true,"state_34":true,"state_43":true},{"type":"person","title":"Matt Q. Morgan","avail":true,"age":69,"date":1327017600000.0,"state_3":true,"state_12":true,"state_14":true,"state_16":true,"state_18":true,"state_23":true,"state_25":true,"state_35":true,"state_36":true,"state_37":true,"state_40":true,"state_41":true,"state_46":true},{"type":"person","title":"Michael X. Cornish","avail":true,"age":80,"state_5":true,"state_8":true,"state_11":true,"state_16":true,"state_22":true,"state_28":true,"state_29":true,"state_33":true,"state_39":true,"state_40":true},{"type":"person","title":"Phoenix Nolan","avail":true,"age":44,"state_1":true,"state_8":true,"state_13":true,"state_18":true,"state_22":true,"state_40":true,"state_42":true,"state_44":true,"state_45":true,"state_48":true},{"type":"person","title":"Donna E. May","state":"h","avail":true,"age":24,"date":410745600000.0,"state_8":true,"state_9":true,"state_10":true,"state_13":true,"state_23":true,"state_27":true,"state_29":true,"state_42":true,"state_49":true},{"type":"person","title":"Jesse Slater","avail":true,"age":33,"date":859680000000.0,"state_17":true,"state_20":true,"state_22":true,"state_23":true,"state_24":true,"state_28":true,"state_37":true,"state_38":true,"state_42":true,"state_43":true,"state_47":true},{"type":"person","title":"Matt I. Roberts","avail":true,"age":85,"state_2":true,"state_8":true,"state_16":true,"state_26":true,"state_28":true,"state_30":true,"state_31":true,"state_36":true,"state_37":true,"state_41":true,"state_44":true},{"type":"person","title":"Christopher X. Hemmings","state":"h","avail":true,"age":23,"date":1308268800000.0,"state_3":true,"state_5":true,"state_9":true,"state_13":true,"state_25":true,"state_27":true,"state_28":true,"state_46":true,"state_48":true,"state_49":true},{"type":"person","title":"Trevor X. Miller","avail":true,"age":95,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_1":true,"state_4":true,"state_5":true,"state_9":true,"state_12":true,"state_17":true,"state_23":true,"state_29":true,"state_34":true,"state_38":true,"state_41":true,"state_45":true,"state_50":true}]},{"type":"role","title":"Clean atmospheres"},{"type":"role","title":"Alight baths","children":[{"type":"person","title":"Ella R. McLean","avail":true,"age":30,"state_4":true,"state_8":true,"state_11":true,"state_19":true,"state_20":true,"state_29":true,"state_36":true,"state_37":true},{"type":"person","title":"Joe G. Peake","state":"s","avail":true,"age":67,"state_8":true,"state_17":true,"state_19":true,"state_24":true,"state_38":true,"state_45":true},{"type":"person","title":"Dylan Knox","avail":true,"age":58,"date":647481600000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_17":true,"state_21":true,"state_22":true,"state_23":true,"state_25":true,"state_28":true,"state_29":true,"state_34":true,"state_35":true,"state_39":true,"state_40":true,"state_47":true},{"type":"person","title":"Joshua Taylor","avail":true,"age":38,"date":1567900800000.0,"state_1":true,"state_5":true,"state_8":true,"state_9":true,"state_13":true,"state_29":true,"state_36":true,"state_47":true,"state_49":true},{"type":"person","title":"Dylan Y. Wilson","avail":true,"age":22,"date":521164800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_9":true,"state_14":true,"state_17":true,"state_20":true,"state_32":true,"state_34":true,"state_39":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Alexandra F. Thomson","avail":true,"age":75,"date":442195200000.0,"state_2":true,"state_7":true,"state_11":true,"state_15":true,"state_24":true,"state_28":true,"state_29":true,"state_30":true,"state_32":true,"state_33":true,"state_34":true,"state_35":true,"state_39":true,"state_40":true},{"type":"person","title":"Brian H. Allan","avail":true,"age":22,"state_21":true,"state_30":true,"state_33":true,"state_40":true,"state_42":true,"state_47":true,"state_48":true},{"type":"person","title":"Alexandra Marshall","state":"s","avail":true,"age":25,"date":862876800000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_5":true,"state_14":true,"state_16":true,"state_18":true,"state_21":true,"state_23":true,"state_33":true,"state_34":true,"state_43":true,"state_49":true,"state_50":true},{"type":"person","title":"Joseph H. Bailey","avail":true,"age":96,"state_6":true,"state_19":true,"state_25":true,"state_36":true,"state_37":true,"state_38":true,"state_42":true,"state_44":true},{"type":"person","title":"Phoenix R. Walker","avail":true,"age":73,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_2":true,"state_10":true,"state_16":true,"state_20":true,"state_25":true,"state_32":true,"state_38":true,"state_40":true,"state_42":true,"state_44":true,"state_47":true},{"type":"person","title":"Gabrielle Fraser","avail":true,"age":79,"date":339379200000.0,"state_3":true,"state_8":true,"state_22":true,"state_43":true,"state_48":true},{"type":"person","title":"Phoenix Dowd","avail":true,"age":33,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_8":true,"state_11":true,"state_20":true,"state_32":true,"state_36":true,"state_41":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Neil Sanderson","state":"h","avail":true,"age":69,"state_4":true,"state_6":true,"state_7":true,"state_8":true,"state_9":true,"state_13":true,"state_16":true,"state_33":true,"state_44":true,"state_46":true,"state_49":true}]},{"type":"role","title":"Construe temporaries","children":[{"type":"person","title":"Victor M. Underwood","avail":true,"age":42,"date":1625875200000.0,"state_1":true,"state_7":true,"state_8":true,"state_10":true,"state_14":true,"state_25":true,"state_26":true,"state_28":true,"state_37":true,"state_40":true,"state_41":true,"state_45":true},{"type":"person","title":"Zoe S. Bell","avail":true,"age":57,"date":1285891200000.0,"state_5":true,"state_9":true,"state_13":true,"state_15":true,"state_23":true,"state_42":true},{"type":"person","title":"Taylor E. MacLeod","state":"s","avail":true,"age":34,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_4":true,"state_8":true,"state_10":true,"state_24":true,"state_30":true,"state_31":true,"state_34":true,"state_36":true,"state_43":true,"state_47":true},{"type":"person","title":"Lily O. Forsyth","avail":true,"age":61,"date":1602201600000.0,"state_8":true,"state_12":true,"state_18":true,"state_24":true,"state_27":true,"state_28":true,"state_38":true},{"type":"person","title":"Owen Mitchell","state":"h","avail":true,"age":46,"state_9":true,"state_10":true,"state_15":true,"state_27":true,"state_30":true,"state_32":true,"state_33":true,"state_39":true,"state_43":true,"state_44":true}]}]},{"type":"department","title":"Dept. for Beats and Reputations","children":[{"type":"role","title":"Rob skunks","children":[{"type":"person","title":"Emily Terry","state":"s","avail":true,"age":39,"date":1306627200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_18":true,"state_25":true,"state_27":true,"state_33":true,"state_47":true}]},{"type":"role","title":"Grind yards","children":[{"type":"person","title":"Lily Hunter","avail":true,"age":48,"date":352512000000.0,"state_15":true,"state_24":true,"state_32":true,"state_35":true,"state_43":true,"state_50":true},{"type":"person","title":"David Hardacre","avail":true,"age":48,"date":672451200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_2":true,"state_12":true,"state_19":true,"state_31":true,"state_43":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Pat Glover","avail":true,"age":89,"date":451699200000.0,"state_8":true,"state_9":true,"state_10":true,"state_18":true,"state_27":true,"state_42":true,"state_50":true},{"type":"person","title":"Piers H. Buckland","avail":true,"age":47,"state_1":true,"state_2":true,"state_3":true,"state_4":true,"state_11":true,"state_14":true,"state_17":true,"state_23":true,"state_26":true,"state_28":true,"state_29":true,"state_35":true,"state_38":true,"state_44":true},{"type":"person","title":"Lily Terry","state":"h","age":72,"state_5":true,"state_7":true,"state_12":true,"state_27":true,"state_28":true,"state_30":true,"state_43":true,"state_45":true,"state_46":true},{"type":"person","title":"Ryan Rutherford","avail":true,"age":65,"date":1044489600000.0,"state_6":true,"state_8":true,"state_15":true,"state_17":true,"state_23":true,"state_25":true,"state_26":true,"state_39":true,"state_40":true},{"type":"person","title":"Olivia B. Langdon","age":34,"date":1618617600000.0,"state_3":true,"state_5":true,"state_20":true,"state_26":true,"state_34":true,"state_40":true,"state_45":true,"state_47":true},{"type":"person","title":"Dan R. Buckland","state":"h","avail":true,"age":42,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_5":true,"state_6":true,"state_8":true,"state_11":true,"state_12":true,"state_24":true,"state_32":true,"state_40":true,"state_42":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Stewart J. Berry","avail":true,"age":73,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_4":true,"state_12":true,"state_16":true,"state_24":true,"state_25":true,"state_26":true,"state_27":true,"state_41":true,"state_44":true,"state_45":true,"state_46":true},{"type":"person","title":"Chris Rampling","avail":true,"age":69,"state_4":true,"state_5":true,"state_11":true,"state_13":true,"state_19":true,"state_20":true,"state_24":true,"state_26":true,"state_27":true,"state_31":true,"state_40":true,"state_43":true,"state_48":true,"state_50":true},{"type":"person","title":"Kylie Wright","state":"s","avail":true,"age":78,"date":955065600000.0,"state_10":true,"state_11":true,"state_19":true,"state_20":true,"state_22":true,"state_24":true,"state_36":true,"state_48":true}]},{"type":"role","title":"Believe swimmings","children":[{"type":"person","title":"Katherine Quinn","state":"s","avail":true,"age":74,"state_10":true,"state_18":true,"state_19":true,"state_23":true,"state_24":true,"state_26":true,"state_27":true,"state_38":true,"state_40":true,"state_42":true,"state_47":true,"state_50":true},{"type":"person","title":"Maria Slater","avail":true,"age":53,"date":329270400000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_3":true,"state_10":true,"state_12":true,"state_18":true,"state_23":true,"state_27":true,"state_35":true,"state_42":true},{"type":"person","title":"Audrey Ogden","avail":true,"age":35,"state_4":true,"state_5":true,"state_6":true,"state_13":true,"state_14":true,"state_17":true,"state_19":true,"state_21":true,"state_26":true,"state_29":true,"state_30":true,"state_33":true,"state_40":true,"state_42":true,"state_43":true,"state_44":true,"state_47":true,"state_48":true},{"type":"person","title":"Austin C. Davidson","state":"h","avail":true,"age":48,"state_2":true,"state_5":true,"state_14":true,"state_23":true,"state_27":true,"state_36":true,"state_40":true,"state_49":true},{"type":"person","title":"Pippa Hamilton","age":40,"date":635644800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_1":true,"state_3":true,"state_5":true,"state_14":true,"state_25":true,"state_26":true,"state_28":true,"state_36":true,"state_40":true,"state_47":true},{"type":"person","title":"Jack S. Terry","avail":true,"age":92,"date":817257600000.0,"state_1":true,"state_13":true,"state_22":true,"state_23":true,"state_25":true,"state_42":true,"state_44":true},{"type":"person","title":"Vanessa Mackay","avail":true,"age":34,"date":242524800000.0,"state_8":true,"state_10":true,"state_15":true,"state_19":true,"state_23":true,"state_27":true,"state_28":true,"state_30":true,"state_32":true,"state_36":true,"state_46":true,"state_47":true,"state_48":true}]},{"type":"role","title":"Modify departures","children":[{"type":"person","title":"Audrey Wilson","avail":true,"age":34,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_1":true,"state_5":true,"state_6":true,"state_8":true,"state_15":true,"state_17":true,"state_25":true,"state_29":true,"state_45":true,"state_46":true,"state_48":true}]},{"type":"role","title":"Vomit tigers","children":[{"type":"person","title":"Carl T. Russell","avail":true,"age":69,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_8":true,"state_11":true,"state_13":true,"state_18":true,"state_38":true,"state_42":true,"state_43":true,"state_44":true,"state_46":true,"state_49":true},{"type":"person","title":"Joan J. Hardacre","avail":true,"age":70,"date":1250035200000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_10":true,"state_18":true,"state_19":true,"state_20":true,"state_22":true,"state_31":true,"state_33":true,"state_46":true},{"type":"person","title":"Max Howard","avail":true,"age":40,"state_2":true,"state_18":true,"state_19":true,"state_21":true,"state_22":true,"state_24":true,"state_37":true,"state_42":true,"state_46":true},{"type":"person","title":"Jan Ball","state":"h","avail":true,"age":50,"date":1669420800000.0,"state_1":true,"state_3":true,"state_13":true,"state_20":true,"state_21":true,"state_23":true,"state_24":true,"state_41":true}]},{"type":"role","title":"Grab preparations","children":[{"type":"person","title":"Corey McLean","avail":true,"age":29,"date":654134400000.0,"state_2":true,"state_9":true,"state_13":true,"state_14":true,"state_18":true,"state_24":true,"state_27":true,"state_29":true,"state_36":true,"state_48":true},{"type":"person","title":"Victor Hardacre","avail":true,"age":83,"date":206409600000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_1":true,"state_6":true,"state_15":true,"state_18":true,"state_20":true,"state_26":true,"state_33":true,"state_36":true,"state_37":true,"state_41":true,"state_42":true},{"type":"person","title":"Oliver Y. Martin","state":"s","avail":true,"age":74,"state_4":true,"state_8":true,"state_10":true,"state_16":true,"state_21":true,"state_24":true,"state_31":true,"state_33":true,"state_36":true,"state_39":true,"state_41":true,"state_47":true},{"type":"person","title":"Phil L. Underwood","age":66,"date":532396800000.0,"state_2":true,"state_3":true,"state_8":true,"state_9":true,"state_11":true,"state_18":true,"state_29":true,"state_37":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Amy Cameron","state":"h","avail":true,"age":54,"date":794102400000.0,"state_2":true,"state_5":true,"state_11":true,"state_12":true,"state_16":true,"state_26":true,"state_31":true,"state_34":true,"state_35":true,"state_46":true},{"type":"person","title":"Victoria North","avail":true,"age":27,"state_10":true,"state_18":true,"state_19":true,"state_22":true,"state_35":true},{"type":"person","title":"Mary Rutherford","avail":true,"age":94,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_4":true,"state_13":true,"state_15":true,"state_35":true,"state_36":true,"state_41":true,"state_46":true,"state_48":true},{"type":"person","title":"Alexander T. Wilkins","avail":true,"age":69,"date":439603200000.0,"state_2":true,"state_10":true,"state_12":true,"state_15":true,"state_28":true,"state_31":true,"state_36":true,"state_37":true,"state_38":true,"state_41":true,"state_43":true,"state_44":true,"state_49":true},{"type":"person","title":"Anne C. Hemmings","age":95,"date":708739200000.0,"state_2":true,"state_7":true,"state_10":true,"state_16":true,"state_23":true,"state_27":true,"state_28":true,"state_42":true,"state_48":true},{"type":"person","title":"Sarah U. James","state":"s","avail":true,"age":51,"date":424569600000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_4":true,"state_5":true,"state_7":true,"state_11":true,"state_12":true,"state_13":true,"state_15":true,"state_19":true,"state_21":true,"state_28":true,"state_36":true,"state_40":true,"state_44":true,"state_45":true,"state_48":true}]},{"type":"role","title":"Opt criticisms","children":[{"type":"person","title":"Wendy C. Kelly","age":34,"state_5":true,"state_7":true,"state_8":true,"state_11":true,"state_29":true,"state_33":true,"state_36":true,"state_40":true,"state_41":true,"state_43":true},{"type":"person","title":"Wendy Brown","avail":true,"age":95,"state_21":true,"state_23":true,"state_32":true,"state_36":true,"state_43":true,"state_44":true,"state_47":true},{"type":"person","title":"Dylan McDonald","avail":true,"age":40,"date":598838400000.0,"state_13":true,"state_25":true,"state_27":true,"state_32":true,"state_38":true,"state_41":true,"state_45":true,"state_46":true,"state_47":true,"state_48":true,"state_50":true}]},{"type":"role","title":"Convert staies","children":[{"type":"person","title":"Liam I. Chapman","avail":true,"age":54,"date":384134400000.0,"state_1":true,"state_15":true,"state_27":true,"state_34":true,"state_41":true,"state_42":true,"state_44":true,"state_48":true},{"type":"person","title":"Vanessa Slater","avail":true,"age":22,"date":305596800000.0,"state_3":true,"state_11":true,"state_17":true,"state_22":true,"state_27":true,"state_28":true,"state_30":true,"state_31":true,"state_37":true,"state_40":true,"state_41":true,"state_50":true},{"type":"person","title":"Dylan Springer","avail":true,"age":61,"date":313977600000.0,"state_13":true,"state_15":true,"state_16":true,"state_21":true,"state_23":true,"state_26":true,"state_30":true,"state_31":true,"state_37":true,"state_42":true,"state_43":true,"state_49":true},{"type":"person","title":"Ruth Piper","avail":true,"age":78,"state_5":true,"state_6":true,"state_7":true,"state_8":true,"state_11":true,"state_18":true,"state_25":true,"state_26":true,"state_30":true,"state_35":true,"state_44":true}]},{"type":"role","title":"Nap schedules","children":[{"type":"person","title":"Adam Poole","age":93,"state_5":true,"state_7":true,"state_9":true,"state_16":true,"state_18":true,"state_19":true,"state_21":true,"state_24":true,"state_28":true,"state_32":true,"state_33":true,"state_34":true,"state_35":true,"state_42":true,"state_45":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Jasmine Wilson","state":"h","avail":true,"age":40,"state_4":true,"state_7":true,"state_10":true,"state_14":true,"state_20":true,"state_21":true,"state_27":true,"state_34":true,"state_35":true,"state_41":true,"state_45":true,"state_47":true}]},{"type":"role","title":"Solicit lacks","children":[{"type":"person","title":"Karen Lyman","avail":true,"age":44,"date":169603200000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_7":true,"state_8":true,"state_16":true,"state_23":true,"state_24":true,"state_27":true,"state_41":true,"state_49":true},{"type":"person","title":"Jesse A. Newman","state":"s","avail":true,"age":84,"date":233107200000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_1":true,"state_3":true,"state_15":true,"state_26":true,"state_28":true,"state_44":true},{"type":"person","title":"Toby P. Dyer","avail":true,"age":85,"date":653529600000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_9":true,"state_19":true,"state_32":true,"state_46":true,"state_48":true},{"type":"person","title":"Alan Nash","avail":true,"age":69,"state_6":true,"state_16":true,"state_19":true,"state_33":true,"state_37":true,"state_39":true,"state_46":true,"state_47":true},{"type":"person","title":"David P. Hill","avail":true,"age":72,"date":712713600000.0,"state_9":true,"state_18":true,"state_19":true,"state_21":true,"state_22":true,"state_27":true,"state_33":true,"state_35":true,"state_41":true,"state_42":true,"state_45":true,"state_48":true},{"type":"person","title":"Daryl Q. Peake","avail":true,"age":29,"date":755568000000.0,"state_2":true,"state_5":true,"state_12":true,"state_23":true,"state_32":true,"state_35":true,"state_43":true,"state_47":true,"state_49":true},{"type":"person","title":"Casey I. Randall","avail":true,"age":38,"date":1572307200000.0,"state_3":true,"state_7":true,"state_10":true,"state_18":true,"state_19":true,"state_21":true,"state_24":true,"state_31":true,"state_38":true,"state_39":true,"state_41":true,"state_42":true,"state_47":true,"state_50":true},{"type":"person","title":"Casey W. McDonald","age":92,"date":79574400000.0,"state_3":true,"state_14":true,"state_17":true,"state_19":true,"state_40":true,"state_47":true},{"type":"person","title":"Joe B. Hughes","avail":true,"age":63,"date":257644800000.0,"state_1":true,"state_4":true,"state_6":true,"state_7":true,"state_12":true,"state_13":true,"state_17":true,"state_23":true,"state_25":true,"state_37":true,"state_41":true,"state_42":true,"state_43":true,"state_47":true},{"type":"person","title":"Zoe D. Thomson","state":"s","avail":true,"age":52,"date":892080000000.0,"state_3":true,"state_4":true,"state_6":true,"state_9":true,"state_10":true,"state_12":true,"state_15":true,"state_20":true,"state_31":true,"state_38":true,"state_42":true,"state_46":true,"state_47":true},{"type":"person","title":"Jamie Ogden","state":"h","avail":true,"age":62,"date":354240000000.0,"state_3":true,"state_5":true,"state_8":true,"state_12":true,"state_13":true,"state_14":true,"state_20":true,"state_23":true,"state_24":true,"state_25":true,"state_28":true,"state_29":true,"state_30":true,"state_31":true,"state_33":true,"state_37":true,"state_41":true,"state_49":true},{"type":"person","title":"Abigail Turner","avail":true,"age":62,"date":745113600000.0,"state_2":true,"state_5":true,"state_8":true,"state_13":true,"state_16":true,"state_17":true,"state_24":true,"state_25":true,"state_27":true,"state_29":true,"state_42":true,"state_45":true},{"type":"person","title":"Leonard Berry","age":71,"date":1473292800000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_4":true,"state_13":true,"state_19":true,"state_28":true,"state_31":true,"state_35":true,"state_37":true,"state_38":true,"state_43":true},{"type":"person","title":"Jonathan Vaughan","state":"s","age":45,"date":1532649600000.0,"state_1":true,"state_5":true,"state_7":true,"state_8":true,"state_20":true,"state_24":true,"state_25":true,"state_26":true,"state_32":true,"state_33":true,"state_36":true,"state_42":true,"state_47":true},{"type":"person","title":"Audrey U. Ince","avail":true,"age":57,"state_6":true,"state_23":true,"state_24":true,"state_25":true,"state_26":true,"state_34":true,"state_37":true,"state_44":true,"state_46":true},{"type":"person","title":"Samantha H. Clarkson","avail":true,"age":65,"state_3":true,"state_7":true,"state_13":true,"state_20":true,"state_21":true,"state_25":true,"state_28":true,"state_32":true,"state_34":true,"state_35":true,"state_36":true,"state_39":true},{"type":"person","title":"Kyle U. Dickens","state":"s","avail":true,"age":94,"date":55814400000.0,"state_3":true,"state_5":true,"state_11":true,"state_13":true,"state_15":true,"state_22":true,"state_23":true,"state_25":true,"state_26":true,"state_27":true,"state_35":true,"state_37":true,"state_42":true},{"type":"person","title":"Jean Ogden","avail":true,"age":50,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_14":true,"state_17":true,"state_30":true,"state_35":true,"state_36":true,"state_38":true,"state_49":true,"state_50":true},{"type":"person","title":"Max V. Forsyth","avail":true,"age":21,"date":789004800000.0,"state_4":true,"state_9":true,"state_15":true,"state_18":true,"state_26":true,"state_28":true,"state_30":true,"state_34":true,"state_39":true,"state_40":true,"state_44":true},{"type":"person","title":"Alison F. Thomson","state":"s","avail":true,"age":75,"state_2":true,"state_14":true,"state_20":true,"state_21":true,"state_23":true,"state_25":true,"state_26":true,"state_29":true,"state_35":true,"state_36":true,"state_41":true,"state_46":true},{"type":"person","title":"Ryan A. Sanderson","state":"s","avail":true,"age":77,"date":883699200000.0,"state_1":true,"state_4":true,"state_9":true,"state_10":true,"state_14":true,"state_18":true,"state_20":true,"state_27":true,"state_39":true,"state_44":true,"state_45":true,"state_48":true}]},{"type":"role","title":"Choose blames","children":[{"type":"person","title":"Bernadette White","avail":true,"age":21,"state_3":true,"state_11":true,"state_14":true,"state_21":true,"state_27":true,"state_35":true,"state_46":true,"state_49":true},{"type":"person","title":"Anne S. May","avail":true,"age":79,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_2":true,"state_3":true,"state_18":true,"state_30":true,"state_31":true,"state_44":true,"state_47":true},{"type":"person","title":"Ryan K. McLean","avail":true,"age":55,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_8":true,"state_32":true,"state_35":true,"state_40":true,"state_42":true,"state_48":true},{"type":"person","title":"Alex E. Gray","state":"s","avail":true,"age":60,"state_6":true,"state_9":true,"state_10":true,"state_15":true,"state_17":true,"state_27":true,"state_28":true,"state_29":true,"state_34":true,"state_40":true,"state_46":true},{"type":"person","title":"Jennifer R. Howard","avail":true,"age":98,"state_8":true,"state_10":true,"state_17":true,"state_18":true,"state_20":true,"state_34":true,"state_37":true,"state_42":true,"state_46":true,"state_48":true},{"type":"person","title":"Samantha U. Campbell","avail":true,"age":56,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_9":true,"state_10":true,"state_18":true,"state_19":true,"state_23":true,"state_39":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Audrey B. Kerr","age":24,"date":328060800000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_6":true,"state_13":true,"state_27":true,"state_29":true,"state_30":true,"state_32":true,"state_33":true,"state_39":true,"state_40":true,"state_42":true,"state_46":true},{"type":"person","title":"Jean Young","avail":true,"age":71,"date":1101686400000.0,"state_2":true,"state_4":true,"state_6":true,"state_9":true,"state_13":true,"state_16":true,"state_31":true,"state_35":true,"state_50":true},{"type":"person","title":"Alison Hunter","state":"s","age":44,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_8":true,"state_10":true,"state_11":true,"state_15":true,"state_17":true,"state_22":true,"state_26":true,"state_36":true,"state_37":true,"state_39":true,"state_41":true,"state_44":true,"state_48":true},{"type":"person","title":"Rachel V. Ellison","age":68,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_4":true,"state_9":true,"state_10":true,"state_16":true,"state_17":true,"state_23":true,"state_25":true,"state_28":true,"state_36":true,"state_48":true,"state_50":true}]}]},{"type":"department","title":"Dept. for Snows and Scratches","children":[{"type":"role","title":"Forsake picks","children":[{"type":"person","title":"Jayden N. Coleman","state":"s","avail":true,"age":24,"date":1016668800000.0,"state_2":true,"state_14":true,"state_15":true,"state_17":true,"state_29":true,"state_33":true,"state_39":true,"state_43":true},{"type":"person","title":"Jason Springer","state":"h","avail":true,"age":25,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_3":true,"state_7":true,"state_15":true,"state_18":true,"state_22":true,"state_23":true,"state_25":true,"state_28":true,"state_35":true,"state_37":true,"state_38":true,"state_40":true,"state_42":true,"state_43":true,"state_45":true,"state_50":true},{"type":"person","title":"Jean R. Powell","avail":true,"age":35,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_13":true,"state_22":true,"state_24":true,"state_27":true,"state_35":true,"state_37":true,"state_38":true,"state_47":true,"state_49":true},{"type":"person","title":"Daryl Gibson","avail":true,"age":82,"state_6":true,"state_18":true,"state_22":true,"state_23":true,"state_33":true,"state_35":true,"state_45":true,"state_49":true,"state_50":true},{"type":"person","title":"Molly Walker","state":"h","avail":true,"age":74,"date":687398400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_3":true,"state_17":true,"state_20":true,"state_25":true,"state_35":true},{"type":"person","title":"Alexander North","avail":true,"age":55,"date":1583193600000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_2":true,"state_3":true,"state_5":true,"state_8":true,"state_12":true,"state_13":true,"state_17":true,"state_31":true,"state_40":true,"state_41":true,"state_45":true},{"type":"person","title":"Sebastian Hamilton","state":"s","avail":true,"age":47,"date":1323734400000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_17":true,"state_22":true,"state_25":true,"state_30":true,"state_33":true,"state_35":true,"state_45":true},{"type":"person","title":"Jake A. Hodges","avail":true,"age":50,"state_2":true,"state_4":true,"state_12":true,"state_28":true,"state_46":true,"state_50":true},{"type":"person","title":"Kelly W. MacLeod","avail":true,"age":32,"date":1193356800000.0,"state_10":true,"state_11":true,"state_16":true,"state_18":true,"state_30":true,"state_32":true,"state_38":true,"state_39":true,"state_45":true,"state_46":true,"state_50":true},{"type":"person","title":"Julia H. Metcalfe","avail":true,"age":86,"date":555379200000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_5":true,"state_10":true,"state_12":true,"state_17":true,"state_19":true,"state_24":true,"state_29":true,"state_33":true,"state_39":true,"state_40":true,"state_43":true,"state_49":true},{"type":"person","title":"Dorothy Churchill","avail":true,"age":44,"state_25":true,"state_27":true,"state_29":true,"state_34":true,"state_44":true,"state_45":true,"state_49":true},{"type":"person","title":"Nicholas G. Piper","avail":true,"age":51,"date":1647734400000.0,"state_3":true,"state_4":true,"state_8":true,"state_16":true,"state_19":true,"state_20":true,"state_21":true,"state_23":true,"state_31":true,"state_32":true,"state_33":true,"state_43":true,"state_44":true},{"type":"person","title":"Dorothy Carr","avail":true,"age":43,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_2":true,"state_5":true,"state_8":true,"state_32":true,"state_40":true,"state_42":true,"state_45":true},{"type":"person","title":"Gordon Berry","age":79,"date":292118400000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_6":true,"state_11":true,"state_21":true,"state_28":true,"state_30":true,"state_36":true,"state_43":true},{"type":"person","title":"Dan Grant","age":22,"date":1406160000000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_2":true,"state_4":true,"state_14":true,"state_15":true,"state_17":true,"state_21":true,"state_31":true,"state_35":true,"state_38":true,"state_42":true,"state_43":true,"state_47":true}]},{"type":"role","title":"Show clocks","children":[{"type":"person","title":"Alexander Ball","state":"h","age":53,"state_2":true,"state_3":true,"state_9":true,"state_12":true,"state_20":true,"state_21":true,"state_23":true,"state_34":true,"state_42":true,"state_46":true,"state_47":true},{"type":"person","title":"Sam Buckland","avail":true,"age":68,"date":142041600000.0,"state_5":true,"state_8":true,"state_12":true,"state_24":true,"state_25":true,"state_27":true,"state_29":true,"state_30":true,"state_31":true,"state_35":true,"state_41":true,"state_44":true,"state_47":true},{"type":"person","title":"Blake Davidson","avail":true,"age":74,"date":1373846400000.0,"state_1":true,"state_6":true,"state_9":true,"state_11":true,"state_12":true,"state_13":true,"state_22":true,"state_26":true,"state_27":true,"state_35":true,"state_40":true,"state_46":true,"state_49":true},{"type":"person","title":"Luke I. Short","state":"s","avail":true,"age":36,"state_2":true,"state_10":true,"state_17":true,"state_22":true,"state_25":true,"state_36":true,"state_39":true,"state_41":true,"state_43":true,"state_47":true,"state_49":true},{"type":"person","title":"Vanessa X. Murray","avail":true,"age":64,"state_4":true,"state_11":true,"state_26":true,"state_34":true,"state_39":true,"state_40":true,"state_44":true,"state_47":true},{"type":"person","title":"Irene Ball","avail":true,"age":88,"date":781920000000.0,"state_1":true,"state_7":true,"state_9":true,"state_14":true,"state_16":true,"state_20":true,"state_25":true,"state_27":true,"state_29":true,"state_37":true,"state_41":true,"state_45":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Amanda Johnston","avail":true,"age":85,"state_4":true,"state_5":true,"state_11":true,"state_15":true,"state_21":true,"state_31":true,"state_45":true,"state_46":true},{"type":"person","title":"Steven Mitchell","state":"h","avail":true,"age":93,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_8":true,"state_16":true,"state_19":true,"state_21":true,"state_25":true,"state_42":true,"state_50":true},{"type":"person","title":"Sebastian U. Walker","avail":true,"age":45,"date":290390400000.0,"state_5":true,"state_7":true,"state_14":true,"state_18":true,"state_24":true,"state_27":true,"state_30":true,"state_37":true,"state_40":true,"state_45":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Lillian P. Paterson","avail":true,"age":40,"date":421632000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_2":true,"state_6":true,"state_24":true,"state_25":true,"state_30":true,"state_37":true,"state_39":true,"state_47":true},{"type":"person","title":"Paul Q. Arnold","state":"h","avail":true,"age":91,"date":694915200000.0,"state_8":true,"state_11":true,"state_12":true,"state_29":true,"state_38":true,"state_40":true,"state_44":true,"state_45":true},{"type":"person","title":"Brian Wallace","state":"h","avail":true,"age":31,"date":1504224000000.0,"state_7":true,"state_8":true,"state_13":true,"state_14":true,"state_41":true,"state_47":true},{"type":"person","title":"Gavin Powell","avail":true,"age":33,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_4":true,"state_6":true,"state_11":true,"state_19":true,"state_26":true,"state_36":true,"state_39":true,"state_42":true,"state_44":true}]},{"type":"role","title":"Seize individuals","children":[{"type":"person","title":"Frank Jones","state":"s","avail":true,"age":96,"date":1397347200000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_9":true,"state_26":true,"state_32":true,"state_34":true,"state_35":true,"state_44":true,"state_45":true,"state_46":true,"state_47":true},{"type":"person","title":"Ryan Ellison","avail":true,"age":87,"date":1066089600000.0,"state_7":true,"state_17":true,"state_30":true,"state_31":true,"state_39":true,"state_44":true},{"type":"person","title":"Diane Newman","avail":true,"age":24,"state_1":true,"state_2":true,"state_9":true,"state_14":true,"state_16":true,"state_18":true,"state_21":true,"state_24":true,"state_28":true,"state_33":true,"state_39":true,"state_41":true,"state_42":true,"state_45":true},{"type":"person","title":"Piers Allan","avail":true,"age":70,"state_14":true,"state_15":true,"state_18":true,"state_20":true,"state_24":true,"state_31":true,"state_32":true,"state_41":true,"state_43":true},{"type":"person","title":"Phoenix Z. Young","state":"h","avail":true,"age":40,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_6":true,"state_13":true,"state_15":true,"state_39":true,"state_42":true,"state_47":true},{"type":"person","title":"Cameron Rutherford","age":27,"date":817344000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_13":true,"state_17":true,"state_18":true,"state_23":true,"state_27":true,"state_28":true,"state_31":true,"state_44":true,"state_48":true},{"type":"person","title":"Jason Turner","avail":true,"age":77,"state_5":true,"state_15":true,"state_19":true,"state_23":true,"state_28":true,"state_32":true,"state_43":true},{"type":"person","title":"Irene T. Skinner","state":"h","avail":true,"age":74,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_4":true,"state_14":true,"state_24":true,"state_32":true,"state_39":true,"state_40":true}]},{"type":"role","title":"Violate secretaries","children":[{"type":"person","title":"Gordon H. Greene","age":97,"date":980899200000.0,"state_26":true,"state_34":true,"state_37":true,"state_44":true,"state_50":true},{"type":"person","title":"Evan Thomson","avail":true,"age":66,"date":863568000000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_2":true,"state_3":true,"state_19":true,"state_26":true,"state_45":true},{"type":"person","title":"Maria J. King","avail":true,"age":94,"date":1326499200000.0,"state_6":true,"state_21":true,"state_30":true,"state_32":true,"state_33":true,"state_40":true,"state_45":true,"state_47":true},{"type":"person","title":"Yvonne H. Nash","avail":true,"age":38,"date":1102032000000.0,"state_3":true,"state_5":true,"state_11":true,"state_15":true,"state_25":true,"state_28":true,"state_31":true},{"type":"person","title":"Dylan F. Short","avail":true,"age":61,"date":482976000000.0,"state_2":true,"state_7":true,"state_9":true,"state_11":true,"state_13":true,"state_14":true,"state_26":true,"state_36":true,"state_37":true,"state_42":true,"state_45":true},{"type":"person","title":"Katherine Miller","age":84,"date":937267200000.0,"state_13":true,"state_17":true,"state_19":true,"state_25":true,"state_30":true,"state_41":true,"state_43":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Pippa U. Stewart","avail":true,"age":57,"date":1266105600000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_1":true,"state_2":true,"state_4":true,"state_5":true,"state_10":true,"state_11":true,"state_13":true,"state_17":true,"state_23":true,"state_28":true,"state_32":true,"state_37":true,"state_50":true},{"type":"person","title":"William P. Wallace","avail":true,"age":22,"date":27648000000.0,"state_12":true,"state_19":true,"state_22":true,"state_26":true,"state_28":true,"state_39":true,"state_49":true},{"type":"person","title":"Justin D. Gray","state":"h","avail":true,"age":83,"state_3":true,"state_11":true,"state_13":true,"state_14":true,"state_17":true,"state_20":true,"state_22":true,"state_37":true,"state_45":true},{"type":"person","title":"Joanne Y. Mathis","avail":true,"age":57,"date":374284800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_8":true,"state_30":true,"state_31":true,"state_33":true,"state_36":true,"state_49":true},{"type":"person","title":"Fiona K. Hodges","avail":true,"age":25,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_1":true,"state_3":true,"state_6":true,"state_8":true,"state_20":true,"state_23":true,"state_24":true,"state_32":true,"state_45":true,"state_49":true,"state_50":true},{"type":"person","title":"Julian Robertson","age":88,"date":732758400000.0,"state_5":true,"state_7":true,"state_17":true,"state_31":true},{"type":"person","title":"Fiona Kelly","avail":true,"age":55,"date":150508800000.0,"state_1":true,"state_2":true,"state_8":true,"state_15":true,"state_18":true,"state_19":true,"state_23":true,"state_24":true,"state_36":true,"state_47":true},{"type":"person","title":"Gavin C. Nash","age":25,"date":955584000000.0,"state_1":true,"state_5":true,"state_12":true,"state_20":true,"state_25":true,"state_27":true,"state_33":true,"state_39":true,"state_43":true,"state_46":true,"state_48":true},{"type":"person","title":"Andrew M. Berry","avail":true,"age":29,"date":1476403200000.0,"state_2":true,"state_7":true,"state_22":true,"state_26":true,"state_30":true,"state_31":true,"state_34":true,"state_35":true,"state_37":true,"state_43":true,"state_49":true},{"type":"person","title":"Abigail Davies","state":"s","avail":true,"age":22,"state_2":true,"state_5":true,"state_11":true,"state_19":true,"state_20":true,"state_31":true,"state_35":true,"state_42":true,"state_46":true},{"type":"person","title":"Jennifer L. Thomson","avail":true,"age":25,"date":1014595200000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_8":true,"state_14":true,"state_17":true,"state_21":true,"state_24":true,"state_37":true,"state_43":true,"state_49":true,"state_50":true},{"type":"person","title":"Sydney Z. Springer","state":"h","avail":true,"age":75,"state_14":true,"state_15":true,"state_19":true,"state_20":true,"state_25":true,"state_29":true,"state_30":true,"state_33":true,"state_35":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Alexandra M. Mathis","avail":true,"age":73,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_4":true,"state_7":true,"state_13":true,"state_22":true,"state_24":true,"state_32":true,"state_38":true,"state_39":true,"state_41":true,"state_43":true,"state_45":true,"state_48":true},{"type":"person","title":"Keith MacDonald","state":"h","avail":true,"age":83,"date":651628800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_5":true,"state_6":true,"state_12":true,"state_20":true}]},{"type":"role","title":"Forbid students","children":[{"type":"person","title":"James Bond","avail":true,"age":94,"date":879552000000.0,"state_5":true,"state_16":true,"state_41":true},{"type":"person","title":"Bobbie Short","avail":true,"age":87,"date":391392000000.0,"state_2":true,"state_16":true,"state_25":true,"state_28":true,"state_31":true,"state_33":true,"state_35":true,"state_37":true,"state_38":true,"state_43":true,"state_46":true,"state_50":true},{"type":"person","title":"William Jones","state":"s","avail":true,"age":41,"state_12":true,"state_13":true,"state_16":true,"state_17":true,"state_18":true,"state_20":true,"state_21":true,"state_36":true,"state_39":true,"state_44":true,"state_45":true,"state_49":true},{"type":"person","title":"Jamie Bell","avail":true,"age":95,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_5":true,"state_8":true,"state_12":true,"state_17":true,"state_40":true,"state_42":true},{"type":"person","title":"Stephen C. Henderson","state":"h","avail":true,"age":98,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_5":true,"state_6":true,"state_10":true,"state_18":true,"state_20":true,"state_25":true,"state_26":true,"state_29":true,"state_36":true,"state_37":true},{"type":"person","title":"Gordon R. Edmunds","age":67,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_11":true,"state_20":true,"state_30":true,"state_31":true,"state_34":true,"state_41":true,"state_50":true},{"type":"person","title":"Joshua Q. MacDonald","avail":true,"age":77,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_12":true,"state_17":true,"state_19":true,"state_20":true,"state_24":true,"state_28":true,"state_32":true,"state_33":true,"state_36":true,"state_37":true,"state_41":true,"state_43":true,"state_44":true,"state_45":true},{"type":"person","title":"Cameron Hart","avail":true,"age":37,"date":301104000000.0,"state_4":true,"state_5":true,"state_12":true,"state_15":true,"state_22":true,"state_23":true,"state_24":true,"state_25":true,"state_32":true,"state_33":true,"state_46":true},{"type":"person","title":"Jesse Hudson","avail":true,"age":80,"date":1168214400000.0,"state_4":true,"state_18":true,"state_29":true,"state_35":true,"state_38":true,"state_42":true,"state_43":true,"state_45":true,"state_46":true}]},{"type":"role","title":"Increase boyfriends","children":[{"type":"person","title":"Liam Greene","state":"s","avail":true,"age":74,"state_5":true,"state_17":true,"state_21":true,"state_24":true,"state_28":true,"state_30":true,"state_50":true},{"type":"person","title":"Jasmine P. Walsh","state":"h","avail":true,"age":94,"state_8":true,"state_19":true,"state_26":true,"state_33":true,"state_35":true,"state_40":true},{"type":"person","title":"Yvonne Buckland","avail":true,"age":27,"date":492220800000.0,"state_1":true,"state_6":true,"state_7":true,"state_24":true,"state_27":true,"state_29":true,"state_36":true,"state_37":true,"state_42":true,"state_50":true},{"type":"person","title":"Jamie Ross","avail":true,"age":36,"state_3":true,"state_4":true,"state_5":true,"state_7":true,"state_9":true,"state_10":true,"state_13":true,"state_21":true,"state_23":true,"state_24":true,"state_34":true,"state_37":true,"state_41":true,"state_42":true},{"type":"person","title":"Jamie Wilson","state":"s","avail":true,"age":21,"date":1498435200000.0,"state_4":true,"state_5":true,"state_11":true,"state_22":true,"state_27":true,"state_29":true,"state_32":true},{"type":"person","title":"Chloe Johnston","age":71,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_2":true,"state_3":true,"state_7":true,"state_8":true,"state_9":true,"state_14":true,"state_18":true,"state_29":true,"state_30":true,"state_31":true,"state_32":true,"state_38":true,"state_45":true,"state_50":true},{"type":"person","title":"Daryl Bell","avail":true,"age":26,"state_2":true,"state_3":true,"state_7":true,"state_12":true,"state_18":true,"state_22":true,"state_27":true,"state_28":true,"state_30":true,"state_36":true,"state_40":true,"state_42":true,"state_50":true},{"type":"person","title":"Frank Bond","state":"s","avail":true,"age":55,"date":729993600000.0,"state_3":true,"state_5":true,"state_10":true,"state_12":true,"state_20":true,"state_25":true,"state_28":true,"state_32":true,"state_35":true,"state_39":true,"state_42":true,"state_46":true,"state_50":true},{"type":"person","title":"Julian Martin","avail":true,"age":77,"date":650851200000.0,"state_2":true,"state_5":true,"state_6":true,"state_14":true,"state_17":true,"state_22":true,"state_38":true,"state_42":true,"state_49":true},{"type":"person","title":"Max Sharp","avail":true,"age":25,"state_1":true,"state_12":true,"state_14":true,"state_38":true,"state_41":true,"state_47":true},{"type":"person","title":"Jean Z. Ellison","avail":true,"age":33,"date":899510400000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_10":true,"state_12":true,"state_21":true,"state_23":true,"state_30":true,"state_37":true,"state_46":true,"state_49":true},{"type":"person","title":"Phoenix O. McGrath","avail":true,"age":55,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_4":true,"state_5":true,"state_9":true,"state_12":true,"state_15":true,"state_19":true,"state_28":true,"state_29":true,"state_30":true,"state_33":true,"state_37":true,"state_40":true,"state_49":true,"state_50":true},{"type":"person","title":"Anne Peake","avail":true,"age":52,"date":777254400000.0,"state_3":true,"state_5":true,"state_13":true,"state_14":true,"state_20":true,"state_21":true,"state_29":true,"state_30":true,"state_35":true,"state_37":true,"state_38":true,"state_41":true,"state_42":true,"state_43":true,"state_46":true},{"type":"person","title":"Ian Walsh","avail":true,"age":42,"state_3":true,"state_5":true,"state_6":true,"state_8":true,"state_16":true,"state_33":true,"state_38":true,"state_40":true,"state_46":true,"state_47":true,"state_48":true,"state_49":true,"state_50":true},{"type":"person","title":"Corey Vance","avail":true,"age":69,"state_1":true,"state_14":true,"state_22":true,"state_23":true,"state_24":true,"state_27":true,"state_29":true,"state_30":true,"state_36":true,"state_40":true,"state_47":true},{"type":"person","title":"Mary Paige","avail":true,"age":58,"date":777513600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_1":true,"state_3":true,"state_5":true,"state_7":true,"state_11":true,"state_13":true,"state_15":true,"state_18":true,"state_22":true,"state_29":true,"state_34":true},{"type":"person","title":"Max Alsop","avail":true,"age":24,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_5":true,"state_8":true,"state_11":true,"state_27":true,"state_29":true,"state_37":true,"state_38":true,"state_40":true,"state_42":true,"state_45":true},{"type":"person","title":"Jamie Smith","avail":true,"age":54,"state_18":true,"state_21":true,"state_28":true,"state_37":true}]},{"type":"role","title":"Cover foods","children":[{"type":"person","title":"Jayden N. Lee","state":"s","avail":true,"age":45,"state_8":true,"state_13":true,"state_14":true,"state_20":true,"state_34":true,"state_37":true,"state_38":true,"state_40":true,"state_43":true,"state_45":true},{"type":"person","title":"Stephen E. Ince","avail":true,"age":47,"state_4":true,"state_6":true,"state_8":true,"state_13":true,"state_16":true,"state_18":true,"state_20":true,"state_23":true,"state_24":true,"state_29":true,"state_38":true,"state_46":true,"state_50":true}]},{"type":"role","title":"Cower strengths","children":[{"type":"person","title":"Carolyn B. Dowd","avail":true,"age":95,"date":1672704000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_2":true,"state_4":true,"state_15":true,"state_16":true,"state_23":true,"state_27":true,"state_33":true,"state_35":true,"state_38":true,"state_44":true,"state_47":true},{"type":"person","title":"Daryl L. Reid","avail":true,"age":30,"date":1163289600000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_6":true,"state_7":true,"state_9":true,"state_10":true,"state_15":true,"state_18":true,"state_20":true,"state_25":true,"state_30":true,"state_31":true,"state_38":true,"state_41":true,"state_42":true,"state_50":true},{"type":"person","title":"Benjamin B. Vance","state":"h","avail":true,"age":58,"state_1":true,"state_8":true,"state_20":true,"state_23":true,"state_29":true,"state_30":true,"state_33":true,"state_37":true,"state_50":true}]},{"type":"role","title":"Vie agents","children":[{"type":"person","title":"Nathan C. May","avail":true,"age":74,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_19":true,"state_21":true,"state_42":true},{"type":"person","title":"Stewart V. Churchill","avail":true,"age":51,"state_2":true,"state_18":true,"state_22":true,"state_25":true,"state_26":true,"state_35":true,"state_45":true,"state_47":true},{"type":"person","title":"Caroline Johnston","avail":true,"age":95,"date":1081814400000.0,"state_1":true,"state_13":true,"state_26":true,"state_34":true,"state_35":true,"state_42":true,"state_43":true,"state_50":true},{"type":"person","title":"Andy Lambert","avail":true,"age":88,"state_7":true,"state_9":true,"state_11":true,"state_35":true,"state_46":true},{"type":"person","title":"Carl M. Avery","avail":true,"age":45,"date":1366588800000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_10":true,"state_20":true,"state_38":true,"state_40":true},{"type":"person","title":"Lucas Butler","state":"s","avail":true,"age":50,"date":541728000000.0,"state_2":true,"state_5":true,"state_9":true,"state_22":true,"state_24":true,"state_27":true,"state_38":true,"state_43":true,"state_45":true},{"type":"person","title":"Faith V. Reid","avail":true,"age":56,"date":1560988800000.0,"state_3":true,"state_6":true,"state_17":true,"state_25":true,"state_27":true,"state_30":true,"state_35":true,"state_36":true,"state_38":true,"state_47":true,"state_50":true}]},{"type":"role","title":"Decay governments","children":[{"type":"person","title":"Heather K. Russell","avail":true,"age":58,"date":1039046400000.0,"state_4":true,"state_6":true,"state_22":true,"state_26":true,"state_29":true,"state_30":true,"state_34":true,"state_37":true,"state_39":true,"state_42":true,"state_44":true,"state_45":true,"state_46":true,"state_47":true,"state_50":true},{"type":"person","title":"Rebecca Chapman","state":"s","age":33,"state_2":true,"state_5":true,"state_6":true,"state_13":true,"state_16":true,"state_17":true,"state_22":true,"state_24":true,"state_28":true,"state_31":true,"state_36":true,"state_38":true,"state_39":true},{"type":"person","title":"Amelia B. Glover","avail":true,"age":97,"date":1243900800000.0,"state_3":true,"state_5":true,"state_7":true,"state_11":true,"state_15":true,"state_25":true,"state_29":true,"state_30":true,"state_33":true,"state_37":true,"state_44":true,"state_50":true},{"type":"person","title":"Jennifer Mackay","avail":true,"age":32,"state_10":true,"state_15":true,"state_21":true,"state_30":true,"state_31":true,"state_39":true,"state_40":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"John Robertson","state":"h","avail":true,"age":77,"state_7":true,"state_16":true,"state_19":true,"state_29":true,"state_37":true,"state_38":true,"state_45":true,"state_49":true},{"type":"person","title":"Frances Rees","avail":true,"age":34,"date":1532822400000.0,"state_8":true,"state_10":true,"state_12":true,"state_15":true,"state_22":true,"state_23":true,"state_28":true,"state_38":true,"state_47":true,"state_48":true},{"type":"person","title":"Christian Skinner","avail":true,"age":94,"date":983836800000.0,"state_1":true,"state_16":true,"state_21":true,"state_27":true,"state_40":true,"state_43":true,"state_46":true,"state_47":true,"state_49":true},{"type":"person","title":"Alison Chapman","state":"s","avail":true,"age":85,"state_10":true,"state_16":true,"state_19":true,"state_23":true,"state_24":true,"state_25":true,"state_32":true},{"type":"person","title":"Carol S. Grant","state":"h","avail":true,"age":98,"date":1520899200000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_4":true,"state_5":true,"state_10":true,"state_13":true,"state_17":true,"state_23":true,"state_27":true,"state_31":true,"state_32":true,"state_44":true,"state_45":true,"state_47":true},{"type":"person","title":"Diana R. Sanderson","state":"s","avail":true,"age":67,"state_16":true,"state_23":true,"state_28":true,"state_33":true,"state_42":true,"state_50":true},{"type":"person","title":"Jamie Marshall","state":"s","avail":true,"age":92,"date":1386288000000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_3":true,"state_5":true,"state_6":true,"state_14":true,"state_22":true,"state_30":true,"state_33":true,"state_34":true,"state_43":true,"state_46":true,"state_49":true,"state_50":true}]},{"type":"role","title":"Recall gives","children":[{"type":"person","title":"Michael G. Fraser","avail":true,"age":92,"date":669427200000.0,"state_5":true,"state_6":true,"state_7":true,"state_11":true,"state_12":true,"state_21":true,"state_23":true,"state_24":true,"state_32":true,"state_37":true,"state_43":true,"state_47":true},{"type":"person","title":"Andrew L. Paterson","state":"h","avail":true,"age":46,"date":1618358400000.0,"state_1":true,"state_2":true,"state_4":true,"state_8":true,"state_9":true,"state_17":true,"state_20":true,"state_23":true,"state_24":true,"state_27":true,"state_28":true,"state_33":true,"state_35":true,"state_36":true,"state_39":true},{"type":"person","title":"Harry Z. Morrison","avail":true,"age":86,"date":1215043200000.0,"state_10":true,"state_14":true,"state_18":true,"state_20":true,"state_23":true,"state_35":true,"state_40":true,"state_44":true,"state_45":true},{"type":"person","title":"Tracey H. Coleman","avail":true,"age":73,"date":1320624000000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_2":true,"state_4":true,"state_15":true,"state_16":true,"state_37":true,"state_41":true,"state_44":true,"state_47":true,"state_50":true},{"type":"person","title":"Harry Y. Abraham","avail":true,"age":30,"date":1424044800000.0,"state_2":true,"state_5":true,"state_13":true,"state_15":true,"state_17":true,"state_27":true,"state_29":true,"state_50":true},{"type":"person","title":"Tracey B. Quinn","state":"s","age":98,"date":1721088000000.0,"state_6":true,"state_7":true,"state_9":true,"state_20":true,"state_21":true,"state_26":true,"state_27":true,"state_28":true,"state_47":true},{"type":"person","title":"Gavin H. Davidson","avail":true,"age":50,"state_2":true,"state_7":true,"state_10":true,"state_20":true,"state_21":true,"state_22":true,"state_23":true,"state_27":true,"state_31":true,"state_34":true,"state_35":true,"state_37":true,"state_42":true,"state_43":true,"state_50":true}]},{"type":"role","title":"Stare seconds","children":[{"type":"person","title":"Cameron Mathis","state":"s","avail":true,"age":74,"date":540950400000.0,"state_4":true,"state_5":true,"state_16":true,"state_19":true,"state_24":true,"state_29":true},{"type":"person","title":"Virginia Burgess","avail":true,"age":78,"state_4":true,"state_20":true,"state_26":true,"state_28":true,"state_33":true,"state_39":true,"state_40":true,"state_42":true,"state_45":true}]}]},{"type":"department","title":"Dept. for Principles and Officers","children":[{"type":"role","title":"Count assumptions","children":[{"type":"person","title":"Jessica Poole","avail":true,"age":62,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_2":true,"state_3":true,"state_5":true,"state_6":true,"state_8":true,"state_9":true,"state_15":true,"state_28":true,"state_40":true,"state_44":true,"state_46":true},{"type":"person","title":"Brandon Wilkins","avail":true,"age":46,"date":847670400000.0,"state_9":true,"state_20":true,"state_22":true,"state_25":true,"state_30":true,"state_37":true,"state_38":true,"state_41":true,"state_42":true},{"type":"person","title":"Oliver Z. King","avail":true,"age":93,"date":865382400000.0,"state_1":true,"state_10":true,"state_15":true,"state_16":true,"state_21":true,"state_23":true,"state_25":true,"state_30":true},{"type":"person","title":"Sue X. Morgan","avail":true,"age":42,"date":172454400000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_2":true,"state_3":true,"state_6":true,"state_9":true,"state_10":true,"state_13":true,"state_17":true,"state_38":true,"state_42":true,"state_46":true,"state_49":true},{"type":"person","title":"Mary Z. Reid","avail":true,"age":48,"date":896918400000.0,"state_4":true,"state_10":true,"state_17":true,"state_19":true,"state_20":true,"state_29":true,"state_37":true,"state_39":true,"state_47":true},{"type":"person","title":"Zoe Forsyth","avail":true,"age":71,"state_7":true,"state_8":true,"state_9":true,"state_11":true,"state_20":true,"state_25":true,"state_31":true,"state_34":true,"state_35":true},{"type":"person","title":"Brian Knox","avail":true,"age":27,"state_23":true,"state_26":true,"state_27":true,"state_28":true,"state_29":true,"state_31":true,"state_36":true,"state_41":true,"state_43":true,"state_45":true},{"type":"person","title":"Diana Lambert","avail":true,"age":95,"date":901670400000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_2":true,"state_15":true,"state_29":true,"state_35":true,"state_36":true,"state_39":true,"state_48":true},{"type":"person","title":"Dylan Ince","state":"s","avail":true,"age":40,"date":462067200000.0,"state_7":true,"state_11":true,"state_13":true,"state_18":true,"state_19":true,"state_21":true,"state_29":true,"state_38":true,"state_40":true,"state_43":true,"state_44":true,"state_46":true,"state_47":true,"state_50":true},{"type":"person","title":"Trevor Johnston","avail":true,"age":25,"date":1188950400000.0,"state_1":true,"state_4":true,"state_5":true,"state_12":true,"state_14":true,"state_20":true,"state_23":true,"state_29":true,"state_32":true,"state_33":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Katherine T. Hamilton","avail":true,"age":32,"date":123120000000.0,"state_1":true,"state_4":true,"state_13":true,"state_19":true,"state_35":true,"state_40":true,"state_43":true,"state_45":true,"state_49":true}]},{"type":"role","title":"Terrify solutions","children":[{"type":"person","title":"Tim R. Slater","avail":true,"age":34,"date":256089600000.0,"state_9":true,"state_13":true,"state_15":true,"state_29":true,"state_32":true,"state_36":true,"state_38":true},{"type":"person","title":"Kevin W. Watson","avail":true,"age":90,"date":443232000000.0,"state_3":true,"state_6":true,"state_10":true,"state_29":true,"state_31":true,"state_33":true,"state_34":true,"state_37":true,"state_46":true},{"type":"person","title":"Jonathan L. Buckland","state":"s","avail":true,"age":91,"date":588124800000.0,"state_2":true,"state_7":true,"state_8":true,"state_11":true,"state_12":true,"state_14":true,"state_15":true,"state_26":true,"state_29":true,"state_30":true,"state_38":true,"state_48":true},{"type":"person","title":"Adam X. Lewis","avail":true,"age":50,"date":1061683200000.0,"state_2":true,"state_5":true,"state_13":true,"state_22":true,"state_23":true,"state_25":true,"state_30":true,"state_31":true,"state_32":true,"state_34":true,"state_35":true,"state_39":true,"state_40":true,"state_41":true,"state_43":true},{"type":"person","title":"Anthony Turner","state":"h","avail":true,"age":94,"date":1700438400000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_3":true,"state_5":true,"state_7":true,"state_9":true,"state_16":true,"state_23":true,"state_25":true,"state_29":true,"state_35":true},{"type":"person","title":"Stephen Fisher","avail":true,"age":23,"state_8":true,"state_11":true,"state_15":true,"state_17":true,"state_18":true,"state_20":true,"state_23":true,"state_28":true,"state_29":true,"state_33":true,"state_41":true},{"type":"person","title":"David M. Hodges","avail":true,"age":63,"state_6":true,"state_7":true,"state_8":true,"state_11":true,"state_14":true,"state_17":true,"state_19":true,"state_21":true,"state_22":true,"state_30":true,"state_31":true,"state_36":true,"state_43":true,"state_44":true,"state_46":true,"state_50":true},{"type":"person","title":"Jayden F. James","state":"h","avail":true,"age":77,"date":482457600000.0,"state_6":true,"state_12":true,"state_15":true,"state_18":true,"state_22":true,"state_26":true,"state_29":true,"state_41":true},{"type":"person","title":"Amy Grant","avail":true,"age":35,"state_2":true,"state_11":true,"state_12":true,"state_13":true,"state_19":true,"state_28":true,"state_30":true,"state_31":true,"state_33":true,"state_34":true,"state_39":true,"state_41":true,"state_44":true,"state_46":true,"state_50":true},{"type":"person","title":"Dan Hill","avail":true,"age":40,"date":466128000000.0,"state_8":true,"state_11":true,"state_12":true,"state_16":true,"state_19":true,"state_22":true,"state_28":true,"state_30":true,"state_34":true,"state_39":true,"state_46":true},{"type":"person","title":"Wendy Tucker","state":"s","avail":true,"age":96,"date":652406400000.0,"state_3":true,"state_8":true,"state_14":true,"state_15":true,"state_16":true,"state_25":true,"state_31":true,"state_33":true,"state_37":true},{"type":"person","title":"Yvonne Cornish","avail":true,"age":42,"date":583372800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_5":true,"state_8":true,"state_13":true,"state_18":true,"state_40":true,"state_44":true,"state_45":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Katherine Z. Ince","state":"h","age":43,"date":109900800000.0,"state_6":true,"state_9":true,"state_10":true,"state_22":true,"state_32":true,"state_45":true,"state_48":true},{"type":"person","title":"Paul Walsh","avail":true,"age":68,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_4":true,"state_8":true,"state_14":true,"state_20":true,"state_23":true,"state_29":true,"state_33":true,"state_37":true,"state_43":true,"state_45":true},{"type":"person","title":"Dorothy C. McGrath","age":66,"date":1108512000000.0,"state_3":true,"state_4":true,"state_13":true,"state_16":true,"state_20":true,"state_21":true,"state_32":true,"state_33":true,"state_34":true,"state_40":true,"state_43":true,"state_48":true},{"type":"person","title":"Frank O. McDonald","avail":true,"age":59,"date":1321488000000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_2":true,"state_4":true,"state_5":true,"state_22":true,"state_24":true,"state_26":true,"state_33":true,"state_49":true},{"type":"person","title":"Jamie Hill","avail":true,"age":41,"state_2":true,"state_4":true,"state_11":true,"state_24":true,"state_27":true,"state_44":true},{"type":"person","title":"Sam Poole","avail":true,"age":88,"state_5":true,"state_7":true,"state_14":true,"state_21":true,"state_22":true,"state_26":true,"state_29":true,"state_33":true},{"type":"person","title":"Piers Tucker","avail":true,"age":69,"date":1693094400000.0,"state_4":true,"state_8":true,"state_10":true,"state_15":true,"state_16":true,"state_24":true,"state_28":true,"state_34":true,"state_35":true,"state_38":true,"state_50":true},{"type":"person","title":"Andy Hamilton","avail":true,"age":80,"date":198288000000.0,"state_1":true,"state_16":true,"state_23":true,"state_37":true,"state_39":true,"state_44":true}]},{"type":"role","title":"Force reflections","children":[{"type":"person","title":"Diane N. Hunter","avail":true,"age":41,"date":817257600000.0,"state_7":true,"state_17":true,"state_19":true,"state_26":true,"state_32":true,"state_38":true,"state_45":true},{"type":"person","title":"Sonia E. Baker","age":32,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_2":true,"state_13":true,"state_15":true,"state_19":true,"state_22":true,"state_32":true,"state_38":true,"state_42":true,"state_43":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Amanda Avery","state":"s","avail":true,"age":93,"state_7":true,"state_10":true,"state_16":true,"state_17":true,"state_19":true,"state_26":true,"state_31":true,"state_35":true,"state_36":true,"state_42":true,"state_43":true,"state_45":true},{"type":"person","title":"Trevor McLean","avail":true,"age":63,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_16":true,"state_19":true,"state_20":true,"state_25":true,"state_26":true,"state_32":true,"state_35":true,"state_37":true,"state_41":true,"state_45":true,"state_48":true},{"type":"person","title":"Isaac Johnston","state":"h","age":94,"date":1718409600000.0,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_1":true,"state_3":true,"state_5":true,"state_10":true,"state_18":true,"state_21":true,"state_29":true,"state_34":true,"state_42":true,"state_44":true},{"type":"person","title":"Carolyn Rampling","avail":true,"age":55,"date":124070400000.0,"state_3":true,"state_10":true,"state_13":true,"state_14":true,"state_15":true,"state_17":true,"state_23":true,"state_27":true,"state_28":true,"state_30":true,"state_32":true,"state_46":true,"state_50":true},{"type":"person","title":"Tracey J. Avery","avail":true,"age":59,"state_6":true,"state_11":true,"state_14":true,"state_20":true,"state_30":true,"state_47":true,"state_50":true},{"type":"person","title":"Lillian M. Nolan","state":"h","age":27,"state_6":true,"state_17":true,"state_21":true,"state_24":true,"state_28":true,"state_32":true,"state_35":true,"state_40":true,"state_41":true,"state_46":true,"state_47":true},{"type":"person","title":"Chris Wilkins","avail":true,"age":92,"state_6":true,"state_9":true,"state_13":true,"state_34":true,"state_37":true},{"type":"person","title":"Samantha D. Ellison","avail":true,"age":65,"date":618796800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_9":true,"state_18":true,"state_25":true,"state_27":true,"state_34":true,"state_38":true,"state_39":true,"state_41":true,"state_42":true,"state_43":true,"state_45":true},{"type":"person","title":"Jane M. Ball","state":"h","avail":true,"age":93,"date":1020643200000.0,"state_4":true,"state_9":true,"state_10":true,"state_15":true,"state_16":true,"state_17":true,"state_27":true,"state_31":true,"state_38":true,"state_46":true},{"type":"person","title":"Jean Hunter","avail":true,"age":33,"date":1522627200000.0,"state_2":true,"state_6":true,"state_7":true,"state_9":true,"state_12":true,"state_14":true,"state_20":true,"state_27":true,"state_32":true,"state_35":true,"state_43":true,"state_44":true,"state_48":true},{"type":"person","title":"Piers T. Mills","avail":true,"age":98,"date":1651363200000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_30":true,"state_36":true,"state_41":true,"state_42":true,"state_47":true,"state_48":true},{"type":"person","title":"Sally Clark","avail":true,"age":22,"date":419990400000.0,"state_2":true,"state_4":true,"state_7":true,"state_15":true,"state_18":true,"state_20":true,"state_27":true,"state_33":true,"state_34":true,"state_35":true,"state_36":true,"state_44":true,"state_49":true},{"type":"person","title":"Gordon H. Sanderson","age":24,"state_11":true,"state_17":true,"state_19":true,"state_20":true,"state_24":true,"state_26":true,"state_37":true,"state_38":true,"state_44":true,"state_46":true,"state_48":true},{"type":"person","title":"Ava X. Abraham","state":"s","avail":true,"age":59,"state_8":true,"state_11":true,"state_12":true,"state_13":true,"state_38":true,"state_43":true,"state_45":true,"state_46":true},{"type":"person","title":"Warren Lewis","avail":true,"age":29,"date":678240000000.0,"state_6":true,"state_10":true,"state_22":true,"state_27":true,"state_30":true,"state_38":true,"state_44":true,"state_45":true,"state_46":true,"state_49":true},{"type":"person","title":"Blake Glover","avail":true,"age":79,"state_2":true,"state_8":true,"state_13":true,"state_14":true,"state_17":true,"state_20":true,"state_22":true,"state_30":true,"state_34":true,"state_39":true,"state_44":true,"state_47":true,"state_50":true},{"type":"person","title":"Carl H. Butler","avail":true,"age":68,"state_1":true,"state_4":true,"state_5":true,"state_17":true,"state_18":true,"state_20":true,"state_32":true,"state_35":true,"state_37":true,"state_43":true,"state_49":true},{"type":"person","title":"Sam D. Hart","avail":true,"age":61,"state_2":true,"state_6":true,"state_8":true,"state_9":true,"state_15":true,"state_19":true,"state_20":true,"state_26":true,"state_29":true,"state_31":true,"state_32":true,"state_35":true,"state_47":true,"state_48":true},{"type":"person","title":"Justin Lyman","avail":true,"age":38,"state_3":true,"state_8":true,"state_12":true,"state_14":true,"state_15":true,"state_20":true,"state_23":true,"state_25":true,"state_26":true,"state_32":true,"state_35":true,"state_38":true,"state_39":true,"state_42":true,"state_44":true,"state_45":true,"state_46":true,"state_50":true}]},{"type":"role","title":"Inflate clerks","children":[{"type":"person","title":"Carol G. Wilson","avail":true,"age":26,"state_4":true,"state_5":true,"state_10":true,"state_14":true,"state_17":true,"state_29":true,"state_35":true,"state_38":true,"state_41":true,"state_42":true,"state_44":true},{"type":"person","title":"Nicola U. Jackson","avail":true,"age":94,"state_19":true,"state_21":true,"state_22":true,"state_29":true,"state_31":true,"state_38":true,"state_40":true,"state_43":true,"state_46":true,"state_48":true,"state_50":true},{"type":"person","title":"Sean Lewis","state":"h","avail":true,"age":33,"date":1242518400000.0,"state_14":true,"state_32":true},{"type":"person","title":"Christopher Underwood","avail":true,"age":72,"date":1089849600000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_2":true,"state_13":true,"state_23":true,"state_25":true,"state_28":true,"state_35":true,"state_38":true,"state_44":true},{"type":"person","title":"Daryl X. Lee","avail":true,"age":72,"state_11":true,"state_19":true,"state_20":true,"state_21":true,"state_22":true,"state_27":true,"state_28":true,"state_29":true,"state_41":true,"state_50":true},{"type":"person","title":"Luke Gray","state":"s","age":79,"date":955670400000.0,"state_2":true,"state_16":true,"state_22":true,"state_24":true,"state_26":true,"state_29":true,"state_38":true,"state_40":true,"state_44":true},{"type":"person","title":"Alan W. Scott","avail":true,"age":79,"date":426556800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_3":true,"state_10":true,"state_13":true,"state_15":true,"state_17":true,"state_28":true,"state_35":true,"state_36":true,"state_41":true,"state_45":true,"state_48":true},{"type":"person","title":"Bernadette Johnston","avail":true,"age":21,"date":1188000000000.0,"state_2":true,"state_8":true,"state_14":true,"state_16":true,"state_22":true,"state_32":true,"state_41":true,"state_43":true}]},{"type":"role","title":"Cool suspects","children":[{"type":"person","title":"Vanessa Ross","state":"h","avail":true,"age":27,"date":963100800000.0,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_1":true,"state_9":true,"state_10":true,"state_17":true,"state_18":true,"state_24":true,"state_25":true,"state_26":true,"state_29":true,"state_35":true,"state_44":true},{"type":"person","title":"Frances O. Brown","avail":true,"age":39,"date":402105600000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_8":true,"state_9":true,"state_17":true,"state_23":true,"state_24":true,"state_29":true,"state_37":true,"state_38":true,"state_47":true,"state_49":true},{"type":"person","title":"Charlie Tucker","avail":true,"age":56,"date":1576627200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_7":true,"state_8":true,"state_10":true,"state_13":true,"state_17":true,"state_18":true,"state_20":true,"state_22":true,"state_32":true},{"type":"person","title":"Heather Walsh","avail":true,"age":40,"date":535420800000.0,"state_6":true,"state_13":true,"state_14":true,"state_16":true,"state_18":true,"state_20":true,"state_23":true,"state_24":true,"state_27":true,"state_32":true,"state_36":true},{"type":"person","title":"Andrea White","age":36,"date":412387200000.0,"state_11":true,"state_12":true,"state_14":true,"state_19":true,"state_25":true,"state_27":true,"state_30":true,"state_32":true,"state_34":true,"state_38":true,"state_39":true,"state_40":true},{"type":"person","title":"Anne C. MacLeod","age":94,"date":795571200000.0,"state_7":true,"state_19":true,"state_20":true,"state_22":true,"state_25":true,"state_27":true,"state_34":true,"state_36":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Liam Vaughan","avail":true,"age":58,"date":694828800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_4":true,"state_15":true,"state_17":true,"state_23":true,"state_32":true,"state_34":true,"state_35":true,"state_38":true,"state_42":true,"state_48":true,"state_49":true},{"type":"person","title":"Dorothy E. Hardacre","avail":true,"age":56,"date":200448000000.0,"state_3":true,"state_14":true,"state_18":true,"state_27":true,"state_34":true},{"type":"person","title":"Ian N. Bell","avail":true,"age":94,"date":740880000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_4":true,"state_9":true,"state_12":true,"state_22":true,"state_26":true,"state_31":true,"state_32":true,"state_35":true,"state_45":true,"state_49":true},{"type":"person","title":"Jamie S. Parr","avail":true,"age":55,"state_2":true,"state_3":true,"state_5":true,"state_7":true,"state_11":true,"state_12":true,"state_17":true,"state_24":true,"state_30":true,"state_33":true,"state_40":true,"state_43":true,"state_44":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Colin Murray","avail":true,"age":71,"state_15":true,"state_16":true,"state_25":true,"state_38":true},{"type":"person","title":"Piers C. Sutherland","avail":true,"age":62,"date":511747200000.0,"state_13":true,"state_14":true,"state_20":true,"state_26":true,"state_28":true,"state_34":true,"state_46":true,"state_50":true},{"type":"person","title":"Jack Ellison","avail":true,"age":31,"date":1211760000000.0,"state_2":true,"state_5":true,"state_15":true,"state_31":true,"state_34":true,"state_40":true,"state_42":true},{"type":"person","title":"Sydney Wright","avail":true,"age":69,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_2":true,"state_6":true,"state_7":true,"state_8":true,"state_10":true,"state_14":true,"state_16":true,"state_23":true,"state_24":true,"state_35":true,"state_36":true,"state_44":true,"state_48":true},{"type":"person","title":"Alex Y. Scott","state":"s","avail":true,"age":68,"state_3":true,"state_21":true,"state_22":true,"state_27":true,"state_36":true,"state_50":true},{"type":"person","title":"Daryl MacDonald","avail":true,"age":46,"date":1449705600000.0,"state_3":true,"state_5":true,"state_8":true,"state_12":true,"state_17":true,"state_25":true,"state_32":true,"state_35":true,"state_36":true,"state_48":true,"state_50":true},{"type":"person","title":"Kyle C. Ferguson","avail":true,"age":45,"date":906336000000.0,"state_7":true,"state_12":true,"state_14":true,"state_21":true,"state_24":true,"state_25":true,"state_26":true,"state_42":true,"state_46":true},{"type":"person","title":"Ryan Simpson","avail":true,"age":26,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_8":true,"state_18":true,"state_23":true,"state_25":true,"state_28":true,"state_38":true,"state_39":true,"state_41":true},{"type":"person","title":"Lillian King","avail":true,"age":21,"state_2":true,"state_5":true,"state_10":true,"state_13":true,"state_17":true,"state_20":true,"state_26":true,"state_29":true,"state_31":true,"state_36":true,"state_45":true}]},{"type":"role","title":"Ignore parts","children":[{"type":"person","title":"Andrew H. Greene","state":"s","avail":true,"age":71,"state_10":true,"state_11":true,"state_12":true,"state_14":true,"state_18":true,"state_24":true,"state_26":true,"state_28":true,"state_30":true,"state_37":true}]},{"type":"role","title":"Bite terms","children":[{"type":"person","title":"Audrey C. Walker","avail":true,"age":57,"date":1701216000000.0,"state_2":true,"state_5":true,"state_6":true,"state_7":true,"state_24":true,"state_31":true},{"type":"person","title":"Jean L. Mitchell","avail":true,"age":75,"date":1311638400000.0,"state_4":true,"state_7":true,"state_10":true,"state_15":true,"state_28":true,"state_40":true,"state_49":true},{"type":"person","title":"Daryl Cameron","avail":true,"age":50,"date":316742400000.0,"state_8":true,"state_9":true,"state_14":true,"state_18":true,"state_19":true,"state_21":true,"state_27":true,"state_33":true,"state_38":true,"state_45":true,"state_46":true,"state_48":true},{"type":"person","title":"Maria Davidson","avail":true,"age":66,"date":582076800000.0,"state_6":true,"state_7":true,"state_8":true,"state_11":true,"state_36":true,"state_44":true,"state_49":true},{"type":"person","title":"Gabrielle M. Vance","avail":true,"age":92,"state_5":true,"state_6":true,"state_7":true,"state_9":true,"state_11":true,"state_20":true,"state_36":true,"state_37":true,"state_41":true,"state_45":true,"state_48":true},{"type":"person","title":"Felicity W. Lee","avail":true,"age":96,"date":1168732800000.0,"state_3":true,"state_23":true,"state_25":true,"state_32":true,"state_34":true,"state_35":true,"state_45":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Adam Henderson","age":73,"state_3":true,"state_9":true,"state_21":true,"state_26":true,"state_35":true,"state_39":true,"state_40":true,"state_42":true,"state_45":true,"state_47":true},{"type":"person","title":"Jacob P. Russell","age":30,"date":1610668800000.0,"state_8":true,"state_18":true,"state_20":true,"state_33":true,"state_34":true,"state_37":true,"state_39":true,"state_48":true,"state_49":true},{"type":"person","title":"Dorothy I. Berry","avail":true,"age":64,"date":1570320000000.0,"state_1":true,"state_2":true,"state_6":true,"state_10":true,"state_12":true,"state_18":true,"state_23":true,"state_25":true,"state_42":true,"state_44":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Nathan Cornish","avail":true,"age":70,"date":210038400000.0,"state_5":true,"state_12":true,"state_13":true,"state_15":true,"state_19":true,"state_21":true,"state_29":true,"state_30":true,"state_33":true,"state_35":true,"state_37":true,"state_45":true,"state_48":true}]},{"type":"role","title":"Omit elevators","children":[{"type":"person","title":"Taylor H. Vance","avail":true,"age":86,"state_2":true,"state_14":true,"state_17":true,"state_19":true,"state_26":true,"state_27":true,"state_29":true,"state_32":true,"state_36":true}]}]},{"type":"department","title":"Dept. for Importances and Mammoths","children":[{"type":"role","title":"Kid minutes","children":[{"type":"person","title":"Isaac Sharp","avail":true,"age":58,"date":1254960000000.0,"state_13":true,"state_33":true,"state_35":true,"state_39":true,"state_45":true,"state_46":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Rose X. Allan","state":"h","avail":true,"age":73,"date":1477008000000.0,"state_1":true,"state_8":true,"state_14":true,"state_15":true,"state_16":true,"state_18":true,"state_22":true,"state_23":true,"state_24":true,"state_25":true,"state_36":true,"state_37":true,"state_43":true},{"type":"person","title":"Max Clark","avail":true,"age":89,"date":1371081600000.0,"state_1":true,"state_11":true,"state_19":true,"state_25":true,"state_26":true,"state_27":true,"state_30":true,"state_36":true,"state_38":true,"state_49":true},{"type":"person","title":"Casey Z. Poole","avail":true,"age":45,"state_9":true,"state_10":true,"state_11":true,"state_12":true,"state_14":true,"state_15":true,"state_24":true,"state_29":true,"state_32":true,"state_40":true,"state_42":true,"state_44":true,"state_47":true},{"type":"person","title":"Stewart J. Short","avail":true,"age":54,"date":881712000000.0,"state_1":true,"state_3":true,"state_10":true,"state_15":true,"state_20":true,"state_24":true,"state_26":true,"state_29":true,"state_35":true,"state_37":true,"state_38":true,"state_39":true,"state_50":true},{"type":"person","title":"Amy Ellison","avail":true,"age":23,"state_2":true,"state_4":true,"state_5":true,"state_17":true,"state_18":true,"state_23":true,"state_40":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Liam N. Lambert","avail":true,"age":83,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_9":true,"state_15":true,"state_18":true,"state_22":true,"state_27":true,"state_30":true,"state_31":true,"state_36":true,"state_37":true,"state_45":true},{"type":"person","title":"Dylan Bower","avail":true,"age":89,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_9":true,"state_22":true,"state_27":true,"state_29":true,"state_32":true,"state_34":true,"state_36":true,"state_44":true,"state_50":true},{"type":"person","title":"Luke Y. Johnston","avail":true,"age":38,"date":1190160000000.0,"state_20":true,"state_21":true,"state_28":true,"state_40":true,"state_41":true,"state_43":true},{"type":"person","title":"Anthony H. MacDonald","state":"h","avail":true,"age":45,"state_2":true,"state_6":true,"state_10":true,"state_23":true,"state_25":true,"state_26":true,"state_40":true,"state_49":true},{"type":"person","title":"Jacob Poole","state":"h","avail":true,"age":66,"state_7":true,"state_17":true,"state_18":true,"state_20":true,"state_33":true,"state_37":true,"state_41":true,"state_44":true,"state_49":true,"state_50":true},{"type":"person","title":"Olivia X. Dickens","state":"s","avail":true,"age":86,"state_4":true,"state_6":true,"state_8":true,"state_26":true,"state_32":true,"state_35":true,"state_36":true,"state_37":true,"state_48":true},{"type":"person","title":"Eddie Dowd","avail":true,"age":73,"state_5":true,"state_7":true,"state_10":true,"state_18":true,"state_21":true,"state_24":true,"state_32":true,"state_35":true,"state_36":true,"state_43":true,"state_48":true},{"type":"person","title":"Sonia D. Peake","avail":true,"age":23,"state_2":true,"state_4":true,"state_7":true,"state_15":true,"state_23":true,"state_34":true,"state_39":true,"state_41":true,"state_42":true,"state_47":true,"state_48":true},{"type":"person","title":"Emma Reid","avail":true,"age":22,"date":1033603200000.0,"state_8":true,"state_13":true,"state_14":true,"state_27":true,"state_31":true,"state_42":true,"state_43":true,"state_46":true},{"type":"person","title":"Taylor G. Peake","age":82,"date":484876800000.0,"state_4":true,"state_9":true,"state_15":true,"state_16":true,"state_19":true,"state_24":true,"state_26":true,"state_42":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Carol Nolan","avail":true,"age":30,"date":277084800000.0,"state_1":true,"state_3":true,"state_5":true,"state_10":true,"state_11":true,"state_12":true,"state_13":true,"state_36":true,"state_43":true,"state_44":true},{"type":"person","title":"Adam Z. Hill","state":"h","avail":true,"age":84,"state_6":true,"state_8":true,"state_13":true,"state_18":true,"state_30":true,"state_32":true,"state_40":true},{"type":"person","title":"Pat R. Hamilton","avail":true,"age":53,"state_3":true,"state_5":true,"state_14":true,"state_26":true,"state_29":true,"state_38":true,"state_41":true,"state_50":true},{"type":"person","title":"Julia Q. Parsons","state":"h","age":67,"state_2":true,"state_3":true,"state_5":true,"state_12":true,"state_16":true,"state_21":true,"state_22":true,"state_24":true,"state_25":true,"state_30":true,"state_41":true,"state_45":true,"state_47":true,"state_49":true}]},{"type":"role","title":"Arise clerks","children":[{"type":"person","title":"Alison K. Fisher","avail":true,"age":59,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_3":true,"state_8":true,"state_10":true,"state_15":true,"state_19":true,"state_20":true,"state_21":true,"state_24":true,"state_25":true,"state_29":true,"state_33":true,"state_45":true,"state_48":true,"state_49":true},{"type":"person","title":"Austin Rampling","avail":true,"age":78,"date":136771200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_4":true,"state_9":true,"state_10":true,"state_13":true,"state_14":true,"state_15":true,"state_21":true,"state_23":true,"state_31":true,"state_35":true,"state_42":true,"state_48":true},{"type":"person","title":"Frances Quinn","avail":true,"age":86,"date":136684800000.0,"state_12":true,"state_14":true,"state_16":true,"state_17":true,"state_18":true,"state_24":true,"state_30":true,"state_36":true,"state_38":true,"state_45":true,"state_46":true,"state_50":true},{"type":"person","title":"Joseph Hunter","avail":true,"age":37,"date":1675987200000.0,"state_13":true,"state_20":true,"state_34":true,"state_49":true},{"type":"person","title":"Victor Thomson","state":"s","avail":true,"age":82,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_2":true,"state_10":true,"state_18":true,"state_22":true,"state_35":true,"state_42":true,"state_48":true},{"type":"person","title":"Molly R. MacLeod","avail":true,"age":89,"date":686793600000.0,"state_1":true,"state_6":true,"state_12":true,"state_13":true,"state_19":true,"state_31":true},{"type":"person","title":"William Bailey","avail":true,"age":61,"date":425433600000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_1":true,"state_6":true,"state_17":true,"state_26":true,"state_27":true,"state_28":true,"state_32":true,"state_34":true,"state_38":true,"state_41":true,"state_42":true,"state_47":true},{"type":"person","title":"Jack Forsyth","state":"h","avail":true,"age":23,"date":10454400000.0,"state_3":true,"state_4":true,"state_11":true,"state_18":true,"state_19":true,"state_26":true,"state_29":true,"state_33":true,"state_45":true,"state_46":true,"state_48":true},{"type":"person","title":"Sally Y. Avery","avail":true,"age":98,"date":625881600000.0,"state_11":true,"state_13":true,"state_16":true,"state_17":true,"state_19":true,"state_29":true,"state_33":true,"state_34":true,"state_37":true,"state_40":true,"state_45":true},{"type":"person","title":"Jamie Hardacre","avail":true,"age":82,"date":1517443200000.0,"state_2":true,"state_11":true,"state_12":true,"state_13":true,"state_19":true,"state_27":true,"state_28":true,"state_32":true,"state_33":true,"state_46":true},{"type":"person","title":"Ian Walker","state":"h","avail":true,"age":60,"date":1166313600000.0,"state_2":true,"state_6":true,"state_7":true,"state_12":true,"state_15":true,"state_20":true,"state_24":true,"state_25":true,"state_38":true,"state_39":true,"state_40":true},{"type":"person","title":"Caroline Abraham","avail":true,"age":94,"date":607392000000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_7":true,"state_12":true,"state_13":true,"state_14":true,"state_22":true,"state_35":true,"state_39":true,"state_44":true,"state_45":true,"state_46":true},{"type":"person","title":"Leonard S. Morrison","avail":true,"age":33,"date":550022400000.0,"state_6":true,"state_17":true,"state_18":true,"state_19":true,"state_45":true,"state_47":true},{"type":"person","title":"Kylie McLean","avail":true,"age":71,"date":575769600000.0,"state_7":true,"state_14":true,"state_15":true,"state_21":true,"state_29":true,"state_31":true,"state_36":true,"state_38":true,"state_44":true},{"type":"person","title":"Zoe Tucker","age":83,"date":459302400000.0,"state_4":true,"state_10":true,"state_11":true,"state_15":true,"state_16":true,"state_17":true,"state_22":true,"state_38":true,"state_39":true,"state_43":true,"state_47":true},{"type":"person","title":"Liam A. Bower","avail":true,"age":75,"state_3":true,"state_15":true,"state_20":true,"state_22":true,"state_24":true,"state_26":true,"state_31":true,"state_34":true,"state_38":true,"state_39":true,"state_40":true,"state_44":true},{"type":"person","title":"Kylie Blake","avail":true,"age":29,"state_4":true,"state_7":true,"state_9":true,"state_12":true,"state_14":true,"state_16":true,"state_24":true,"state_25":true,"state_26":true,"state_29":true,"state_31":true,"state_48":true},{"type":"person","title":"Fiona Tucker","avail":true,"age":72,"date":830908800000.0,"state_4":true,"state_6":true,"state_7":true,"state_15":true,"state_28":true,"state_36":true,"state_37":true,"state_38":true,"state_43":true,"state_47":true},{"type":"person","title":"Phoenix L. Wilkins","avail":true,"age":62,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_11":true,"state_19":true,"state_28":true,"state_34":true,"state_35":true,"state_37":true,"state_45":true,"state_46":true,"state_49":true},{"type":"person","title":"Heather Y. Bond","age":85,"date":1620259200000.0,"state_7":true,"state_11":true,"state_15":true,"state_19":true,"state_22":true,"state_27":true,"state_35":true,"state_36":true,"state_46":true},{"type":"person","title":"Frances Welch","avail":true,"age":55,"date":1250726400000.0,"state_2":true,"state_15":true,"state_16":true,"state_19":true,"state_28":true,"state_31":true,"state_38":true,"state_44":true}]},{"type":"role","title":"Course noises"},{"type":"role","title":"Fax boxes","children":[{"type":"person","title":"Joe White","state":"h","avail":true,"age":55,"state_4":true,"state_11":true,"state_22":true,"state_30":true,"state_42":true,"state_49":true,"state_50":true},{"type":"person","title":"Anthony Short","avail":true,"age":95,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_5":true,"state_6":true,"state_7":true,"state_9":true,"state_12":true,"state_13":true,"state_14":true,"state_17":true,"state_19":true,"state_23":true,"state_30":true,"state_42":true,"state_45":true,"state_49":true},{"type":"person","title":"Theresa Paterson","avail":true,"age":61,"date":1032998400000.0,"state_1":true,"state_3":true,"state_11":true,"state_13":true,"state_24":true,"state_37":true},{"type":"person","title":"Sam M. Hunter","state":"h","age":29,"date":686188800000.0,"state_7":true,"state_11":true,"state_21":true,"state_27":true,"state_29":true,"state_37":true,"state_38":true,"state_49":true},{"type":"person","title":"Sue Underwood","state":"s","avail":true,"age":55,"date":362880000000.0,"state_11":true,"state_12":true,"state_19":true,"state_50":true},{"type":"person","title":"Joseph E. Morgan","age":40,"date":1400976000000.0,"state_4":true,"state_11":true,"state_19":true,"state_21":true,"state_23":true,"state_39":true,"state_42":true,"state_43":true,"state_49":true,"state_50":true},{"type":"person","title":"Toby Dickens","state":"s","avail":true,"age":88,"state_5":true,"state_11":true,"state_17":true,"state_19":true,"state_20":true,"state_28":true,"state_29":true,"state_34":true,"state_46":true,"state_47":true,"state_48":true}]},{"type":"role","title":"Leer guineas","children":[{"type":"person","title":"Felicity J. Blake","avail":true,"age":90,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_9":true,"state_16":true,"state_17":true,"state_19":true,"state_23":true,"state_24":true,"state_28":true,"state_29":true,"state_31":true,"state_34":true,"state_44":true,"state_47":true}]},{"type":"role","title":"Try paths","children":[{"type":"person","title":"Caroline M. Davies","avail":true,"age":81,"date":873244800000.0,"state_12":true,"state_13":true,"state_22":true,"state_23":true,"state_27":true,"state_41":true,"state_43":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Pat Clarkson","avail":true,"age":37,"date":70761600000.0,"state_5":true,"state_9":true,"state_10":true,"state_12":true,"state_15":true,"state_17":true,"state_19":true,"state_22":true,"state_23":true,"state_26":true,"state_28":true,"state_29":true,"state_30":true,"state_35":true,"state_36":true,"state_39":true,"state_43":true},{"type":"person","title":"Hannah W. Butler","state":"h","avail":true,"age":92,"date":1328400000000.0,"state_1":true,"state_3":true,"state_5":true,"state_14":true,"state_16":true,"state_19":true,"state_20":true,"state_26":true,"state_28":true,"state_45":true},{"type":"person","title":"Austin MacLeod","avail":true,"age":40,"date":118886400000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_7":true,"state_8":true,"state_11":true,"state_12":true,"state_16":true,"state_23":true,"state_26":true,"state_27":true,"state_28":true,"state_31":true,"state_34":true,"state_40":true,"state_48":true},{"type":"person","title":"Sydney McLean","state":"s","avail":true,"age":55,"state_2":true,"state_5":true,"state_13":true,"state_23":true,"state_33":true,"state_41":true},{"type":"person","title":"Kyle X. White","avail":true,"age":47,"date":1506816000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_2":true,"state_3":true,"state_4":true,"state_5":true,"state_8":true,"state_13":true,"state_17":true,"state_19":true,"state_22":true,"state_30":true,"state_40":true,"state_47":true},{"type":"person","title":"Melanie N. Metcalfe","avail":true,"age":44,"date":167875200000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_3":true,"state_8":true,"state_11":true,"state_12":true,"state_20":true,"state_24":true,"state_25":true,"state_29":true,"state_36":true,"state_42":true},{"type":"person","title":"Deirdre Baker","avail":true,"age":90,"date":1160697600000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_4":true,"state_26":true,"state_29":true,"state_48":true}]},{"type":"role","title":"Disobey rivers","children":[{"type":"person","title":"Kelly E. Campbell","avail":true,"age":35,"date":904694400000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_8":true,"state_9":true,"state_16":true,"state_22":true,"state_24":true,"state_28":true,"state_38":true,"state_40":true,"state_49":true,"state_50":true},{"type":"person","title":"Pippa Ogden","state":"h","avail":true,"age":32,"date":1708041600000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_5":true,"state_7":true,"state_12":true,"state_15":true,"state_18":true,"state_19":true,"state_23":true,"state_24":true,"state_29":true,"state_30":true,"state_32":true,"state_33":true,"state_35":true,"state_36":true,"state_38":true,"state_50":true},{"type":"person","title":"Deirdre T. Robertson","avail":true,"age":37,"date":1399248000000.0,"state_3":true,"state_6":true,"state_13":true,"state_20":true,"state_24":true,"state_42":true,"state_48":true},{"type":"person","title":"Colin O. Davies","age":59,"state_1":true,"state_4":true,"state_12":true,"state_13":true,"state_18":true,"state_25":true,"state_28":true,"state_30":true,"state_35":true,"state_46":true,"state_49":true},{"type":"person","title":"Boris V. Piper","state":"h","avail":true,"age":55,"date":729734400000.0,"state_8":true,"state_15":true,"state_24":true,"state_31":true,"state_32":true,"state_41":true,"state_43":true},{"type":"person","title":"Justin U. Walsh","avail":true,"age":68,"date":217728000000.0,"state_13":true,"state_20":true,"state_25":true,"state_30":true,"state_35":true,"state_38":true,"state_39":true,"state_42":true},{"type":"person","title":"Jean Parr","avail":true,"age":30,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_2":true,"state_15":true,"state_17":true,"state_18":true,"state_33":true,"state_43":true,"state_46":true},{"type":"person","title":"Grace E. McGrath","state":"h","avail":true,"age":67,"date":1170633600000.0,"state_4":true,"state_6":true,"state_11":true,"state_12":true,"state_15":true,"state_27":true,"state_30":true,"state_40":true,"state_44":true},{"type":"person","title":"Colin X. Tucker","avail":true,"age":95,"date":439862400000.0,"state_1":true,"state_2":true,"state_6":true,"state_9":true,"state_10":true,"state_12":true,"state_18":true,"state_19":true,"state_20":true,"state_24":true,"state_33":true,"state_39":true},{"type":"person","title":"Alexander Hudson","avail":true,"age":25,"state_2":true,"state_5":true,"state_7":true,"state_10":true,"state_16":true,"state_18":true,"state_19":true,"state_27":true,"state_35":true,"state_39":true,"state_42":true},{"type":"person","title":"Jean Dickens","state":"s","age":64,"date":619574400000.0,"state_4":true,"state_6":true,"state_7":true,"state_8":true,"state_14":true,"state_15":true,"state_16":true,"state_19":true,"state_22":true,"state_24":true,"state_30":true,"state_31":true,"state_36":true,"state_40":true},{"type":"person","title":"Megan Q. Rampling","avail":true,"age":91,"date":173491200000.0,"state_16":true,"state_17":true,"state_20":true,"state_22":true,"state_23":true,"state_31":true,"state_36":true,"state_39":true,"state_40":true,"state_43":true},{"type":"person","title":"Fiona K. Hemmings","avail":true,"age":94,"state_1":true,"state_8":true,"state_10":true,"state_12":true,"state_28":true,"state_29":true,"state_31":true,"state_40":true,"state_42":true},{"type":"person","title":"Madeleine Cameron","avail":true,"age":97,"state_1":true,"state_2":true,"state_10":true,"state_19":true,"state_30":true,"state_42":true,"state_46":true}]},{"type":"role","title":"Solve leaderships","children":[{"type":"person","title":"Sarah Greene","state":"s","age":63,"date":1029196800000.0,"state_1":true,"state_2":true,"state_6":true,"state_10":true,"state_12":true,"state_15":true,"state_16":true,"state_20":true,"state_38":true,"state_40":true,"state_42":true,"state_46":true,"state_50":true},{"type":"person","title":"Sydney V. Marshall","avail":true,"age":95,"state_5":true,"state_6":true,"state_8":true,"state_9":true,"state_11":true,"state_12":true,"state_14":true,"state_17":true,"state_36":true,"state_42":true,"state_45":true,"state_48":true},{"type":"person","title":"Elizabeth Payne","avail":true,"age":21,"date":954806400000.0,"state_14":true,"state_19":true,"state_24":true,"state_26":true,"state_30":true,"state_40":true,"state_41":true},{"type":"person","title":"Adam E. Ogden","state":"h","avail":true,"age":79,"date":897436800000.0,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_4":true,"state_5":true,"state_15":true,"state_16":true,"state_17":true,"state_23":true,"state_28":true,"state_31":true,"state_33":true,"state_35":true,"state_37":true,"state_46":true,"state_49":true},{"type":"person","title":"Nathan N. Langdon","avail":true,"age":28,"date":491875200000.0,"state_2":true,"state_8":true,"state_12":true,"state_13":true,"state_18":true,"state_25":true,"state_30":true,"state_38":true,"state_43":true,"state_44":true}]},{"type":"role","title":"Swear times","children":[{"type":"person","title":"Phoenix Lee","avail":true,"age":83,"state_2":true,"state_3":true,"state_5":true,"state_8":true,"state_10":true,"state_18":true,"state_23":true,"state_50":true},{"type":"person","title":"Luke X. Edmunds","avail":true,"age":92,"date":1412726400000.0,"state_4":true,"state_6":true,"state_8":true,"state_19":true,"state_21":true,"state_23":true,"state_26":true,"state_27":true,"state_31":true,"state_44":true,"state_45":true,"state_47":true,"state_48":true,"state_49":true},{"type":"person","title":"Charlie Ross","avail":true,"age":61,"date":1306800000000.0,"state_1":true,"state_6":true,"state_10":true,"state_16":true,"state_19":true,"state_30":true,"state_31":true,"state_34":true,"state_47":true,"state_49":true},{"type":"person","title":"Frances Black","state":"h","avail":true,"age":38,"state_7":true,"state_22":true,"state_23":true,"state_25":true,"state_32":true,"state_34":true,"state_35":true}]},{"type":"role","title":"Verify replacements","children":[{"type":"person","title":"Sam K. Knox","state":"s","avail":true,"age":95,"date":1109030400000.0,"state_15":true,"state_24":true,"state_30":true,"state_41":true,"state_42":true,"state_44":true,"state_46":true,"state_49":true},{"type":"person","title":"Michelle Avery","avail":true,"age":36,"state_3":true,"state_20":true,"state_26":true,"state_27":true,"state_36":true,"state_46":true,"state_50":true},{"type":"person","title":"Lillian Hardacre","state":"h","avail":true,"age":66,"date":1281571200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_2":true,"state_3":true,"state_4":true,"state_7":true,"state_9":true,"state_13":true,"state_19":true,"state_30":true,"state_34":true,"state_48":true},{"type":"person","title":"Boris Piper","avail":true,"age":79,"date":152668800000.0,"state_2":true,"state_9":true,"state_10":true,"state_11":true,"state_18":true,"state_20":true,"state_42":true,"state_49":true},{"type":"person","title":"Neil W. Churchill","avail":true,"age":24,"remarks":"At vero eos et accusam et justo duo dolores et ea rebum.","state_9":true,"state_12":true,"state_13":true,"state_14":true,"state_16":true,"state_18":true,"state_25":true,"state_33":true,"state_34":true,"state_39":true,"state_40":true,"state_44":true},{"type":"person","title":"Anne B. Hart","avail":true,"age":88,"date":1161388800000.0,"state_2":true,"state_4":true,"state_9":true,"state_20":true,"state_21":true,"state_23":true,"state_24":true,"state_44":true,"state_47":true},{"type":"person","title":"Cameron S. Slater","avail":true,"age":90,"state_7":true,"state_13":true,"state_16":true,"state_17":true,"state_18":true,"state_19":true,"state_20":true,"state_25":true,"state_31":true,"state_33":true,"state_35":true,"state_39":true,"state_44":true,"state_50":true},{"type":"person","title":"Jan Powell","state":"h","age":84,"date":155433600000.0,"state_8":true,"state_9":true,"state_25":true,"state_28":true,"state_39":true,"state_43":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Eddie L. Dowd","age":89,"state_2":true,"state_5":true,"state_9":true,"state_24":true,"state_27":true,"state_32":true,"state_34":true,"state_37":true,"state_39":true,"state_42":true,"state_43":true,"state_44":true,"state_45":true,"state_47":true,"state_49":true},{"type":"person","title":"Toby B. Paterson","avail":true,"age":81,"state_5":true,"state_12":true,"state_18":true,"state_20":true,"state_26":true,"state_35":true,"state_37":true,"state_43":true},{"type":"person","title":"Sebastian Peters","state":"s","avail":true,"age":77,"state_4":true,"state_9":true,"state_15":true,"state_17":true,"state_35":true,"state_36":true,"state_40":true,"state_45":true,"state_49":true},{"type":"person","title":"Joe Payne","age":98,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_2":true,"state_6":true,"state_9":true,"state_18":true,"state_27":true,"state_28":true,"state_33":true,"state_35":true,"state_38":true},{"type":"person","title":"Madeleine Duncan","state":"s","avail":true,"age":68,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_1":true,"state_14":true,"state_15":true,"state_16":true,"state_19":true,"state_24":true,"state_26":true,"state_31":true,"state_43":true},{"type":"person","title":"Colin N. Ogden","avail":true,"age":42,"date":1078272000000.0,"state_6":true,"state_9":true,"state_10":true,"state_15":true,"state_17":true,"state_25":true,"state_31":true,"state_32":true,"state_41":true,"state_43":true,"state_49":true,"state_50":true},{"type":"person","title":"Lucas H. Sharp","avail":true,"age":28,"date":1550448000000.0,"state_1":true,"state_2":true,"state_4":true,"state_9":true,"state_10":true,"state_13":true,"state_16":true,"state_19":true,"state_20":true,"state_24":true,"state_37":true,"state_39":true,"state_40":true,"state_43":true,"state_46":true},{"type":"person","title":"Angela Sharp","state":"h","avail":true,"age":31,"date":966124800000.0,"state_1":true,"state_2":true,"state_6":true,"state_33":true,"state_35":true,"state_37":true,"state_41":true,"state_44":true,"state_46":true},{"type":"person","title":"Amanda F. Carr","avail":true,"age":54,"state_2":true,"state_16":true,"state_17":true,"state_27":true,"state_31":true,"state_33":true,"state_37":true,"state_38":true}]},{"type":"role","title":"Scar reflections","children":[{"type":"person","title":"Frank A. Oliver","avail":true,"age":60,"state_2":true,"state_5":true,"state_7":true,"state_11":true,"state_18":true,"state_29":true,"state_35":true,"state_40":true},{"type":"person","title":"Casey Blake","avail":true,"age":81,"date":1482537600000.0,"state_11":true,"state_15":true,"state_18":true,"state_22":true,"state_25":true,"state_26":true,"state_38":true,"state_44":true},{"type":"person","title":"Jasmine V. Metcalfe","avail":true,"age":53,"state_3":true,"state_6":true,"state_7":true,"state_8":true,"state_10":true,"state_20":true,"state_23":true,"state_27":true,"state_31":true,"state_38":true,"state_42":true,"state_45":true,"state_49":true},{"type":"person","title":"Sam X. Gray","state":"h","avail":true,"age":65,"date":332812800000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_2":true,"state_17":true,"state_21":true,"state_22":true,"state_23":true,"state_30":true,"state_33":true,"state_36":true,"state_42":true},{"type":"person","title":"Lauren O. Ellison","avail":true,"age":70,"date":41990400000.0,"state_2":true,"state_6":true,"state_11":true,"state_15":true,"state_17":true,"state_18":true,"state_19":true,"state_24":true,"state_26":true,"state_27":true,"state_35":true,"state_36":true,"state_37":true,"state_47":true},{"type":"person","title":"Robert Hudson","state":"h","age":29,"state_11":true,"state_17":true,"state_26":true,"state_32":true,"state_40":true,"state_49":true},{"type":"person","title":"Eric Kelly","avail":true,"age":25,"date":1036368000000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_1":true,"state_2":true,"state_4":true,"state_20":true,"state_21":true,"state_31":true,"state_32":true,"state_39":true,"state_40":true,"state_46":true,"state_47":true},{"type":"person","title":"Melanie Sharp","avail":true,"age":21,"date":382233600000.0,"state_2":true,"state_9":true,"state_15":true,"state_16":true,"state_23":true,"state_28":true,"state_29":true,"state_33":true,"state_39":true,"state_40":true,"state_46":true,"state_50":true},{"type":"person","title":"Kylie W. Morgan","avail":true,"age":93,"date":567129600000.0,"state_3":true,"state_6":true,"state_12":true,"state_24":true,"state_29":true,"state_30":true,"state_38":true},{"type":"person","title":"Taylor R. Nash","state":"s","age":63,"state_8":true,"state_10":true,"state_16":true,"state_26":true,"state_28":true,"state_29":true,"state_38":true,"state_39":true,"state_40":true},{"type":"person","title":"Christopher P. Morgan","avail":true,"age":78,"date":1667260800000.0,"state_3":true,"state_11":true,"state_12":true,"state_15":true,"state_19":true,"state_20":true,"state_25":true,"state_27":true,"state_29":true,"state_34":true,"state_35":true,"state_47":true}]},{"type":"role","title":"Search classes","children":[{"type":"person","title":"Ruth Robertson","avail":true,"age":84,"state_2":true,"state_4":true,"state_10":true,"state_11":true,"state_16":true,"state_25":true,"state_26":true,"state_34":true,"state_36":true,"state_45":true,"state_46":true,"state_50":true},{"type":"person","title":"Andrea Clark","avail":true,"age":82,"date":820713600000.0,"state_1":true,"state_9":true,"state_23":true,"state_37":true},{"type":"person","title":"Frances A. Piper","age":75,"state_6":true,"state_9":true,"state_10":true,"state_11":true,"state_14":true,"state_18":true,"state_24":true,"state_30":true,"state_32":true,"state_36":true,"state_39":true,"state_46":true,"state_50":true},{"type":"person","title":"Isaac Allan","state":"s","age":42,"date":1066262400000.0,"state_1":true,"state_5":true,"state_15":true,"state_19":true,"state_25":true,"state_26":true,"state_28":true,"state_33":true,"state_39":true,"state_48":true,"state_50":true},{"type":"person","title":"Brandon Baker","avail":true,"age":66,"state_2":true,"state_4":true,"state_6":true,"state_7":true,"state_9":true,"state_11":true,"state_13":true,"state_15":true,"state_28":true,"state_30":true,"state_34":true,"state_35":true,"state_38":true,"state_42":true},{"type":"person","title":"Sophie C. Paige","avail":true,"age":30,"state_1":true,"state_6":true,"state_14":true,"state_20":true,"state_21":true,"state_27":true,"state_30":true,"state_32":true,"state_33":true,"state_37":true,"state_38":true,"state_39":true,"state_42":true,"state_48":true},{"type":"person","title":"Abigail G. Alsop","avail":true,"age":64,"state_5":true,"state_9":true,"state_19":true,"state_24":true,"state_43":true,"state_45":true},{"type":"person","title":"Andy Wilson","avail":true,"age":73,"date":171158400000.0,"state_3":true,"state_6":true,"state_8":true,"state_14":true,"state_17":true,"state_24":true,"state_29":true,"state_33":true,"state_34":true,"state_40":true,"state_41":true,"state_44":true,"state_46":true,"state_47":true},{"type":"person","title":"Andrea Rampling","avail":true,"age":29,"date":1694908800000.0,"state_1":true,"state_3":true,"state_4":true,"state_5":true,"state_12":true,"state_14":true,"state_27":true,"state_32":true,"state_38":true,"state_43":true,"state_47":true},{"type":"person","title":"Frank Morgan","avail":true,"age":75,"remarks":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat.","state_8":true,"state_9":true,"state_12":true,"state_16":true,"state_17":true,"state_19":true,"state_20":true,"state_24":true,"state_25":true,"state_28":true,"state_29":true,"state_31":true,"state_33":true,"state_44":true,"state_46":true,"state_47":true,"state_49":true,"state_50":true}]}]},{"type":"department","title":"Dept. for Bigs and Abroads","children":[{"type":"role","title":"Uproot creatives","children":[{"type":"person","title":"Dan McGrath","avail":true,"age":78,"date":436320000000.0,"state_8":true,"state_10":true,"state_11":true,"state_19":true,"state_23":true,"state_34":true,"state_37":true,"state_38":true,"state_44":true,"state_49":true,"state_50":true},{"type":"person","title":"Steven Metcalfe","avail":true,"age":92,"date":1452556800000.0,"state_2":true,"state_3":true,"state_13":true,"state_16":true,"state_24":true,"state_29":true,"state_41":true,"state_43":true,"state_45":true},{"type":"person","title":"Anna Hardacre","avail":true,"age":50,"date":38793600000.0,"state_1":true,"state_14":true,"state_16":true,"state_18":true,"state_22":true,"state_31":true,"state_33":true,"state_38":true,"state_47":true,"state_49":true},{"type":"person","title":"Justin K. Rampling","avail":true,"age":46,"date":1543968000000.0,"state_2":true,"state_20":true,"state_22":true,"state_26":true,"state_29":true,"state_33":true,"state_38":true,"state_47":true},{"type":"person","title":"Amanda Wallace","state":"s","avail":true,"age":27,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_4":true,"state_8":true,"state_14":true,"state_24":true,"state_28":true,"state_29":true,"state_38":true,"state_44":true},{"type":"person","title":"Hannah Ellison","state":"h","avail":true,"age":81,"date":1635984000000.0,"state_7":true,"state_25":true,"state_36":true,"state_41":true,"state_44":true,"state_50":true},{"type":"person","title":"Kelly T. Clarkson","state":"h","avail":true,"age":83,"state_4":true,"state_8":true,"state_17":true,"state_20":true,"state_24":true,"state_31":true,"state_36":true,"state_38":true,"state_39":true,"state_40":true,"state_42":true},{"type":"person","title":"Caroline U. Hemmings","avail":true,"age":66,"date":1295222400000.0,"state_6":true,"state_14":true,"state_18":true,"state_23":true,"state_25":true,"state_33":true,"state_34":true,"state_42":true,"state_44":true},{"type":"person","title":"Virginia McLean","state":"s","age":48,"date":1347062400000.0,"state_5":true,"state_10":true,"state_22":true,"state_30":true,"state_38":true,"state_47":true},{"type":"person","title":"Dorothy L. Gill","avail":true,"age":71,"date":15552000000.0,"state_8":true,"state_9":true,"state_15":true,"state_20":true,"state_23":true,"state_24":true,"state_25":true,"state_39":true,"state_44":true},{"type":"person","title":"Victor T. Watson","state":"s","avail":true,"age":30,"state_2":true,"state_3":true,"state_13":true,"state_22":true,"state_23":true,"state_30":true,"state_34":true,"state_35":true,"state_44":true,"state_49":true},{"type":"person","title":"Jayden A. Miller","avail":true,"age":64,"state_2":true,"state_6":true,"state_16":true,"state_17":true,"state_19":true,"state_23":true,"state_26":true,"state_28":true,"state_31":true,"state_36":true,"state_44":true,"state_49":true},{"type":"person","title":"Christopher Churchill","avail":true,"age":76,"state_12":true,"state_23":true,"state_24":true,"state_26":true,"state_29":true,"state_46":true},{"type":"person","title":"Theresa Sanderson","avail":true,"age":95,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_2":true,"state_4":true,"state_5":true,"state_10":true,"state_20":true,"state_27":true,"state_29":true,"state_33":true,"state_34":true,"state_37":true,"state_44":true,"state_45":true,"state_48":true}]},{"type":"role","title":"Secure memories","children":[{"type":"person","title":"Carolyn X. Wright","avail":true,"age":48,"date":498268800000.0,"state_8":true,"state_11":true,"state_12":true,"state_18":true,"state_30":true,"state_33":true,"state_36":true,"state_39":true,"state_41":true},{"type":"person","title":"Sam W. Paterson","state":"s","avail":true,"age":75,"date":1303603200000.0,"state_5":true,"state_7":true,"state_12":true,"state_14":true,"state_15":true,"state_18":true,"state_25":true,"state_27":true,"state_30":true,"state_32":true,"state_38":true,"state_44":true},{"type":"person","title":"Pat Ince","state":"h","avail":true,"age":30,"state_6":true,"state_7":true,"state_8":true,"state_11":true,"state_15":true,"state_25":true,"state_27":true,"state_28":true,"state_31":true,"state_43":true,"state_50":true},{"type":"person","title":"Gordon R. Powell","age":59,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_8":true,"state_9":true,"state_10":true,"state_27":true,"state_31":true,"state_40":true,"state_41":true,"state_44":true,"state_46":true},{"type":"person","title":"Steven Jones","avail":true,"age":96,"state_3":true,"state_5":true,"state_6":true,"state_9":true,"state_16":true,"state_18":true,"state_31":true,"state_36":true,"state_39":true,"state_40":true,"state_47":true},{"type":"person","title":"Eddie R. Coleman","avail":true,"age":66,"date":1630886400000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_14":true,"state_17":true,"state_19":true,"state_26":true,"state_45":true},{"type":"person","title":"Katherine Payne","avail":true,"age":44,"date":419731200000.0,"state_2":true,"state_14":true,"state_16":true,"state_17":true,"state_19":true,"state_23":true,"state_27":true,"state_31":true,"state_36":true,"state_40":true,"state_43":true},{"type":"person","title":"Dan S. Rutherford","avail":true,"age":63,"date":1320883200000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_2":true,"state_6":true,"state_7":true,"state_8":true,"state_19":true,"state_26":true,"state_32":true,"state_37":true,"state_39":true,"state_41":true,"state_45":true,"state_46":true},{"type":"person","title":"Julia Anderson","state":"s","avail":true,"age":50,"state_2":true,"state_5":true,"state_20":true,"state_25":true,"state_26":true,"state_30":true,"state_31":true,"state_36":true,"state_37":true},{"type":"person","title":"Faith S. Alsop","avail":true,"age":33,"date":1284681600000.0,"state_1":true,"state_2":true,"state_3":true,"state_7":true,"state_10":true,"state_19":true,"state_27":true,"state_28":true,"state_35":true,"state_36":true}]},{"type":"role","title":"Confiscate tomorrows","children":[{"type":"person","title":"Grace Chapman","state":"s","avail":true,"age":49,"state_1":true,"state_6":true,"state_8":true,"state_10":true,"state_14":true,"state_22":true,"state_24":true,"state_33":true,"state_40":true,"state_50":true},{"type":"person","title":"Elizabeth B. Watson","state":"h","avail":true,"age":36,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_12":true,"state_16":true,"state_18":true,"state_19":true,"state_21":true,"state_27":true,"state_28":true,"state_36":true,"state_41":true,"state_43":true,"state_44":true},{"type":"person","title":"Tim R. Mackenzie","avail":true,"age":95,"state_2":true,"state_3":true,"state_5":true,"state_8":true,"state_9":true,"state_10":true,"state_11":true,"state_12":true,"state_13":true,"state_25":true,"state_36":true,"state_39":true,"state_48":true},{"type":"person","title":"Julia Underwood","avail":true,"age":68,"date":944006400000.0,"state_14":true,"state_20":true,"state_25":true,"state_28":true,"state_32":true,"state_33":true,"state_35":true,"state_38":true,"state_39":true,"state_40":true,"state_44":true,"state_45":true,"state_47":true,"state_48":true},{"type":"person","title":"Sydney U. Cameron","avail":true,"age":28,"date":1570060800000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_6":true,"state_11":true,"state_12":true,"state_13":true,"state_20":true,"state_28":true,"state_49":true},{"type":"person","title":"Joanne Ogden","state":"h","avail":true,"age":31,"date":732067200000.0,"state_1":true,"state_7":true,"state_13":true,"state_20":true,"state_28":true,"state_30":true,"state_41":true},{"type":"person","title":"Kylie E. Glover","state":"s","age":61,"state_2":true,"state_8":true,"state_18":true,"state_22":true,"state_27":true,"state_28":true,"state_45":true,"state_46":true},{"type":"person","title":"Grace R. Mackenzie","avail":true,"age":75,"state_3":true,"state_7":true,"state_17":true,"state_19":true,"state_24":true,"state_27":true,"state_28":true,"state_29":true,"state_31":true,"state_32":true,"state_39":true,"state_40":true,"state_43":true,"state_44":true,"state_49":true}]},{"type":"role","title":"Bid smells","children":[{"type":"person","title":"Sonia U. Hardacre","age":81,"date":71280000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_9":true,"state_12":true,"state_23":true,"state_24":true,"state_27":true,"state_30":true,"state_31":true,"state_35":true,"state_38":true,"state_40":true,"state_42":true,"state_43":true,"state_48":true},{"type":"person","title":"Joanne Ferguson","state":"h","avail":true,"age":92,"state_11":true,"state_13":true,"state_14":true,"state_20":true,"state_21":true,"state_22":true,"state_30":true},{"type":"person","title":"Jasmine B. Grant","avail":true,"age":75,"date":754444800000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_3":true,"state_6":true,"state_10":true,"state_15":true,"state_17":true,"state_19":true,"state_20":true,"state_30":true,"state_40":true,"state_41":true,"state_45":true,"state_48":true,"state_49":true},{"type":"person","title":"Sebastian F. Edmunds","state":"h","avail":true,"age":79,"date":1535155200000.0,"state_3":true,"state_17":true,"state_32":true,"state_35":true,"state_36":true,"state_40":true,"state_42":true,"state_43":true,"state_47":true},{"type":"person","title":"Benjamin F. Baker","avail":true,"age":59,"date":4752000000.0,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_6":true,"state_17":true,"state_21":true,"state_28":true,"state_37":true,"state_39":true,"state_40":true,"state_43":true,"state_47":true,"state_50":true},{"type":"person","title":"Matt Bower","avail":true,"age":31,"date":1642636800000.0,"state_6":true,"state_13":true,"state_19":true,"state_20":true,"state_21":true,"state_25":true,"state_26":true,"state_27":true,"state_30":true,"state_36":true,"state_39":true,"state_43":true},{"type":"person","title":"Joan Piper","avail":true,"age":93,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_9":true,"state_16":true,"state_18":true,"state_37":true,"state_38":true,"state_42":true,"state_47":true},{"type":"person","title":"Jean R. Greene","avail":true,"age":30,"date":213667200000.0,"remarks":"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.","state_1":true,"state_10":true,"state_11":true,"state_15":true,"state_23":true,"state_25":true,"state_27":true,"state_31":true,"state_45":true,"state_48":true},{"type":"person","title":"Jack Murray","avail":true,"age":63,"state_2":true,"state_3":true,"state_27":true,"state_30":true,"state_37":true,"state_47":true},{"type":"person","title":"Boris C. Langdon","avail":true,"age":83,"date":1502064000000.0,"state_7":true,"state_10":true,"state_18":true,"state_20":true,"state_25":true,"state_29":true,"state_32":true,"state_33":true,"state_49":true},{"type":"person","title":"Eddie Sharp","age":61,"state_5":true,"state_10":true,"state_11":true,"state_22":true,"state_30":true,"state_40":true,"state_43":true,"state_46":true},{"type":"person","title":"Michael Smith","state":"h","avail":true,"age":69,"date":1469491200000.0,"state_2":true,"state_4":true,"state_13":true,"state_21":true,"state_27":true,"state_30":true,"state_39":true,"state_41":true,"state_44":true,"state_47":true,"state_49":true},{"type":"person","title":"Sydney Terry","avail":true,"age":42,"date":808617600000.0,"state_11":true,"state_17":true,"state_22":true,"state_29":true,"state_34":true,"state_36":true,"state_44":true,"state_45":true,"state_46":true,"state_47":true},{"type":"person","title":"Joe D. Russell","state":"h","avail":true,"age":90,"date":1353542400000.0,"state_2":true,"state_7":true,"state_15":true,"state_24":true,"state_28":true,"state_31":true,"state_32":true,"state_34":true,"state_35":true,"state_41":true,"state_42":true,"state_43":true,"state_45":true},{"type":"person","title":"Diane Wright","avail":true,"age":63,"date":131846400000.0,"state_8":true,"state_9":true,"state_11":true,"state_13":true,"state_15":true,"state_16":true,"state_21":true,"state_23":true,"state_29":true,"state_30":true,"state_32":true,"state_44":true,"state_45":true,"state_46":true,"state_48":true},{"type":"person","title":"Michael Thomson","avail":true,"age":50,"date":804902400000.0,"state_16":true,"state_20":true,"state_22":true,"state_27":true,"state_30":true,"state_32":true,"state_34":true,"state_38":true,"state_44":true,"state_46":true,"state_48":true}]},{"type":"role","title":"Punish chips","children":[{"type":"person","title":"Casey Mathis","avail":true,"age":97,"state_7":true,"state_9":true,"state_19":true,"state_23":true,"state_26":true,"state_27":true,"state_32":true,"state_35":true,"state_38":true,"state_43":true},{"type":"person","title":"Alexandra Lee","avail":true,"age":80,"date":1374278400000.0,"state_5":true,"state_9":true,"state_24":true,"state_30":true,"state_33":true,"state_42":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Sam Forsyth","avail":true,"age":78,"state_1":true,"state_6":true,"state_9":true,"state_10":true,"state_16":true,"state_23":true,"state_25":true,"state_27":true,"state_28":true,"state_33":true,"state_39":true,"state_43":true,"state_46":true}]},{"type":"role","title":"Download pianos","children":[{"type":"person","title":"Kylie I. Mackay","age":25,"date":803174400000.0,"state_6":true,"state_8":true,"state_10":true,"state_14":true,"state_24":true,"state_31":true,"state_32":true,"state_36":true,"state_37":true,"state_49":true,"state_50":true},{"type":"person","title":"Nicholas Peake","avail":true,"age":38,"date":98928000000.0,"state_7":true,"state_14":true,"state_18":true,"state_21":true,"state_25":true,"state_34":true,"state_36":true,"state_40":true,"state_43":true,"state_44":true,"state_45":true,"state_50":true},{"type":"person","title":"James Hamilton","avail":true,"age":46,"date":1667520000000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_3":true,"state_5":true,"state_7":true,"state_9":true,"state_23":true,"state_24":true,"state_25":true,"state_26":true,"state_31":true,"state_41":true,"state_44":true,"state_48":true},{"type":"person","title":"Charlie N. McGrath","age":52,"date":911260800000.0,"remarks":"Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.","state_12":true,"state_16":true,"state_21":true,"state_24":true,"state_26":true,"state_37":true,"state_44":true,"state_46":true,"state_49":true,"state_50":true},{"type":"person","title":"Maria P. Paige","avail":true,"age":32,"date":668304000000.0,"state_11":true,"state_16":true,"state_20":true,"state_21":true,"state_23":true,"state_29":true,"state_30":true,"state_36":true},{"type":"person","title":"Charlie Fraser","avail":true,"age":52,"date":1153094400000.0,"state_6":true,"state_10":true,"state_11":true,"state_16":true,"state_18":true,"state_22":true,"state_28":true,"state_33":true},{"type":"person","title":"Jake Z. Terry","avail":true,"age":66,"date":1353888000000.0,"state_2":true,"state_4":true,"state_9":true,"state_10":true,"state_24":true,"state_27":true,"state_36":true,"state_39":true},{"type":"person","title":"Stephen L. Gill","avail":true,"age":78,"state_4":true,"state_5":true,"state_18":true,"state_26":true,"state_29":true,"state_32":true,"state_38":true,"state_40":true,"state_42":true,"state_43":true,"state_46":true,"state_48":true},{"type":"person","title":"Thomas Q. Clarkson","avail":true,"age":84,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_20":true,"state_21":true,"state_25":true,"state_27":true,"state_33":true,"state_35":true,"state_39":true,"state_42":true,"state_49":true},{"type":"person","title":"Samantha N. Churchill","avail":true,"age":72,"date":1244332800000.0,"state_1":true,"state_2":true,"state_35":true,"state_36":true,"state_37":true,"state_43":true,"state_46":true},{"type":"person","title":"Casey Chapman","avail":true,"age":84,"date":1158105600000.0,"remarks":"Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.","state_3":true,"state_5":true,"state_10":true,"state_11":true,"state_14":true,"state_15":true,"state_23":true,"state_25":true,"state_38":true,"state_45":true,"state_49":true}]},{"type":"role","title":"Advise sizes","children":[{"type":"person","title":"Harry Dickens","avail":true,"age":79,"date":755740800000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_1":true,"state_11":true,"state_14":true,"state_18":true,"state_19":true,"state_20":true,"state_35":true,"state_46":true,"state_49":true},{"type":"person","title":"Cameron Terry","state":"s","avail":true,"age":63,"date":1620518400000.0,"state_1":true,"state_5":true,"state_7":true,"state_11":true,"state_15":true,"state_23":true,"state_30":true,"state_35":true,"state_39":true,"state_40":true,"state_41":true,"state_44":true,"state_45":true,"state_48":true},{"type":"person","title":"Taylor Y. Thomson","state":"h","avail":true,"age":24,"remarks":"Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.","state_5":true,"state_6":true,"state_16":true,"state_36":true,"state_38":true,"state_46":true},{"type":"person","title":"Jane Carr","avail":true,"age":96,"date":1365465600000.0,"state_2":true,"state_3":true,"state_4":true,"state_10":true,"state_14":true,"state_15":true,"state_17":true,"state_20":true,"state_22":true,"state_30":true,"state_36":true,"state_42":true,"state_46":true,"state_49":true},{"type":"person","title":"Megan Z. Harris","age":62,"state_7":true,"state_10":true,"state_11":true,"state_16":true,"state_17":true,"state_31":true,"state_34":true,"state_39":true,"state_40":true},{"type":"person","title":"Vanessa C. Blake","state":"s","avail":true,"age":21,"date":753408000000.0,"state_15":true,"state_17":true,"state_18":true,"state_27":true,"state_37":true,"state_42":true,"state_44":true,"state_46":true},{"type":"person","title":"Anne Y. Mitchell","avail":true,"age":42,"date":205632000000.0,"state_1":true,"state_2":true,"state_33":true,"state_37":true,"state_41":true,"state_42":true,"state_44":true,"state_49":true},{"type":"person","title":"Connor Clarkson","state":"h","avail":true,"age":70,"date":1330214400000.0,"remarks":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.","state_2":true,"state_9":true,"state_12":true,"state_15":true,"state_18":true,"state_20":true,"state_21":true,"state_22":true,"state_26":true,"state_28":true,"state_35":true,"state_41":true,"state_43":true,"state_48":true},{"type":"person","title":"Justin Q. Roberts","avail":true,"age":33,"state_15":true,"state_17":true,"state_18":true,"state_28":true,"state_29":true,"state_30":true,"state_40":true,"state_43":true,"state_47":true},{"type":"person","title":"Sue M. Hart","avail":true,"age":60,"state_2":true,"state_5":true,"state_8":true,"state_11":true,"state_17":true,"state_21":true,"state_29":true,"state_33":true,"state_36":true,"state_40":true,"state_42":true,"state_43":true,"state_49":true},{"type":"person","title":"Steven Sanderson","avail":true,"age":93,"date":209174400000.0,"state_11":true,"state_24":true,"state_28":true,"state_34":true,"state_42":true,"state_46":true,"state_49":true}]},{"type":"role","title":"Pray staffs"},{"type":"role","title":"Yell shares","children":[{"type":"person","title":"Molly Baker","avail":true,"age":36,"state_5":true,"state_7":true,"state_11":true,"state_12":true,"state_17":true,"state_29":true,"state_30":true,"state_31":true},{"type":"person","title":"Edward Rees","avail":true,"age":52,"state_12":true,"state_14":true,"state_24":true,"state_30":true,"state_31":true,"state_32":true,"state_35":true,"state_37":true,"state_38":true},{"type":"person","title":"Cameron H. Wilson","avail":true,"age":88,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.","state_2":true,"state_4":true,"state_5":true,"state_7":true,"state_8":true,"state_10":true,"state_20":true,"state_22":true,"state_24":true,"state_27":true,"state_30":true,"state_35":true,"state_38":true,"state_44":true,"state_48":true},{"type":"person","title":"Victor Ellison","avail":true,"age":24,"remarks":"Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua.","state_11":true,"state_21":true,"state_29":true,"state_32":true,"state_36":true,"state_46":true,"state_47":true,"state_50":true},{"type":"person","title":"Justin May","avail":true,"age":91,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_6":true,"state_12":true,"state_30":true,"state_32":true,"state_41":true,"state_48":true},{"type":"person","title":"Daryl Terry","state":"s","avail":true,"age":58,"state_10":true,"state_14":true,"state_17":true,"state_21":true,"state_34":true,"state_36":true,"state_44":true,"state_47":true}]},{"type":"role","title":"Hang nobodies","children":[{"type":"person","title":"Diana Hill","avail":true,"age":87,"date":342316800000.0,"state_6":true,"state_12":true,"state_15":true,"state_25":true,"state_28":true,"state_40":true},{"type":"person","title":"Paul Turner","avail":true,"age":70,"date":99964800000.0,"state_7":true,"state_14":true,"state_19":true,"state_23":true,"state_26":true,"state_27":true,"state_35":true,"state_41":true,"state_44":true,"state_46":true},{"type":"person","title":"Jennifer Fisher","avail":true,"age":32,"state_3":true,"state_4":true,"state_5":true,"state_18":true,"state_22":true,"state_32":true,"state_42":true,"state_49":true},{"type":"person","title":"Deirdre F. McGrath","avail":true,"age":78,"state_6":true,"state_15":true,"state_17":true,"state_22":true,"state_26":true,"state_36":true,"state_41":true,"state_47":true,"state_50":true},{"type":"person","title":"Simon S. North","avail":true,"age":29,"state_4":true,"state_6":true,"state_7":true,"state_20":true,"state_22":true,"state_34":true,"state_40":true}]},{"type":"role","title":"Sling walruses","children":[{"type":"person","title":"Brandon Poole","state":"s","avail":true,"age":53,"state_4":true,"state_6":true,"state_13":true,"state_15":true,"state_21":true,"state_26":true,"state_27":true,"state_30":true,"state_32":true,"state_41":true,"state_43":true},{"type":"person","title":"Chris Y. Dyer","avail":true,"age":48,"date":1644278400000.0,"state_2":true,"state_7":true,"state_15":true,"state_16":true,"state_18":true,"state_31":true,"state_33":true,"state_47":true,"state_49":true,"state_50":true},{"type":"person","title":"Joanne D. Miller","avail":true,"age":34,"date":106358400000.0,"state_5":true,"state_11":true,"state_16":true,"state_24":true,"state_27":true,"state_39":true,"state_42":true},{"type":"person","title":"Dominic Wright","age":48,"state_9":true,"state_13":true,"state_22":true,"state_28":true,"state_29":true,"state_43":true,"state_45":true},{"type":"person","title":"Jacob S. Stewart","avail":true,"age":34,"date":433382400000.0,"state_1":true,"state_2":true,"state_4":true,"state_5":true,"state_6":true,"state_9":true,"state_10":true,"state_12":true,"state_13":true,"state_33":true,"state_40":true,"state_45":true,"state_49":true,"state_50":true},{"type":"person","title":"Emily Lee","state":"h","avail":true,"age":96,"date":1275177600000.0,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_1":true,"state_14":true,"state_17":true,"state_19":true,"state_22":true,"state_24":true,"state_28":true,"state_34":true,"state_40":true,"state_44":true,"state_48":true,"state_50":true},{"type":"person","title":"Brian J. Parr","state":"s","avail":true,"age":29,"remarks":"Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.","state_2":true,"state_8":true,"state_9":true,"state_10":true,"state_14":true,"state_15":true,"state_16":true,"state_19":true,"state_33":true,"state_43":true,"state_44":true,"state_46":true},{"type":"person","title":"Victoria Duncan","avail":true,"age":62,"state_6":true,"state_10":true,"state_11":true,"state_20":true,"state_22":true,"state_25":true,"state_26":true,"state_30":true,"state_33":true,"state_38":true,"state_40":true,"state_41":true,"state_44":true,"state_49":true},{"type":"person","title":"Sydney Terry","age":56,"date":344304000000.0,"state_3":true,"state_10":true,"state_19":true,"state_20":true,"state_21":true,"state_22":true,"state_25":true,"state_36":true,"state_38":true,"state_41":true,"state_42":true,"state_49":true},{"type":"person","title":"Charles B. Walker","state":"h","avail":true,"age":42,"date":1529539200000.0,"state_16":true,"state_20":true,"state_21":true,"state_27":true,"state_32":true,"state_44":true},{"type":"person","title":"Frank D. Manning","state":"h","avail":true,"age":85,"state_20":true,"state_25":true,"state_27":true,"state_29":true,"state_30":true,"state_38":true,"state_47":true,"state_48":true,"state_50":true},{"type":"person","title":"Tracey MacLeod","avail":true,"age":94,"state_1":true,"state_5":true,"state_9":true,"state_11":true,"state_20":true,"state_27":true,"state_28":true,"state_35":true,"state_39":true,"state_41":true,"state_45":true,"state_46":true}]},{"type":"role","title":"Mow cicadas","children":[{"type":"person","title":"Andrea V. Gibson","avail":true,"age":40,"date":529804800000.0,"state_17":true,"state_19":true,"state_21":true,"state_25":true,"state_39":true,"state_42":true,"state_43":true},{"type":"person","title":"Charles C. Parsons","avail":true,"age":61,"date":411091200000.0,"remarks":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.","state_1":true,"state_4":true,"state_13":true,"state_28":true,"state_32":true,"state_33":true,"state_36":true,"state_43":true,"state_44":true,"state_49":true},{"type":"person","title":"Austin R. Scott","avail":true,"age":86,"date":1352937600000.0,"state_1":true,"state_6":true,"state_11":true,"state_13":true,"state_14":true,"state_17":true,"state_20":true,"state_24":true,"state_28":true,"state_29":true,"state_38":true,"state_41":true,"state_44":true,"state_48":true,"state_49":true},{"type":"person","title":"Una D. Arnold","avail":true,"age":90,"date":1025654400000.0,"state_5":true,"state_9":true,"state_11":true,"state_16":true,"state_18":true,"state_36":true,"state_42":true,"state_44":true,"state_48":true},{"type":"person","title":"Felicity N. Berry","state":"h","avail":true,"age":37,"date":1597363200000.0,"remarks":"Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.","state_1":true,"state_8":true,"state_21":true,"state_32":true,"state_34":true,"state_38":true,"state_44":true,"state_46":true},{"type":"person","title":"Elizabeth Walker","avail":true,"age":23,"state_3":true,"state_6":true,"state_7":true,"state_10":true,"state_14":true,"state_15":true,"state_19":true,"state_22":true,"state_40":true,"state_48":true},{"type":"person","title":"Theresa A. Knox","avail":true,"age":52,"date":1059350400000.0,"state_4":true,"state_9":true,"state_13":true,"state_19":true,"state_21":true,"state_25":true,"state_37":true,"state_43":true,"state_49":true},{"type":"person","title":"Zoe S. Tucker","avail":true,"age":43,"state_1":true,"state_2":true,"state_6":true,"state_11":true,"state_12":true,"state_19":true,"state_28":true,"state_31":true,"state_35":true,"state_38":true,"state_41":true}]}]}]} ================================================ FILE: docs/assets/skeleton.html ================================================
  • Xxxx xx
    • Xxxx xxx xxx
      • Xxxx xxx
    • Xxxx xx xx
  • Xxxx
    • xx xxx
      • Xx xx xxx
    • Xxx xx xxx
================================================ FILE: docs/demo/demo-custom.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ document.getElementById("demo-info").innerHTML = ` Convert and consume data from a custom endpoint (Fake Store API). `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), source: { url: "https://fakestoreapi.com/products/categories" }, columns: [ { id: "*", title: "Product", width: "250px" }, { id: "price", title: "Price ($)", width: "80px", classes: "wb-helper-end", }, { id: "description", title: "Details", width: "*" }, ], columnsResizable: true, types: { category: { colspan: true }, electronics: { icon: "bi bi-cpu" }, jewelery: { icon: "bi bi-gem" }, "men's clothing": { icon: "bi bi-gender-male" }, "women's clothing": { icon: "bi bi-gender-female" }, }, receive: (e) => { const parent = e.node; const parentType = parent.isRootNode() ? "root" : parent.type; switch (parentType) { case "root": // Initial request: we received the list of category names return e.response.map((elem) => { return { title: elem, type: "category", refKey: elem, lazy: true, }; }); case "category": // Lazy-load a category node: return e.response.map((elem) => { return { title: elem.title, type: parent.refKey, price: elem.price, description: elem.description, }; }); } }, lazyLoad: (e) => { return { url: `https://fakestoreapi.com/products/category/${e.node.refKey}`, }; }, render: function (e) { const node = e.node; for (const col of Object.values(e.renderColInfosById)) { switch (col.id) { default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } } }, }); ================================================ FILE: docs/demo/demo-editable.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A treegrid with embedded input controls (also renaming nodes, but no filter or d'n'd). Navigation mode: 'startRow'. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), debugLevel: 5, connectTopBreadcrumb: "output#parentPath", checkbox: true, // fixedCol: true, // navigationModeOption: "row", navigationModeOption: "startRow", // navigationModeOption: "cell", // The JSON only contains a list of nested node dicts, but no types or // column definitions: source: "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_department_M_p.json", types: { department: { icon: "bi bi-diagram-3", colspan: true }, role: { icon: "bi bi-microsoft-teams", colspan: true }, person: { icon: "bi bi-person" }, }, // The `html` properties are shown as comments. // If enabled, it would be used as markup, so `if (e.isNew) {...}` could be // omitted in the `render` callback: columns: [ { title: "Title", id: "*", width: "250px", }, { title: "Age", id: "age", width: "50px", classes: "wb-helper-end", // "html": "", }, { title: "Date", id: "date", width: "100px", classes: "wb-helper-end", // "html": '', }, { title: "Status", id: "state", width: "70px", classes: "wb-helper-center", // "html": `` }, { title: "Avail.", id: "avail", width: "80px", classes: "wb-helper-center", // "html": '', }, { title: "Remarks", id: "remarks", width: "*", // "html": "", menu: true, }, ], columnsResizable: true, columnsSortable: true, edit: { trigger: ["clickActive", "F2"], // "macEnter"], select: true, beforeEdit: function (e) { // console.log(e.type, e); // return e.node.type === "person"; }, edit: function (e) { console.log(e.type, e); }, apply: function (e) { console.log(e.type, e); // Simulate async storage that also validates: return e.util.setTimeoutPromise(() => { e.inputElem.setCustomValidity(""); if (e.newValue.match(/.*\d.*/)) { e.inputElem.setCustomValidity("No numbers please."); return false; } }, 1000); }, }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, }, init: (e) => {}, // load: function (e) { // }, buttonClick: function (e) { console.log(e.type, e); if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } }, change: function (e) { const util = e.util; const node = e.node; const info = e.info; const colId = info.colId; this.logDebug(`change(${colId})`, util.getValueFromElem(e.inputElem, true)); // For demo purposes, simulate a backend delay: return util.setTimeoutPromise(() => { // Assumption: we named column.id === node.data.NAME // We can hand-code and customize it like so: // switch (colId) { // case "author": // case "details": // case "price": // case "qty": // case "sale": // checkbox control // case "avail": // checkbox control // case "state": // dropdown // case "year": // // e.node.data[colId] = e.inputValue; // // ... but this helper should work in most cases: // e.node.data[colId] = util.getValueFromElem(e.inputElem, true); // break; // } // ... but this helper should work in most cases: node.data[colId] = util.getValueFromElem(e.inputElem, true); }, 500); }, render: function (e) { // console.log(e.type, e.isNew, e); const node = e.node; const util = e.util; // Render embedded input controls for all data columns for (const col of Object.values(e.renderColInfosById)) { // Assumption: we named column.id === node.data.NAME const val = node.data[col.id]; switch (col.id) { case "author": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "remarks": // text control if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; // case "details": // text control // if (e.isNew) { // col.elem.innerHTML = ''; // } // util.setValueToElem(col.elem, node.data.details); // break; // case "price": // if (e.isNew) { // col.elem.innerHTML = ''; // } // util.setValueToElem(col.elem, node.data.price.toFixed(2)); // break; case "age": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "state": if (e.isNew) { col.elem.innerHTML = ``; } util.setValueToElem(col.elem, val); break; case "avail": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; // case "qty": // if (e.isNew) { // col.elem.innerHTML = ''; // } // util.setValueToElem(col.elem, val); // break; // case "sale": // checkbox control // if (e.isNew) { // col.elem.innerHTML = ''; // } // // Cast value to bool, since we don't want tri-state behavior // util.setValueToElem(col.elem, !!val); // break; case "date": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; // case "year": // if (e.isNew) { // col.elem.innerHTML = ''; // } // util.setValueToElem(col.elem, node.data.year); // break; default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } } }, }); ================================================ FILE: docs/demo/demo-fixedcol.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A treegrid with a fixed left column: Try horizontal scrolling... Navigation mode: 'cell'. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), // Columns- and types-definition are part of the Ajax response: source: // "../../test/fixtures/tree_department_M_t_c.json", "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_department_M_t_c_comp.json", // "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/fixture_department_1k_3_6_p.json", debugLevel: 5, // header: false, connectTopBreadcrumb: "output#parentPath", // checkbox: true, // minExpandLevel: 1, fixedCol: true, // columnsResizable: true, navigationModeOption: "cell", edit: { trigger: ["clickActive", "F2", "macEnter"], select: true, beforeEdit: function (e) { console.log(e.type, e); // return false; }, edit: function (e) { console.log(e.type, e); }, apply: function (e) { console.log(e.type, e); // Simulate async storage that also validates: return e.util.setTimeoutPromise(() => { e.inputElem.setCustomValidity(""); if (e.newValue.match(/.*\d.*/)) { e.inputElem.setCustomValidity("No numbers please."); return false; } }, 1000); }, }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, // mode: "dim", }, init: (e) => { // e.tree.setFocus(); }, load: function (e) { // e.tree.addChildren({ title: "custom1", classes: "wb-error" }); }, lazyLoad: function (e) { console.log(e.type, e); // return { url: "../assets/json/ajax-lazy-products.json" }; return new Promise((resolve, reject) => { setTimeout(() => { // reject("Epic fail") resolve({ url: "../assets/json/ajax-lazy-products.json" }); }, 1500); }); }, buttonClick: function (e) { console.log(e.type, e); if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } else if (e.command === "menu") { // eslint-disable-next-line no-alert alert("Menu clicked"); } }, change: function (e) { const info = e.info; const colId = info.colId; console.log(e.type, e); // For demo purposes, simulate a backend delay: return e.util.setTimeoutPromise(() => { // Assumption: we named column.id === node.data.NAME switch (colId) { case "sale": case "details": e.node.data[colId] = e.inputValue; break; } // e.node.update() }, 500); }, render: function (e) { // console.log(e.type, e.isNew, e); const node = e.node; const util = e.util; for (const col of Object.values(e.renderColInfosById)) { // Assumption: we named column.id === node.data.NAME const val = node.data[col.id]; switch (col.id) { case "date": if (val) { const dt = new Date(val); col.elem.textContent = dt.toISOString().slice(0, 10); } else { col.elem.textContent = "n.a."; } break; case "mood": { const map = { h: "Happy", s: "Sad" }; col.elem.textContent = map[val] || ""; } break; case "price": col.elem.textContent = "$ " + val.toFixed(2); break; case "qty": // thousands separator col.elem.textContent = val.toLocaleString(); break; case "details": // text control if (e.isNew) { col.elem.innerHTML = ""; } util.setValueToElem(col.elem, val); break; default: // Assumption: we named column.id === node.data.NAME if (typeof val === "boolean") { col.elem.textContent = val ? "X" : ""; } else { col.elem.textContent = val; } break; } } }, }); ================================================ FILE: docs/demo/demo-grid.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 addCssImport */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A readonly treegrid with renaming, 'checkbox: true', 'minExpandLevel: 1'. Navigation mode: 'row/cell'.
Click the button to toggle navigation mode. `; // addCssImport( // "fontawesome6", // "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" // ); new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), source: "../assets/json/ajax-tree-products.json", debugLevel: 5, connectTopBreadcrumb: "output#parentPath", checkbox: true, minExpandLevel: 1, // iconMap: "fontawesome6", // fixedCol: true, // Types are sent as part of the source data: navigationModeOption: "startRow", types: {}, columns: [ { id: "*", title: "Product", width: "250px" }, { id: "author", title: "Author", width: 1, minWidth: "100px", }, { id: "year", title: "Year", width: "50px", classes: "wb-helper-end", }, { id: "qty", title: "Qty", width: "50px", classes: "wb-helper-end", }, { id: "price", title: "Price ($)", width: "80px", classes: "wb-helper-end", }, // In order to test horizontal scrolling, we need a fixed or at least minimal width: { id: "details", title: "Details", width: 3, minWidth: "200px" }, ], columnsResizable: true, columnsSortable: true, dnd: { dragStart: (e) => { if (e.node.type === "folder") { return false; } e.event.dataTransfer.effectAllowed = "all"; return true; }, dragEnter: (e) => { if (e.node.type === "folder") { e.event.dataTransfer.dropEffect = "copy"; return "over"; } return ["before", "after"]; }, drop: (e) => { console.log("Drop " + e.sourceNode + " => " + e.region + " " + e.node); e.sourceNode.moveTo(e.node, e.suggestedDropMode); }, }, edit: { trigger: ["clickActive", "F2", "macEnter"], select: true, beforeEdit: function (e) { console.log(e.type, e); // return false; }, edit: function (e) { console.log(e.type, e); }, apply: function (e) { console.log(e.type, e); // Simulate async storage that also validates: return e.util.setTimeoutPromise(() => { e.inputElem.setCustomValidity(""); if (e.newValue.match(/.*\d.*/)) { e.inputElem.setCustomValidity("No numbers please."); return false; } }, 1000); }, }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, // mode: "dim", }, init: (e) => { console.log(e.type, e); e.tree.findFirst("More...").setExpanded(); e.tree .findFirst((n) => { return n.data.qty === 21; }) .setActive(); // e.tree.setFocus(); }, load: (e) => { console.log(e.type, e); // e.tree.addChildren({ title: "custom1", classes: "wb-error" }); }, buttonClick: function (e) { console.log(e.type, e); if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } }, lazyLoad: function (e) { console.log(e.type, e); // return { url: "../assets/json/ajax-lazy-products.json" }; return new Promise((resolve, reject) => { setTimeout(() => { // reject("Epic fail") // resolve({ url: "../assets/json/ajax-lazy-products.json", params: {foo: 42} , options:{method: "PUT"}}); resolve({ url: "../assets/json/ajax-lazy-products.json" }); }, 1500); }); }, render: function (e) { // console.log(e.type, e.isNew, e); const node = e.node; // const util = e.util; for (const col of Object.values(e.renderColInfosById)) { switch (col.id) { default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } } }, }); ================================================ FILE: docs/demo/demo-large.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A treegrid with about 100,000 nodes. Navigation mode: 'row'. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), debugLevel: 5, connectTopBreadcrumb: "output#parentPath", // checkbox: false, // minExpandLevel: 1, // fixedCol: true, navigationModeOption: "row", source: // "../../test/fixtures/tree_store_XL_t_c_comp.json", "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_store_XL_t_c_comp.json", columnsResizable: true, columnsSortable: true, dnd: { dragStart: (e) => { if (e.node.type === "folder") { return false; } e.event.dataTransfer.effectAllowed = "all"; return true; }, dragEnter: (e) => { if (e.node.type === "folder") { e.event.dataTransfer.dropEffect = "copy"; return "over"; } return ["before", "after"]; }, drop: (e) => { console.log("Drop " + e.sourceNode + " => " + e.region + " " + e.node); e.sourceNode.moveTo(e.node, e.suggestedDropMode); }, }, edit: { trigger: ["clickActive", "F2", "macEnter"], select: true, beforeEdit: function (e) { console.log(e.type, e); // return false; }, edit: function (e) { console.log(e.type, e); }, apply: function (e) { console.log(e.type, e); // Simulate async storage that also validates: return e.util.setTimeoutPromise(() => { e.inputElem.setCustomValidity(""); if (e.newValue.match(/.*\d.*/)) { e.inputElem.setCustomValidity("No numbers please."); return false; } }, 1000); }, }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, // mode: "dim", }, init: (e) => { // e.tree.setFocus(); }, load: function (e) { // e.tree.addChildren({ title: "custom1", classes: "wb-error" }); }, lazyLoad: function (e) { console.log(e.type, e); // return { url: "../assets/json/ajax-lazy-products.json" }; return new Promise((resolve, reject) => { setTimeout(() => { // reject("Epic fail") resolve({ url: "../assets/json/ajax-lazy-products.json" }); }, 1500); }); }, buttonClick: function (e) { console.log(e.type, e); if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } else if (e.command === "menu") { // eslint-disable-next-line no-alert alert("Menu clicked"); } }, change: function (e) { const info = e.info; const colId = info.colId; console.log(e.type, e); // For demo purposes, simulate a backend delay: return e.util.setTimeoutPromise(() => { // Assumption: we named column.id === node.data.NAME switch (colId) { case "sale": case "details": e.node.data[colId] = e.inputValue; break; } // e.node.update() }, 500); }, render: function (e) { // console.log(e.type, e.isNew, e); const node = e.node; const util = e.util; for (const col of Object.values(e.renderColInfosById)) { switch (col.id) { case "price": col.elem.textContent = "$ " + node.data.price.toFixed(2); break; case "year": // date stamp col.elem.textContent = new Date(node.data.year).getFullYear(); break; case "qty": // thousands separator col.elem.textContent = node.data.qty.toLocaleString(); break; case "sale": // checkbox control if (e.isNew) { col.elem.innerHTML = ""; } // Cast value to bool, since we don't want tri-state behavior util.setValueToElem(col.elem, !!node.data.sale); break; // case "details": // text control // if (e.isNew) { // col.elem.innerHTML = ""; // } // util.setValueToElem(col.elem, node.data.details); // break; default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } } }, }); ================================================ FILE: docs/demo/demo-minimal.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ document.getElementById("demo-info").innerHTML = ` A simple tree, no frills (filter will not work). Source node list is inlined, instead of using Ajax. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), source: [ { title: "Node 1", expanded: true, children: [{ title: "Node 1.1" }, { title: "Node 1.2" }], }, { title: "Node 2" }, ], activate: (e) => { alert(`Thank you for activating ${e.node}.`); // eslint-disable-line no-alert }, }); ================================================ FILE: docs/demo/demo-plain.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A simple tree with filter, rename, drag'n'drop, lazy-loading. Auto-focus on init.
100k nodes: click to expand them all. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), // header: "Plain Tree", source: // "../../test/fixtures/tree_fmea_XL_t_flat_comp.json", "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_fmea_XL_t_flat_comp.json", debugLevel: 5, connectTopBreadcrumb: "output#parentPath", checkbox: true, // minExpandLevel: 1, types: {}, dnd: { dragStart: (e) => { if (e.node.type === "folder") { return false; } e.event.dataTransfer.effectAllowed = "all"; return true; }, dragEnter: (e) => { if (e.node.type === "folder") { e.event.dataTransfer.dropEffect = "copy"; return "over"; } return ["before", "after"]; }, drop: (e) => { console.log( `Drop ${e.sourceNode} => ${e.suggestedDropEffect} ${e.suggestedDropMode} ${e.node}`, e ); e.sourceNode.moveTo(e.node, e.suggestedDropMode); }, }, edit: { trigger: ["clickActive", "F2", "macEnter"], select: true, beforeEdit: function (e) { console.log(e.type, e); // return false; }, edit: function (e) { console.log(e.type, e); }, apply: function (e) { console.log(e.type, e); // Simulate async storage that also validates: return new Promise((resolve, reject) => { setTimeout(() => { e.inputElem.setCustomValidity(""); if (e.newValue.match(/.*\d.*/)) { e.inputElem.setCustomValidity("No numbers please."); reject(); } else { resolve(); } }, 500); }); // return new e.util.setTimeoutPromise(() => { // e.inputElem.setCustomValidity(""); // if (e.newValue.match(/.*\d.*/)) { // e.inputElem.setCustomValidity("No numbers please."); // return false; // } // }, 500); }, }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, }, iconBadge: (e) => { const node = e.node; if (node.children?.length > 0 && !node.expanded && node.subMatchCount > 0) { return { badge: node.subMatchCount, badgeTooltip: `${node.subMatchCount} matches`, badgeClass: "match-count", }; } }, init: (e) => { // Tree was loaded and rendered. Now set focus: const node = e.tree.findFirst("Crazies not provided"); node.setActive(); e.tree.setFocus(); }, lazyLoad: function (e) { // User expanded a lazy node for the first time. console.log(e.type, e); // A typical handler would return a URL that should be fetched: // return { url: "../assets/json/ajax-lazy-products.json" }; // ... Simulate a long-running request return new Promise((resolve, reject) => { setTimeout(() => { reject("Epic fail"); // resolve({ url: "../assets/json/ajax-lazy-products.json" }); }, 1500); }); }, }); ================================================ FILE: docs/demo/demo-readonly.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` A read-only treegrid (no d'n'd). Navigation mode: 'cell'. `; new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), debugLevel: 5, connectTopBreadcrumb: "output#parentPath", // checkbox: true, // fixedCol: true, navigationModeOption: "cell", // The JSON only contains a list of nested node dicts (no types or columns): source: "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_department_M_p.json", types: { department: { icon: "bi bi-diagram-3", colspan: true }, role: { icon: "bi bi-microsoft-teams", colspan: true }, person: { icon: "bi bi-person" }, }, columns: [ { title: "Title", id: "*", width: "250px" }, { title: "Age", id: "age", width: "60px", classes: "wb-helper-end" }, { title: "Date", id: "date", width: "90px", classes: "wb-helper-end" }, { title: "Status", id: "state", width: "70px", classes: "wb-helper-center", }, { title: "Avail.", id: "avail", width: "60px", classes: "wb-helper-center", }, { title: "Remarks", id: "remarks", width: "*" }, ], columnsResizable: true, columnsSortable: true, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, }, init: (e) => {}, buttonClick: function (e) { console.log(e.type, e); if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } }, render: function (e) { // console.log(e.type, e.isNew, e); const node = e.node; // const util = e.util; // Render formatted data values for all columns for (const col of Object.values(e.renderColInfosById)) { // Assumption: we named column.id === node.data.NAME const val = node.data[col.id]; switch (col.id) { case "date": if (val) { const dt = new Date(val); col.elem.textContent = dt.toISOString().slice(0, 10); } else { col.elem.textContent = "n.a."; } break; case "state": { const map = { h: "Happy", s: "Sad" }; col.elem.textContent = map[val] || "n.a."; } break; case "avail": col.elem.textContent = val ? "Yes" : "No"; break; default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = val; break; } } }, }); ================================================ FILE: docs/demo/demo-select.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10, addCssImport */ /* eslint-disable no-console */ document.getElementById("demo-info").innerHTML = ` Hierarchical selection demo (selectMode: 'hier') . Uses icons from Font Awesome.
Collapse nodes to test select counter badges. `; // document.getElementById("selectMode").classList.remove("hidden"); addCssImport( "fontawesome6", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" ); new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), header: "Select Tree", // selectMode: "single", // iconMap: "fontawesome6", selectMode: "hier", checkbox: true, // minExpandLevel: 1, types: {}, source: [ { title: "n1", expanded: true, children: [ { title: "n1.1" }, { title: "n1.2" }, { title: "n1.3", lazy: true }, ], }, { title: "n2", expanded: true, children: [ { title: "n2.1 (selected)", selected: true }, { title: "n2.2", selected: false }, { title: "n2.3", selected: null }, ], }, { title: "n3", expanded: true, children: [ { title: "n3.1", expanded: true, children: [ { title: "n3.1.1 (unselectable)", unselectable: true }, { title: "n3.1.2 (unselectable)", unselectable: true }, { title: "n3.1.3" }, ], }, { title: "n3.3", expanded: true, children: [ { title: "n3.2.1 (unselectable)", unselectable: true }, { title: "n3.2.2 (unselectable, selected)", unselectable: true, selected: true, }, { title: "n3.2.3" }, { title: "n3.2.4 (selected)", selected: true }, ], }, { title: "n3.4 (radiogroup)", expanded: true, radiogroup: true, checkbox: false, children: [ { title: "n3.4.1 (unselectable: true)", unselectable: true, }, { title: "n3.4.2 (selected)", selected: true, }, { title: "n3.4.3" }, ], }, ], }, ], debugLevel: 5, connectTopBreadcrumb: "output#parentPath", init: (e) => { // Tree was loaded and rendered. Now set focus: e.tree.setFocus(); }, lazyLoad: function (e) { return e.util.setTimeoutPromise(() => { return { url: "../assets/json/ajax-lazy-products.json" }; }, 4000); }, filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", // modeButton: "#filter-hide", // using a custom handler nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", }, }, iconBadge: (e) => { const node = e.node; if (node.expanded || !node.children) { return; } const count = node.children && node.getSelectedNodes()?.length; return { badge: count, badgeTooltip: `${count} selected`, badgeClass: "selection-count", }; }, beforeSelect: function (e) { console.log(e.type, e); }, select: function (e) { console.log(e.type, e, e.tree.getSelectedNodes()); document.getElementById("tree-info-custom").textContent = `Selected: ${e.tree.getSelectedNodes(true)}`; }, }); ================================================ FILE: docs/demo/demo-welcome.js ================================================ /** * Demo code for Wunderbaum (https://github.com/mar10/wunderbaum). * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). */ /* global mar10 */ document.getElementById("demo-tree").innerHTML = `

Demo Application


  • Select a demo in the navigation tree on the left side.
  • Use buttons (, , etc.) and checkboxes above the demo trees to apply commands.
  • Click the View Source Code link below the demo trees to view implementation details.
  • The navigation tree on the left of this demo app is also implemented with Wunderbaum.
    Check the source code of this demo app for some ideas how to implement GUI controls for the tree.
`; ================================================ FILE: docs/demo/index.html ================================================ Demo | Wunderbaum

Wunderbaum Demo

Loading…

Wunderbaum v?.?.? © 2021-2025 Martin Wendt
================================================ FILE: docs/demo/navigation.js ================================================ /* global mar10 */ /* eslint-disable no-console */ document.addEventListener("DOMContentLoaded", (event) => { /* --------------------------------------------------------------------------- * Navigation */ const is_local = !!window.location.hostname.match(/127.0.0.1/); const util = mar10.Wunderbaum.util; const navTree = new mar10.Wunderbaum({ id: "navigation", header: "Wunderbaum", element: document.querySelector("#nav-tree"), // checkbox: false, minExpandLevel: 2, debugLevel: 2, types: { link: { icon: "bi bi-link-45deg", classes: "wb-helper-link" }, show: { icon: "bi bi-file-code" }, }, source: [ { title: "GitHub Project", type: "link", icon: "bi bi-github", href: "https://github.com/mar10/wunderbaum", }, { title: "User Guide", type: "link", href: "../index.html", }, { title: "API Reference", type: "link", href: "../api", }, { title: "Unit Tests", type: "link", href: is_local ? "../../test/unit/test-dev.html" : "../unittest/test-dist.html", }, { title: "Demo", type: "folder", expanded: true, children: [ { title: "Welcome", type: "show", key: "demo-welcome", icon: "bi bi-info-square", }, { title: "Minimal", type: "show", key: "demo-minimal" }, { title: "Plain", type: "show", key: "demo-plain" }, { title: "Select", type: "show", key: "demo-select" }, { title: "Treegrid", type: "show", key: "demo-grid" }, { title: "Large Grid", type: "show", key: "demo-large" }, { title: "Readonly", type: "show", key: "demo-readonly" }, { title: "Editable", type: "show", key: "demo-editable" }, { title: "Fixed Column", type: "show", key: "demo-fixedcol" }, { title: "Custom Data", type: "show", key: "demo-custom" }, ], }, ], init: (e) => { // We do not get a 'hashchange' event on page load, so we call directly: reconfigureTree(window.location.hash || "demo-welcome"); }, keydown: (e) => { const node = e.tree.getActiveNode(); if (e.eventName === "Enter" && node && node.type === "show") { window.location.hash = node.key; } }, click: (e) => { switch (e.node.type) { case "link": window.open(e.node.data.href); break; case "show": // Trigger a 'hashchange' event: window.location.hash = e.node.key; break; } }, }); /* --------------------------------------------------------------------------- * Demo Behavior */ window.addEventListener("hashchange", (e) => { console.log(e.type, e); reconfigureTree(window.location.hash); }); document.querySelectorAll("output.tree-version").forEach((elem) => { elem.textContent = mar10.Wunderbaum.version; }); /** * Handle checkboxes that set global modifier classes, e.g. `wb-rainbow`, ... */ util.onEvent(document, "change", "input.auto-class-setter", (e) => { document .getElementById("demo-tree") .classList.toggle(e.target.dataset.classname, e.target.checked); }); toggleButtonCreate("#filter-hide", (e, flag) => { const tree = mar10.Wunderbaum.getTree("demo"); tree.setOption("filter.mode", flag ? "hide" : "dim"); }); toggleButtonCreate("#show-checkboxes", (e, flag) => { const tree = mar10.Wunderbaum.getTree("demo"); tree.setOption("checkbox", !!flag); }); toggleButtonCreate("#disable-tree", (e, flag) => { const tree = mar10.Wunderbaum.getTree("demo"); tree.setOption("enabled", !flag); }); toggleButtonCreate("#enable-cellnav", (e, flag) => { const tree = mar10.Wunderbaum.getTree("demo"); if (tree.isRowNav() && tree.isGrid()) { tree.setCellNav(); } else { tree.setCellNav(false); } return tree.isCellNav(); }); document .querySelector("#toggle-expand-all") .addEventListener("click", (e) => { const tree = mar10.Wunderbaum.getTree("demo"); tree.expandAll(!tree.getFirstChild().isExpanded()); }); document .querySelector("#toggle-select-all") .addEventListener("click", (e) => { const tree = mar10.Wunderbaum.getTree("demo"); const label = tree.logTime(`selectAll()`); tree.toggleSelect(); // tree.selectAll(!tree.getFirstChild().isSelected()); tree.logTimeEnd(label); }); /* Update info pane every second. This is fast and we handle exceptions, so 'evil' setInterval() should be Ok here. */ const STATUS_UPDATE_INTERVAL = 1_000; setInterval(() => { try { const demoTree = mar10.Wunderbaum.getTree("demo"); showStatus(demoTree); } catch (error) { console.error("showStatus() failed", error); } }, STATUS_UPDATE_INTERVAL); }); /** */ function addCssImport(tag, url) { let linkElem = document.querySelector(`link[data-tag="${tag}"]`); if (!linkElem) { linkElem = document.createElement("link"); linkElem.setAttribute("rel", "stylesheet"); linkElem.setAttribute("href", url); linkElem.setAttribute("data-tag", tag); document.head.appendChild(linkElem); } return linkElem; } /** * Toggle button */ function toggleButtonCreate(selector, onChange) { const buttonElem = document.querySelector(selector); buttonElem.classList.add("toggle-button"); buttonElem.addEventListener("click", (e) => { buttonElem.classList.toggle("checked"); const res = onChange(e, buttonElem.classList.contains("checked")); if (typeof res === "boolean") { buttonElem.classList.toggle("checked", res); } }); } /** * */ function loadScript( url, async = true, module = true, type = "text/javascript", destroyExisting = true ) { return new Promise((resolve, reject) => { console.log(`Loading script ${url}...`); // Update address of 'View Source Code' link: const sourceLink = document.getElementById("sourceLink"); sourceLink.setAttribute("href", url); // Remove previously loaded demo scripts and event listeners: if (destroyExisting) { document.querySelectorAll("script.demo-case-handler").forEach((elem) => { console.log("Remove old script:", elem); elem.remove(); }); } // const scriptElem = document.querySelector("#demo-tree-script"); const scriptElem = document.createElement("script"); if (module) { scriptElem.setAttribute("type", "module"); } scriptElem.setAttribute("type", type); scriptElem.setAttribute("async", async); scriptElem.classList.add("demo-case-handler"); document.body.appendChild(scriptElem); scriptElem.setAttribute("src", url); scriptElem.addEventListener("load", (e) => { console.log(`Loading script ${url} done.`); resolve(e); }); scriptElem.addEventListener("error", (e) => { console.error("Loading script %s... ERROR:", url, e); reject(e); }); }); } /** * * @param {*} options */ function reconfigureTree(tag = null) { const navTree = mar10.Wunderbaum.getTree("navigation"); let demoTree = mar10.Wunderbaum.getTree("demo"); const detailsElem = document.getElementById("demo-info"); console.info("reconfigureTree(%s), tree=%s", tag, demoTree, demoTree); if (tag == null) { tag = window.location.hash; } tag = tag.replace(/^#/, ""); tag = tag || "demo-welcome"; const isWelcome = tag === "demo-welcome"; const label = `reconfigureTree(${tag})`; console.time(label); window.location.hash = tag; detailsElem.classList.remove("error"); detailsElem.textContent = `Loading demo '${tag}'...`; // Elements that are hidden from the initial welcome page: document.querySelectorAll(".hide-on-welcome").forEach((elem) => { elem.classList.toggle("hidden", isWelcome); }); // Elements that are hidden on every page change (need to explicitly show by demo code): document.querySelectorAll(".hide-on-init").forEach((elem) => { elem.classList.add("hidden"); }); document.querySelectorAll(".clear-on-init").forEach((elem) => { elem.innerHTML = ""; }); demoTree?.destroy(); demoTree?.element.classList.add("wb-initializing"); const url = `./${tag}.js`; navTree.setActiveNode(tag); loadScript(url) .then(() => { demoTree = mar10.Wunderbaum.getTree("demo"); console.debug("Script %s was run. tree:", url, demoTree); if (!demoTree) { detailsElem.innerHTML = " "; console.timeEnd(label); return; } // Update GUI controls from current tree settings. demoTree.ready.then(() => { console.timeEnd(label); // console.info("Reloaded tree is initialized!") document .getElementById("show-checkboxes") .classList.toggle("checked", !!demoTree.getOption("checkbox")); document .getElementById("filter-hide") .classList.toggle( "checked", demoTree.getOption("filter.mode") === "hide" ); document .getElementById("enable-cellnav") .classList.toggle( "checked", demoTree.isGrid() && demoTree.isRowNav() ); }); }) .catch((e) => { detailsElem.classList.add("error"); detailsElem.innerHTML = `${e}`; }); } /** * * @param {*} tree * @param {*} options */ function showStatus(tree) { // const tree = mar10.Wunderbaum.getTree("demo"); const info = document.querySelector("#tree-info"); const nodeListElem = document.querySelector("#demo-tree .wb-node-list"); if (!tree || !nodeListElem) { info.textContent = "n.a."; return; } const elemCount = nodeListElem.childElementCount; const activeNode = tree.getActiveNode(); const focusNode = tree.getFocusNode(); const focusNodeInfo = activeNode && focusNode === activeNode ? " (has focus)" : ", Focus: " + focusNode; const msg = `Nodes: ${tree.count().toLocaleString()}, rows: ${tree .count(true) .toLocaleString()}, rendered: ${elemCount}. Active: ${activeNode}${focusNodeInfo} `; info.textContent = msg; tree._check(); } ================================================ FILE: docs/demo/style.css ================================================ body { --font-color: darkslategray; --font-color-header: whitesmoke; --bg-color-dark: #707070; /* --bg-color-dark: #7895cb; */ --bg-color-dimmed: whitesmoke; --bg-color-light: white; --hint-color: lightyellow; --link-color: #1d47be; --error-color: red; --border-radius: 4px; } * { box-sizing: border-box; padding: 0; margin: 0; } ul { padding: revert; } body { font-family: "Segoe UI", Candara, "Bitstream Vera Sans", "DejaVu Sans", "Bitstream Vera Sans", "Trebuchet MS", Verdana, "Verdana Ref", sans-serif; background-color: var(--bg-color-dark); color: var(--font-color); /* `` switches to standard mode, which would define 100% as content height. We want 100% to interpreted as viewport height: */ height: 100%; /* prevent scrollbars, which would break `100vh` */ overflow: hidden; } img.logo { height: 1em; vertical-align: text-bottom; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.5); padding: 3px; background-color: var(--bg-color-dimmed); border-radius: 2px; } div.grid-wrapper { display: grid; width: 100vw; width: 100dvw; /* 100vw minus width of scrollbar */ height: 100vh; height: 100dvh; /* 100vh minus height of address bar */ grid-template-columns: 1fr 4fr; grid-template-rows: 3em 6em auto 5em 1em; padding: 5px; grid-gap: 2px; } div.grid-wrapper div.menu { background-color: var(--bg-color-dark); color: var(--font-color-header); grid-column: 1 / span 2; grid-row: 1; } div.grid-wrapper nav.nav { grid-column: 1; grid-row: 2 / 5; /* Needed for fixed columns: embedded tree 100% width should not stretch the grid layout: */ overflow: hidden; } div.grid-wrapper div.flex-container { grid-column: 2; grid-row: 2 / 5; display: flex; flex-direction: column; height: 100%; overflow: hidden; } section.header { border-top-left-radius: var(--border-radius); border-top-right-radius: var(--border-radius); background-color: var(--bg-color-light); padding: 5px; font-size: 90%; /* min-height: 20px; */ max-height: 100px; } main.view { background-color: var(--bg-color-light); min-height: 4px; /* Needed for fixed columns: embedded tree 100% width should not stretch the grid layout: */ overflow: hidden; flex-grow: 1; } section.footer { padding: 5px; font-size: 90%; background-color: var(--bg-color-dimmed); border-bottom-left-radius: var(--border-radius); border-bottom-right-radius: var(--border-radius); /* min-height: 20px; */ max-height: 100px; } div.grid-wrapper div.statbar { grid-column: 1 / span 2; grid-row: 5; text-align: right; font-size: 80%; padding: 1px 4px; color: var(--font-color-header); } /* Suppress system focus border. */ div.wunderbaum:focus-visible { outline-style: none; } div#demo-tree { /* fill parent container */ height: calc(100% - 18px); /* Leave some space for the blue focus outline*/ /* padding: 5px; */ } div#nav-tree { background-color: var(--bg-color-dimmed); /* background-color: transparent; */ overflow: hidden; /* keep background color outside round borders */ border: none; } /* Show tree skeleton while initializing. */ div.wunderbaum.wb-skeleton.wb-initializing { background-position-y: 20px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJsAAAC+CAYAAAAiAH0JAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAdHSURBVHhe7d1PiCRXHcDxfu9VdTLd1TU5JKLGECVK/mhY/AMRwYuHCMkGT4KEoAcvi8GIOQo5KAkeRIjiRRGEaI4qRgVR0DUnFT0sMYEQDXhRYwLZdPdUb3dV9fNV5gdZnd7JTk/n1/Wqvx/YnffeZbp7vvOHqtdVPQAAALSekY9HFEVxfVmW76rrWlbiYIzppWm6yLLsOVlCS6yMbTwe2/DhfAjt4977w8WIOOdeC9F9Os/z38gSWqCJ6ghr7XerqooytEb4JtkPj/1X0+l0X5bQAitjWywWK9djEoKz4XnIDG0QfVSIB7FBDbFBDbFBDbFBDbFBDbFBDbFBzZHYvJ/tWWuuk2nUkuHwHTJEC6z6ybYfYrtRxlGzzt0uQ7TAkdiM2ft3WH5WptGy1vaWRfFbmaIFVv7NliTJN8IXK0QXJ2PMIjz+x8LzOJAltMBx+9nOlGX5iUj3s13MsuwHsgQAAAAAAAC81a54nK1NxuNxYq19qKqqe5fLpaziaiVJ0hx/PDcajV6Qpa2IIrbpdPpQWZbfivWthW3gnHshTdP7BoPB87KkLootRiG0uwntdMJvhPctFov3yHQrooiN0E6veQ23/SdIFLGhG4gNaogNaogNaogNaogNaogNaogNalofm/ezM87ZO2WKU0gGgwdkuBWtj82YvQvLpX9GpjiFejb7kQy3Iopfo2maXmjeNYX1Oef+1e/3/yPTrYhli9G1IbbHq6o6y3nSkwuhNVuMHsjz/LwsAQAAAAAAYLM4UrqmoijunM/nN3XpuJ9cbqzIsuwtOR5HbGuYTCYfXC6XP63r+mZZ6gxr7UH4dy5JkicHg8FGv5OIbQ3j8fjPVVV9WKad05xxCMHtj0ajsSxtBFuM1hBCk1E3NVcbLctSZptDbFBDbFBDbFBDbFBDbFBDbFBDbFBDbFBDbCfk/exGY8xIpp2Vjka3yXBjiO3krgn/ksNhd5lebyjDjSG2EzJm70Xn3Ksy7aTm3Ki/dOkvMt0YYltD+DX6JWvtP2XaKeF5zcLz+0IIbipLG8OujzUVRfGR+Xx+Swf3s02zLPulLAEAAAAAAODqRHGcbTweX2OMeXSXr8/WHANLkuTZ8PzP5Xn+iixHJYrYptPpI2VZfo0LAb5+Fc5fjEaj+2QalShOV4XQ7iK0Q+Gn+10yjE4UsRHaG2J+LTgRDzXEBjXEBjXEBjXEBjXEBjXEBjXEBjWtj41bQP4fY0YH3n9KZlFpfWzcAvJ/mV5vMjTmZzKNShS/Rvv9/u+bXQ/oNTs/npZhdGLZYpRaa79SluW9u3qe9LItRl/O8/yiLAMAAAAAAKAdrnicrSiKO8qy/FhzHyMNcrmmWZZlT8oSOmZlbCG0dy8Wi/Pe+5s1D6KG4Ern3KNJknx9MBhs/k5d2KqVp6uqqnpkuVyqhtYIny8Nn/er4fO/TZbQIVeKTUb6Qmydv8XiroriRDy6gdightightightightightightig5khs3s+ud86+V6ZbkWbZR2WIDln1k23uvZ/IeCv8cvmyDNEhR2IzZm/iXPKSTNU1tx80Zfl3maJDVv7NZq19MHzR/6T9Xk25/eBnwufu5O0Vd91x+9neWZblLcr72cosy/4gSwAAAAAAAEAsYrk+254x5vtVVbXm+mzNccHmwnzh8Xwuz/NXZRnHiCK26XT6zbIsH25LaJdL0/SHo9HoszLFMaLYYhRCu7WNoTXCT9vbZIg3EUVsbQ2t0ebH1jZRxIZuIDaoITaoITaoITaoITaoITaoITaoITaoiSI2a9v7MNv82NomilcqSZKntN9WeDVk58dTMsWbiGWLkXXOfb65BWRzzd02aELr9/tP13X97TzPuQgwAAAAAADAboviOFsbFUVxdj6ff6jL70Fozo70+/1fDwaDjVzGjNjWMJlM7q/r+nvL5XIoS50VgnstTdNPDofDP8rS2jixt4YQ2cO7EFojPM/9qqq+KNNTIbY1aF2Nsy029XyJDWqIDWqIDWqIDWqIDWqIDWqIDWqIDWqI7YS8n52x1rxdpjvBOPf+A+9vkunaiO3kXvS+t1vX0PX+H+H/U9+Wk9hO6PAWmW6nYkuce3lozCWZro3Y1nN/kiQX2vhe1k2S98X+znv/oCydCluM1lQUxQ3z+Xx/B/azvTIYDC7KEgAAAAAAkYnm0EdRFP3FYpG25ZJZ2uQSXfVwODz1wdVtiSK28XichQ8/ruv67l29V1QTm3Pu+TA8m+f53w5X4xLFGYTwQj9eVdXOhtZonnt4DW4Nw58crsQnitjCi7xTuyyOE/NrEUVsu/wTrUs4EQ81xAY1xAY1xAY1xAY1xAY1xAY1rY/N+1lzSo1vijeYA++djKMSwxfxA87ZO2QMY5orXt5zOIlL62MzZu8Z73t/lenOs8ZMh8b8XKZRieLXU5IkTxhjdnNv0WXCa+Cdc0/INDox7Wc7u1gs7tnx/WzPDYfD78gSAAAAAMSq1/svLQc8FjMNXjkAAAAASUVORK5CYII=); background-repeat: no-repeat; } div.wunderbaum span.wb-badge { &.selection-count { color: white; background-color: green; } &.match-count { color: darkgreen; background-color: #e0e0e0; } } div.welcome-page { padding: 4px 4px; } output.error { color: var(--error-color); } output.hint { display: block; width: 100%; padding: 1px 4px; margin: -2px -1px 0 -4px; color: var(--font-color); background-color: var(--hint-color); font-style: italic; } output.tree-version { font-size: 40%; font-family: "Courier New", Courier, monospace; font-weight: lighter; } output#parentPath:not(.hidden) { font-size: 80%; display: block; padding: 1px 3px; height: 18px; background-color: var(--bg-color-light); } .hide-on-welcome.hidden, .hide-on-init.hidden { display: none; } hr { margin: 1px 0; border: none; background-color: var(--font-color); height: 1px; } a { text-decoration: none; color: var(--link-color); } a:hover, a:active { text-decoration: underline; } a.neutral, a.neutral:hover, a.neutral:active { text-decoration: none; color: unset; } button.icon-button, button.toggle-button { border: 1px solid var(--bg-color-light); border-radius: 3px; background-color: var(--bg-color-light); color: var(--link-color); padding: 1px 2px; } button.icon-button:hover, button.toggle-button:hover { border-color: var(--link-color); } button.toggle-button.checked { background-color: var(--link-color); color: var(--bg-color-light); } ================================================ FILE: docs/googlecc7a2a5f2bb40f68.html ================================================ google-site-verification: googlecc7a2a5f2bb40f68.html ================================================ FILE: docs/index.md ================================================ # User Guide [![GitHub version](https://img.shields.io/github/v/release/mar10/wunderbaum?display_name=tag&sort=semver)](https://github.com/mar10/wunderbaum/releases/latest) > A modern JavaScript tree/treegrid control. !!! info Wunderbaum has beta status: API, Markup, Stylesheet, etc. are still subject to change. - [Quick Start](tutorial/quick_start.md) - [API Reference](https://mar10.github.io/wunderbaum/api/index.html){:target="\_blank"} - [Online Demo](https://mar10.github.io/wunderbaum/demo/){:target="\_blank"} - [Source Code on GitHub](https://github.com/mar10/wunderbaum){:target="\_blank"} ![logo](assets/teaser_1.png) - Supports drag and drop, editing, filtering, sorting, and multi-selection. - Written in TypeScript, transpiled to ES6 (esm & umd). - Performant handling of _big_ data structures. - Provide an object oriented API. - Framework agnostic. - Zero dependencies. - Keyboard support. ================================================ FILE: docs/tutorial/concepts.md ================================================ # Concepts This document describes some general concepts of Wunderbaum. ## Design Goals - Implement a **treegrid** control with emphasis on "tree".
Depending on the number of columns and nesting depth, Wunderbaum can also be used as a **plain tree**, **plain grid**, or a **simple list** control. - **Performant** and efficient handling of **big data structures**. - Use modern technologies with **zero dependencies** (except for icon fonts you may want to use).
Drop legacy support (IE, jQuery, ...). - Robust, consistent handling of parallel, asynchronous behavior. - Built-in support for [drag and drop](tutorial_dnd.md), [editing](tutorial_edit.md), [filtering](tutorial_filter.md), [multi-selection](tutorial_select.md). - Fully [controllable using the keyboard](tutorial_keyboard.md). - Framework agnostic. - Good documentation. - Written in TypeScript, transpiled to JavaScript ES6 with type hints (.esm & .umd). ## Main Concepts We have a tree **data model** as the backbone, i.e. an instance of the `Wunderbaum` class that contains a hierarchical structure of `WunderbaumNode` objects. A node may be active, selected, focused, and/or hovered. These **node states are independent**, so one node can have all, some, or none of these states at the same time. See [[FAQ]] 'What statuses can a node have?'. This structure is initialized on startup from a JavaScript data structure, an Ajax JSON response or a generator function. The tree's data model can be accessed and modified using an extensive **object oriented API** like `tree.getNodeByKey()`, `node.setTitle()`, or `node.setExpanded()`. The rectangular, scrollable area on the page, that contains the (potentially much larger) tree is called **viewport**.
HTML markup is **rendered on demand**, i.e. only for nodes that are visible inside the _viewport_.
Also children of collapsed parent nodes don't have HTML elements.
This concept allows to hold a _huge_ data model (100k+ nodes) in the frontend, while only having a few HTML elements materialized in the DOM. Developers should not manipulate the html directly, but change the data model and then call `tree.update()` or `node.update()` if needed.
Due to lazy rendering, it is not possible to bind events to all node HTML elements directly. However this is rarely necessary, since Wunderbaum offers event handlers like `click`, `dblclick`, and `keypress`. Use event delegation otherwise. A tree is usually set up for **lazy loading**: For one thing, the tree initialization may be delayed to an asynchronous Ajax request. This will result in a fast page load, and an empty tree displaying a spinner icon until the data arrives.
Additionally, single child nodes may be marked 'lazy'. These nodes will generate Ajax requests when expanded for the first time. Lazy loading allows to present hierarchical structures of infinite size in an efficient way. But since neither all DOM elements nor even the complete tree data model is available in the browser, API functions like `tree.getNodeByKey()` or `node.findAll()` may not work as expected. Some API functions are potentially **asynchronous**. For example `node.setExpanded()` on a lazy node may have to issue an Ajax request, wait for its response and then scroll and render new nodes. These functions generally return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), so handling deferred responses is easy: ```js node.setExpanded().then(() => { alert("expand has finished"); }); ``` or ```js await tree.expandAll(); alert("expand has finished"); ``` Activities and state changes generate **events**. Event handlers are used to implement behavior: ```js const tree = new Wunderbaum({ // ... activate: (e) => { console.log("Node was activated:", e.node); }, change: (e) => { console.log("Grid data was modified:", e); }, render: (e) => { // e.node was rendered. We may now modify the markup... }, }); ``` ================================================ FILE: docs/tutorial/contribute.md ================================================ # Contributors Guide First off: thank you for your contribution :heart:
This open source project would not be possible without your support! There are many ways how you can help: - Use and test the library. Provide constructive feedback. - Spread the word: Star this GitHub project, share links, or mention it if you find it useful. - Improve the documentation. - Report - or fix - bugs (see below). - Suggest - or implement - new features (see below). - Donate. ## Report Bugs Issues can be reported in the [bug tracker](https://github.com/mar10/wunderbaum/issues). !!! info Try your best to make fixing as easy as possible. Do not assume that bugs are fixed, just because they are reported: Please understand that issues are likely to be closed, if they are hard to reproduce. A bug report should contain: - A short description of the problem. - A [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example): - See here for [a wunderbaum triage template](https://github.com/mar10/wunderbaum/blob/main/test/triage/issue_000.html).
Copy and rename this file, then edit it to reproduce the issue. It can be opened directly in the browser. - See here for [a wunderbaum JS Bin template](https://jsbin.com/lecasinava/edit?html,js,output). Copy and edit this template to reproduce the issue. - The expected result. - The actual result. - The version of the library. - The version of the browser. Of course a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) is the most welcomed form of contribution. 😉
Do not forget to add an entry to the `CHANGELOG.md`. ## Request New Features Features can be requested and discussed in the [bug tracker](https://github.com/mar10/wunderbaum/issues), or - often more adequate - in the [discussion forum](https://github.com/mar10/wunderbaum/discussions). !!! info Please understand that feature requests sometimes are rejected due to the lack of resources, or because they do not fit into the _greater plan_ or paradigm.
This does not mean that the proposal is bad, so do not feel offended. If you plan to contribute a feature via a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) we highly recommend to discuss the approach beforehand to avoid effort. Keep in mind that a feature implementation also includes tests, documentation, and updates to the demo page. ## Development ### Edit TypeScript sources install node.js 18+ Checkout the project from GiHub, then ```bash $ cd path/to/project $ corepack enable $ corepack install ``` ```bash $ cd path/to/project $ yarn $ yarn dev ``` You can now edit the files in `.../wunderbaum/src` folder. TypeScript and SCSS files are automatically transpiled to the `.../wunderbaum/build` folder. Reformat according to the style guide, run unit tests, build, or compile a version using these commands: ```bash $ yarn format $ yarn test ``` !!! note Don't forget to call `yarn format` regularly and before committing: Formatting errors will be rejected by the CI pipeline. ### Edit Documentation The documentation is written in Markdown and can be found in the `docs` folder. The User Guide is generated using [MkDocs](https://www.mkdocs.org/) and the API documentation is generated using [TypeDoc](https://typedoc.org/). **API Documentation** The API reference is generated from the TypeScript sources by TypeDoc and the resulting files are stored in the `docs/api` folder. This is done by the build script or manually by running: ```bash $ yarn api_docs ``` **User Guide** The user guide (i.e. tutorial) is written in Markdown and can be found in the `docs/tutorial` folder. It is rendered to HTML using MkDocs by a GitHub action and published as GitHub pages every time we commit. !!! note In order to generate the User Guide documentation locally, we need to have [Python](https://www.python.org/) and [pipenv](https://pipenv.pypa.io/en/stable/index.html) installed. Then install the required packages: ```bash $ cd path/to/project $ pipenv install ``` Run the following command to start the MkDocs server: ```bash $ yarn dev_mkdocs ``` You can now edit files in the `docs/tutorial` folder and see the changes in the browser. ### Release For a local test build, run the following commands: ```bash $ yarn build ``` A new version is released by creating a new tag in the format `vX.Y.Z` and ```bash $ grunt yabs:release:patch ``` ================================================ FILE: docs/tutorial/migrate.md ================================================ # Migrate from Fancytree to Wunderbaum ## What has Changed? **Main Changes to [Fancytree](https://github.com/mar10/fancytree/):** - Written in TypeScript, transpiled to JavaScript ES6 with type hints (.esm & .umd). - Removed dependecy on jQuery and jQuery UI. - Dropped support for Internet Explorer. - Markup is now `
` based, instead of `
    /
  • ` and ``. - Grid layout is now built-in standard. A plain tree is only a special case thereof. - New viewport concept (implies a fixed header and a scrollable body). - 'Clone' suppport is now built-in standard (i.e. support for duplicate `refKey`s in addition to unique `key`s). - Built-in html5 drag'n'drop. - Titles are always XSS-safe now (former explicit `escapeTitles: true`). - 'folder' is no longer a special built-in node type. Instead, the application can define any number of node types. **Missing Features (as of today)** - Persistence ## Migration Guide Many general concepts from Fancytree are still the same, see the [Concepts](concepts.md) for a general overview. Especially the rendering model and grid support has changed significantly, however. | Feature | Fancytree | Wunderbaum | | --------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | Dependency | Requires jQuery and jQuery UI | No dependency on jQuery | | Browser Support | Supports Internet Explorer | Dropped support for Internet Explorer | | Markup | `
      /
    • ` and `
    ` based | `
    ` based | | Layout | Grid layout as an extension | Grid layout built-in | | Viewport | No built-in viewport concept | New viewport concept with fixed header and scrollable body | | Drag and Drop | Requires additional plugins | Built-in HTML5 drag and drop | | XSS Safety | Requires `escapeTitles: true` | Titles are always XSS-safe | | Node Types | 'folder' as a special node type | Application-defined node types | | Performance | Lazy rendering of HTML elements, but may be slow with large number of expanded nodes. | Handles large number of nodes (100k+) | ### Replace the Script and CSS dependencies | Init | Fancytree | Wunderbaum | | -------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Tutorial | [Tutorial](https://github.com/mar10/fancytree/wiki#embed-fancytree-on-a-web-page) | [Tutorial](https://mar10.github.io/wunderbaum/tutorial/quick_start/) | ### Tree Options Options are mostly the same, but some property names have changed.
    Major changes were made to the `grid` and `dnd` configuration. | Options | Fancytree | Wunderbaum | | --------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Reference | [API](https://wwwendt.de/tech/fancytree/doc/jsdoc/global.html#FancytreeOptions) | [API](https://mar10.github.io/wunderbaum/api/interfaces/wb_options.WunderbaumOptions.html) | | Tutorial | [Docs](https://github.com/mar10/fancytree/wiki) | [Docs](https://mar10.github.io/wunderbaum/tutorial/tutorial_initialize/) | ### Tree and Node Properties - `refKey` and `refType` are now direct properties of the node object. - `node.folder` was dropped in favor af a more general `node.type` property. | Properties | Fancytree | Wunderbaum | | -------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Tree Reference | [Tree model](https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html) | [Tree model](https://mar10.github.io/wunderbaum/api/classes/wunderbaum.Wunderbaum.html) | | Node Reference | [Node model](https://wwwendt.de/tech/fancytree/doc/jsdoc/FancytreeNode.html) | [Node model](https://mar10.github.io/wunderbaum/api/classes/wb_node.WunderbaumNode.html) | | Tutorial | — | [Tutorial](https://mar10.github.io/wunderbaum/tutorial/tutorial_initialize/) | ### Data Format The data format is mostly the same, but some property names have changed. For example, `folder` is now `type: "folder"`. | Data | Fancytree | Wunderbaum | | --------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Reference | — | [Initialize](https://mar10.github.io/wunderbaum/api/interfaces/types.WbNodeData.html) | | | — | [Formats](https://mar10.github.io/wunderbaum/api/interfaces/types.WbNodeData.html) | | Tutorial | [Tutorial](https://github.com/mar10/fancytree/wiki/TutorialLoadData) | [Tutorial](https://mar10.github.io/wunderbaum/tutorial/tutorial_initialize/) | ### API Calls The API is similar, but some method names and signatures have changed. | API | Fancytree | Wunderbaum | | --------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Reference | [Tree API](https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html) | [Tree API](https://mar10.github.io/wunderbaum/api/classes/wunderbaum.Wunderbaum.html) | | | [Node API](https://wwwendt.de/tech/fancytree/doc/jsdoc/FancytreeNode.html) | [NodeAPI](https://mar10.github.io/wunderbaum/api/classes/wb_node.WunderbaumNode.html) | | Tutorial | [Tutorial](https://github.com/mar10/fancytree/wiki) | [Tutorial](https://mar10.github.io/wunderbaum/) | ### Event Handlers The signature of event handlers has changed from `function(event, data)` to `function(event)`. | Events | Fancytree | Wunderbaum | | --------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Reference | [Handler API](https://wwwendt.de/tech/fancytree/doc/jsdoc/global.html#FancytreeEvents) | [Handler API](https://mar10.github.io/wunderbaum/api/interfaces/wb_options.WunderbaumOptions.html) | | | [Event model](https://wwwendt.de/tech/fancytree/doc/jsdoc/global.html#EventData) | [Event model](https://mar10.github.io/wunderbaum/api/interfaces/types.WbTreeEventType.html) | | Tutorial | [Tutorial](https://github.com/mar10/fancytree/wiki/TutorialEvents) | [Tutorial](https://mar10.github.io/wunderbaum/tutorial/tutorial_events/) | ================================================ FILE: docs/tutorial/quick_start.md ================================================ # Quick Start !!! note Wunderbaum has beta status:
    API, Markup, Stylesheet, etc. are still subject to change. A Wunderbaum control is added to a web page by defining a `
    ` tag and then create a new _Wunderbaum_ class instance, passing the tag and configuration options. ```html ...
    ... ``` ESM modules are also supported: ```html ``` !!! info Wunderbaum is a refactored version of [Fancytree](https://github.com/mar10/fancytree). Read [migrate](migrate.md) for details and migration hints. !!! info "See also" See also the [API Documentation](https://mar10.github.io/wunderbaum/api/) and the [live demo](https://mar10.github.io/wunderbaum/demo/). ================================================ FILE: docs/tutorial/tutorial_api.md ================================================ # Tree API !!! abstract "TL;DR" Wunderbaum offers an extensive, object oriented API. !!! warning This chapter is still under construction. !!! info "See also" See also the [API Reference](https://mar10.github.io/wunderbaum/api/). This chapter describes different ways to modify the tree model using the API. ## Iteration - [tree.visit()](https://mar10.github.io/wunderbaum/api/classes/wunderbaum.Wunderbaum.html#visit) There are two ways to traverse the tree _depth-first, pre-order_: ```js for (const node of tree) { node.log(); } ``` ```js tree.visit((node) => { node.log(); }); ``` Both are 'fast enough' for most use cases, but the latter is slightly faster. `visit()` also allows to break or skip nodes by returning a special value: ```js tree.visit((node) => { if (node.isSelected()) { return "skip"; // skip selected nodes and their children } if (node.title === "foo") { return false; // stop iteration } }); ``` Iteration is also available for subnodes: ```js for(const node of parentNode) { ... } parentNode.visit((node) => { ... }); ``` **Related Methods** - [tree.visit()](https://mar10.github.io/wunderbaum/api/classes/wunderbaum.Wunderbaum.html#visit) ## Searching See [Search and Filter Nodes](tutorial_filter.md). ## Selection See [Search and Filter Nodes](tutorial_filter.md). ## Sorting See [Search and Filter Nodes](tutorial_filter.md). ## Mutation ### Adding Nodes ### Removing Nodes ### Moving Nodes ### Changing Node Properties ### Changing Node State ### Changing Node Data ### Changing Node Style ### Changing Node Class ### Changing Node Attributes ### Changing Node Icons ### Changing Node Badge ### Changing Node Badge Colors ## Related Methods - `node.moveTo()` - `node.setExpanded()` - `node.setSelected()` - `tree.applyCommand()` ## Utility Methods The [Module util](https://mar10.github.io/wunderbaum/api/modules/util.html) provides a number of utility methods that are useful when working with trees. ## Performance Tips Use `tree.runWithDeferredUpdate()` to avoid multiple updates while changing many nodes at once.
    Synchronous methods can be wrapped in a `runWithDeferredUpdate()` call to avoid multiple redraws: ```js tree.runWithDeferredUpdate(() => { tree.visit((node) => { node.setSelected(true); }); }); ``` Asynchronous methods can be wrapped in a `runWithDeferredUpdateAsync()` like so: ```js await tree.runWithDeferredUpdateAsync(async () => { return await node.someAsyncFuntion(); }); ``` ================================================ FILE: docs/tutorial/tutorial_dnd.md ================================================ # Drag and Drop !!! abstract "TL;DR" Wunderbaum implements drag and drop according to the native HTML protocol. This allows interaction between different tree instances, browsers, and even native applcations. Wunderbaum supports drag and drop of nodes within the tree and between trees. It is also possible to drag nodes from the tree to other elements on the page or vice versa. Even cross-window drag and drop is supported.
    The implementation is purely based on the native [HTML Drag and Drop API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API). Note that there is no automatic modification of nodes. Instead, the `drop` event is fired on the target tree and it is up to the application to modify the tree accordingly. ## Drag and Drop Events The following events are fired on the tree during drag and drop. !!! info Note that the `dragStart`, `drag`, and `dragEnd` events are fired on the tree that contains the dragged node (i.e. the source node).
    The other events are fired on the tree that contains the drop target. The events are named after the corresponding [HTML Drag and Drop events](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent). However, the event handlers are passed an object with the following properties: ```js e = { type: "dnd.EVENTNAME", node: // the source or target node, depending on the event type event: // } ``` These are events are sent in a typical drag and drop operation: - `dragStart(e)`: Fired when a drag operation is started.
    This event handler MUST be implemented by the application in order to enable dragging in general.
    The handler can return `false` to prevent dragging the source node.
    The handler can set the `e.event.dataTransfer.effectAllowed` property in order to adjust copy/move/link behavior.
    The handler can set the `e.event.dataTransfer.dropEffect` property in order to adjust copy/move/link behavior. - `drag(e)`: Fired repeadedly during a drag operation.
    We will hardly ever have to implement this handler. - `dragEnter(e)`: Fired when a dragged item enters a drop target.
    This event handler MUST be implemented by the application in order to enable dropping in general.
    The handler can return `false` to prevent the drop operation or return a set of drop regions to indicate which drop regions are allowed.
    The handler can set the `e.event.dataTransfer.dropEffect` property in order to adjust copy/move/link behavior. - `dragOver(e)`: Fired continuously when a dragged item is moved over a drop target.
    We will hardly ever have to implement this handler.
    The handler can set the `e.event.dataTransfer.dropEffect` property in order to adjust copy/move/link behavior. - `dragLeave(e)`: Fired when a dragged node leaves a drop target.
    We will hardly ever have to implement this handler. - `drop(e)`: Fired when a dragged node is dropped on a drop target.
    This is the most important event handler. It is responsible for modifying the tree according to the drop operation.
    ```js e = { type: "dnd.drop", node: // the target node event: // region: // 'before', 'after', 'over' suggestedDropMode: // 'before', 'after', 'appendChild' // (compatible with node.moveTo() and .appendChild()) suggestedDropEffect: // 'copy', 'move', 'link' sourceNode: // the source node if available sourceNodeData: // the serialized data of the source node if any } ``` Foreign source data can be retreived from the `e.event.dataTransfer` object. - `dragEnd(e)`: Fired when a drag operation is ended.
    We will hardly ever have to implement this handler. ## Related Tree Options !!! info "See also" See also the [API Documentation for DnD options](https://mar10.github.io/wunderbaum/api/types/types.DndOptionsType.html) and the [live demo](https://mar10.github.io/wunderbaum/demo/#demo-plain). ## Examples ### Basic Drag and Drop Allow sorting of plain nodes: ```js const tree = new Wunderbaum({ // --- Common Options --- ... dnd: { dragStart: (e) => { if (e.node.type === "folder") { return false; // do not allow dragging folders } return true; }, dragEnter: (e) => { if (e.node.type === "folder") { return "over"; } return ["before", "after"]; }, drop: (e) => { console.log( `Drop ${e.sourceNode} => ${e.suggestedDropEffect} ${e.suggestedDropMode} ${e.node}`, e ); e.sourceNode.moveTo(e.node, e.suggestedDropMode) }, }, }); ``` ### Basic Drag and Drop (move) ```js const tree = new Wunderbaum({ // --- Common Options --- ... dnd: { effectAllowed: "all", dropEffectDefault: "move", guessDropEffect: true, dragStart: (e) => { // if (e.node.type === "folder") { // return false; // } return true; }, dragEnter: (e) => { // console.log(`DragEnter ${e.event.dataTransfer.dropEffect} ${e.node}`, e); // We can only drop 'over' a folder, so the source node becomes a child. // We can drop 'before' or 'after' a non-folder, so the source node becomes a sibling. if (e.node.type === "folder") { // e.event.dataTransfer.dropEffect = "link"; return "over"; } return ["before", "after"]; }, drag: (e) => { // e.tree.log(e.type, e); }, drop: (e) => { console.log( `Drop ${e.sourceNode} => ${e.suggestedDropEffect} ${e.suggestedDropMode} ${e.node}`, e ); switch (e.suggestedDropEffect) { case "copy": e.node.addNode( { title: `Copy of ${e.sourceNodeData.title}` }, e.suggestedDropMode ); break; case "link": e.node.addNode( { title: `Link to ${e.sourceNodeData.title}` }, e.suggestedDropMode ); break; default: e.sourceNode.moveTo(e.node, e.suggestedDropMode); } }, }, ``` ### Related Methods - `util.foo()` ### Related CSS Rules ```css div.wb-row.wb-drag-source { /* The dragged node */ } div.wb-row.wb-drop-target { /* The current target node while dragging */ } div.wb-row.wb-drop-target.wb-drop-before .wb-node .wb-icon::after { /* Drop marker */ } ``` ### Code Hacks ```js ``` ================================================ FILE: docs/tutorial/tutorial_edit.md ================================================ # Edit Nodes and Columns !!! abstract "TL;DR" Wunderbaum implements renaming of nodes and editing grid cells. Editing is supported in two different ways: 1. There is direct support for renaming nodes, i.e. editing the node title. Use the `edit.trigger` option and implement the `edit.apply()` callback to enable this. 2. In a treegrid, there is also general support for embedded input elements in column cells, like checkboxes, text fields, etc.
    Note that _Wunderbaum_ does **not** implement fancy input controls though. Rather think of it as a framework that makes it easy to use standard or custom HTML controls:
    Create HTML controls in the `tree.render()` callback and implement the `tree.change()` event to enable this. ## 1. Rename Nodes Here is an example implementation of the edit title feature (i.e. rename): ```js const tree = new Wunderbaum({ // --- Common Options --- ... // --- Common Events --- ... // --- Special Options and Events --- edit: { // --- Options --- trigger: ["clickActive", "F2", "macEnter", ...], select: true, // Select all text on start slowClickDelay: 1000, trim: true, // Trim input before applying validity: true, // Check validation rules while typing ... // --- Events --- /** * Called when an editing request was detected, e.g. `F2` key, etc. * * Return `false` to prevent editing. Optionally an HTML string may be * returned that defines the temporary input element. * Any other return value - including undefined - defaults to * `''` */ beforeEdit: (e) => { }, /** * Called after the temporary input control was created, initialized * with the current node title, focused, and selected. */ edit: (e) => { const inputElem = e.inputElem; }, /** * Called when the edit operation is ending, either because the user * canceled, confirmed, or moved focus. * * Return `false` to keep the input control open (not always possible). * * We can also return a `Promise` (e.g. from an Ajax request). * In this case, the cell is marked 'busy' while the request is pending. * * Implementing this event is optional. By default, `node.setTitle()` is * called with the new text. */ apply: (e) => { const node = e.node; const oldValue = e.oldValue; const newValue = e.newValue; const inputElem = e.inputElem; // For example: // call an async storage function and handle validation. // return storeMyStuff(node.refKey, newValue).then(() => { if( ...) { inputElem.setCustomValidity(`Invalid for *reasons*: ${newValue`}) return false; } }; }, }, }); ``` Input validation can be implemented by using the `inputElem.setCustomValidity()` method as in the example above, or by raising a [util.ValidationError](https://mar10.github.io/wunderbaum/api/classes/ValidationError.html): ```js const tree = new Wunderbaum({ ... edit: { ... apply: (e) => { const util = e.util; const node = e.node; const newValue = e.newValue; return storeMyStuff(node.refKey, newValue).then(() => { if( ...) { throw new util.ValidationError(`Invalid for *reasons*: ${newValue`}); } }; }, }, }); ``` !!! info "See also" See also a [live demo](https://mar10.github.io/wunderbaum/demo/#demo-plain), activate a node, and hit F2. See also [EditOptionsType](https://mar10.github.io/wunderbaum/api/types/types.EditOptionsType.html). ### Related Methods - `node.setActive(true, {colIdx: 0, edit: true})` - `node.startEditTitle()` - `tree.isEditingTitle()` - `tree.startEditTitle(node)` - `tree.stopEditTitle(apply: boolean)` - `util.setValueToElem()` ### Style Hacks ```css input.wb-input-edit {} span.wb-col.wb-busy {} span.wb-col.wb-error {} span.wb-col.wb-invalid {} span.wb-col input:invalid {} wb-tristate ``` ### Code Hacks ```js Todo; ``` ## 2. Edit Cell Content !!! info "See also" See the [Grid Tutorial](tutorial_grid.md?id=editing) for general information about rendering grid cell content. See also a [live demo](https://mar10.github.io/wunderbaum/demo/#demo-editable), expand some nodes and enter values into the input controls. Editing cells — other than the node title column — is not supported by default. Instead we have to 1. Implement the `render(e)` callback to render the cell's content as an HTML element that can be edited, like a text field, checkbox, etc. 2. Implement the `change(e)` callback to update the node data when the user has finished editing a cell. ### 2.1. Render Input Elements Following an example implementation of the `render(e)` callback that renders embedded input controls for all data columns. The [util.setValueToElem()](https://mar10.github.io/wunderbaum/api/functions/util.setValueToElem.html) helper function can be used to update the embedded input element with the current node value. The `e.isNew` property is `true` if the node is new and has not been rendered before. This is useful to avoid overwriting user input when the user is editing a cell. We follow the convention to name the column id after the node data property that should be rendered in that column for simplicity. ```js const tree = new Wunderbaum({ ... columns: [ { title: "Title", id: "*", width: "250px" }, { title: "Age", id: "age", width: "50px", classes: "wb-helper-end" }, { title: "Date", id: "date", width: "100px", classes: "wb-helper-end" }, { title: "Status", id: "state", width: "70px", classes: "wb-helper-center" }, { title: "Avail.", id: "avail", width: "70px", classes: "wb-helper-center" }, { title: "Remarks", id: "remarks", width: "*" }, ], render: function (e) { const node = e.node; const util = e.util; // Render embedded input controls for all data columns for (const col of Object.values(e.renderColInfosById)) { const val = node.data[col.id]; switch (col.id) { case "author": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "remarks": if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "age": // numeric input (positive integers only) if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "state": // select box if (e.isNew) { col.elem.innerHTML = ``; } util.setValueToElem(col.elem, val); break; case "avail": // checkbox if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; case "date": // date picker if (e.isNew) { col.elem.innerHTML = ''; } util.setValueToElem(col.elem, val); break; default: // Render all other node data cells as text (read-only) // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } }, }); ``` #### Simplify the Pattern The pattern above can be simplified by defining the `html` property in the column definition, so the column cells are rendered by default and we can skip the `if (e.isNew) {...}` handling.
    Note that we still have to update the embedded input elements with the current node value. ```js const tree = new Wunderbaum({ ... columns: [ { title: "Title", id: "*", width: "250px" }, { title: "Age", id: "age", width: "50px", classes: "wb-helper-end", "html": "", }, { title: "Date", id: "date", width: "100px", classes: "wb-helper-end", "html": '', }, { title: "Status", id: "state", width: "70px", classes: "wb-helper-center", "html": `` }, { title: "Avail.", id: "avail", width: "70px", classes: "wb-helper-center", "html": '', }, { title: "Remarks", id: "remarks", width: "*", "html": "", }, ], render: function (e) { const node = e.node; const util = e.util; // Render embedded input controls for all data columns for (const col of Object.values(e.renderColInfosById)) { const val = node.data[col.id]; switch (col.id) { default: util.setValueToElem(col.elem, val); break; } } }, }); ``` ### 2.2. Validate and Apply Modified Cell Data The `change(e)` callback is called when the user has finished editing a cell. More precisely, it is called when the embedded _input_, _select_, or _textarea_ element fired a _change_ event.
    It receives a [WbChangeEventType](https://mar10.github.io/wunderbaum/api/interfaces/types.WbChangeEventType.html) object that contains useful properties for this purpose. The [util.getValueFromElem()](https://mar10.github.io/wunderbaum/api/functions/util.getValueFromElem.html) helper function can be used to read the current value from the embedded input element. This value is also avalable as `e.inputValue`, however without coercing date inputs to _Date_ instances. > Again, we follow the convention to name the column id after the node data > property that appears in that column. ```js const tree = new Wunderbaum({ ... change: function (e) { const util = e.util; const node = e.node; const colId = e.info.colId; this.logDebug(`change(${colId})`, util.getValueFromElem(e.inputElem, true)); // Assumption: we named column.id === node.data.NAME node.data[colId] = util.getValueFromElem(e.inputElem, true); }, }); ``` If we want to customize the `change(e)` callback, we can do so like this: ```js const tree = new Wunderbaum({ ... change: function (e) { const util = e.util; const node = e.node; const colId = e.info.colId; let val; switch (colId) { case "year": val = util.getValueFromElem(e.inputElem, true); if(val && new Date(val) > new Date()) { throw new util.ValidationError("Invalid year (must not be in the past)"); } if (val && !/^\d{4}$/.test(val)) { throw new util.ValidationError("Invalid year (yyyy)"); } e.node.data[colId] = val; break; default: e.node.data[colId] = util.getValueFromElem(e.inputElem, true); break; } }, }); ``` ### Related Methods - `node.setActive(true, {colIdx: 2, edit: true})` - `util.getValueFromElem()` - `util.setValueToElem()` - `util.toggleCheckbox()` ### Related CSS Rules ```css span.wb-col.wb-busy { } span.wb-col.wb-error { } span.wb-col input:invalid { } ``` ================================================ FILE: docs/tutorial/tutorial_events.md ================================================ # Event Handling !!! abstract "TL;DR" The interactive behavior of Wunderbaum is controlled by a set of event handlers. Event handlers are callback functions that are passes as options of the tree object, and are called whenever a certain event occurs: ```js const tree = new mar10.Wunderbaum({ id: "demo", element: document.getElementById("demo-tree"), source: "get/root/nodes", ... init: (e) => { e.tree.setFocus(); }, lazyLoad: function (e) { return { url: 'get/child/nodes', params: { parentKey: e.node.key } }; }, activate: function (e) { alert(`Thank you for activating ${e.node}.`); }, ... }); ``` ## The Event Object Depending on the event type, the event handler functions can return a value, that is used by the tree to control the default behavior. For example, the `beforeActivate` event handler can return `false` to prevent activation of a node. Some events are sent by the tree, others by a distinct node. A node event always passes a reference to the node object. A tree event does not always pass a node reference. The event handler functions are called with a single argument, of type [WbTreeEventType](https://mar10.github.io/wunderbaum/api/interfaces/types.WbTreeEventType.html). The event object contains the following properties: ```js e = { type: string, // the event type tree: Wunderbaum, // the tree object util: object, // some useful utility functions node: WunderbaumNode, // the node object (if applicable) event: Event, // the original DOM event (if applicable) flag: boolean, // a flag (if applicable) error: string, // an error (if applicable) ... // additional properties (if applicable) } ``` !!! info "See also" See also the overview of available functions of the [utility module](https://mar10.github.io/wunderbaum/api/modules/util.html). ## Event Handlers !!! info A list of all available events can also be found in the [API Reference](https://mar10.github.io/wunderbaum/api/interfaces/wb_options.WunderbaumOptions.html). Common event handlers include:
    activate(WbActivateEventType) - node event
    `e.node` was activated.
    beforeActivate(WbActivateEventType) - node event
    Return `false` to prevent activation of `e.node`.
    beforeExpand(WbExpandEventType) - node event
    Return `false` to prevent expansion of `e.node`.
    beforeSelect(WbSelectEventType) - node event
    Return `false` to prevent (de)selection.
    buttonClick(WbButtonClickEventType) - tree event
    A column header button was clicked, e.g. sort, filter, or menu. Check `e.command` and `e.info.colId`, ... for details.
    Note that the actual implementation of the command must be explicitly provided.
    See also Search and Filter for examples.
    change(WbChangeEventType) - node event
    The `change(e)` callback is called when the user has finished editing a cell. More precisely, it is called when the embedded input, select, or textarea element fired a change event.
    click(WbClickEventType) - node event
    `e.node` was clicked.
    Return `false` to prevent default behavior, e.g. expand/collapse, (de)selection, or activation.
    dblclick(WbClickEventType) - node event
    `e.node` was double-clicked.
    Return `false` to prevent default behavior, e.g. expand/collapse.
    deactivate(WbDeactivateEventType) - node event
    `e.node` was deactivated.
    discard(WbNodeEventType) - node event
    `e.node` was discarded from the viewport and its HTML markup removed.
    edit.apply(WbEditApplyEventType) - node event
    `e.node` title was changed.
    edit.beforeEdit(WbNodeEventType) - node event
    `e.node` title is about to renamend. Return `false` to prevent renaming.
    edit.edit(WbEditEditEventType) - node event
    `e.node` just switched to edit mode, an input element was created and populated with the current title.
    error(WbErrorEventType) - node event
    An error occurred, e.g. during initialization or lazy loading.
    expand(WbExpandEventType) - node event
    `e.node` was expanded (`e.flag === true`) or collapsed (`e.flag === false`)
    focus(WbFocusEventType) - tree event
    The tree received or lost focus. Check `e.flag`.
    iconBadge(WbIconBadgeEventType) - node event
    `e.node` is about to be rendered. We can add a badge to the icon cell here.
    Returns WbIconBadgeEventResultType.
    init(WbInitEventType) - tree event
    Fires when the tree markup was created and the initial source data was loaded. Typical use cases would be activating a node, setting focus, enabling other controls on the page, etc. Also sent if an error occured during initialization (check for `e.error` property).
    keydown(WbKeydownEventType) - tree event
    Fires when a key was pressed while the tree has focus.
    `e.node` is set if a node is currently active.
    Return `false` to prevent default navigation.
    lazyLoad(WbNodeEventType) - node event
    Fires when a node that was marked 'lazy', is expanded for the first time. Typically we return an endpoint URL or the Promise of a fetch request that provides a (potentially nested) list of child nodes.
    load(WbNodeEventType) - node event
    Fires when data was loaded (initial request, reload, or lazy loading), after the data is applied and rendered.
    modifyChild(WbModifyChildEventType) - node event
    TODO
    receive(WbReceiveEventType) - node event
    Fires when data was fetched (initial request, reload, or lazy loading), but before the data is uncompressed, applied, and rendered. Here we can modify and adjust the received data, for example to convert an external response to native Wunderbaum syntax.
    render(WbRenderEventType) - node event
    Fires when a node is about to be displayed. The default HTML markup is already created, but not yet added to the DOM. Now we can tweak the markup, create HTML elements in this node's column cells, etc.
    See also `Custom Rendering` for details.
    renderStatusNode(WbRenderEventType) - node event
    Same as `render(e)`, but for the status nodes, i.e. `e.node.statusNodeType`.
    select(WbSelectEventType) - node event
    `e.node` was selected (`e.flag === true`) or deselected (`e.flag === false`)
    update(WbRenderEventType) - tree event
    Fires when the viewport was updated, after scroling, expanding etc.
    ## Register Custom events To register a custom event, we can use event delegation. For example, to handle a `contextmenu` event on a row, we can add an event listener to the `body` element. This would allow to prevent the default context menu, or to show a custom context menu.
    The `getNode()` utility method can be used to retrieve the node object that corresponds to the clicked row: ```html ``` ================================================ FILE: docs/tutorial/tutorial_filter.md ================================================ # Search and Filter Nodes !!! abstract "TL;DR" Wunderbaum supports different ways to search and filter nodes. This page describes how searching and filtering can be enabled: 1. **Search** allows to find nodes by title patterns or arbitrary conditions. 2. **Filter** is a more powerful feature that can hide or dim nodes that do not match a given search pattern or condition. It can also highlight matching title parts. ## Searching ### Quicksearch Quicksearch is triggered by typing a character and jumps to the next node that starts with that character.
    It can be disabled by setting `quicksearch: false` in the tree options. ### Using the API Many methods are available to search for nodes. For example, to find a node by its title, use `tree.findFirst()`, `tree.findAll()` and others: ```js // Match all node titles that match exactly 'Joe': nodeList = node.findAll("Joe"); // Match all node titles that start with 'Joe' case sensitive: nodeList = node.findAll(/^Joe/); // Match all node titles that contain 'oe', case insensitive: nodeList = node.findAll(/oe/i); // Match all nodes with `data.price` >= 99: nodeList = node.findAll((n) => { return n.data.price >= 99; }); ``` !!! info "See also" See also the [API tutorial](tutorial_api.md) for more details. ## Filtering A filter can be used to hide or dim nodes that do not match a given search pattern. First, define the filter options in the tree options: ```js const tree = new Wunderbaum({ ... filter: { autoApply: true, // Re-apply last filter if lazy data is loaded mode: "hide", ... }, ... }); ``` Following options are available (see also [FilterOptionsType](https://mar10.github.io/wunderbaum/api/types/types.FilterOptionsType.html)): ```js const tree = new Wunderbaum({ ... filter: { autoApply: true, // Re-apply last filter if lazy data is loaded autoExpand: false, // Expand all branches that contain matches while filtered matchBranch: false, // Whether to implicitly match all children of matched nodes connect: null, // Element or selector of an input control for filter query strings fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' hideExpanders: false, // Hide expanders if all child nodes are hidden by filter highlight: true, // Highlight matches by wrapping inside tags leavesOnly: false, // Match end nodes only mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) noData: true, // Display a 'no data' status node if result is empty }, ... }); ``` ### Filter Nodes The `filterNodes()` method can be used to apply a filter to the tree. It accepts a string, a regular expression, or a function as a filter pattern: ```js // Strings are matched against the node titles (contains, case insensitive) tree.filterNodes("Joe"); // Regular expressions are matched against the node titles // E.g. fin titles that start with 'joe' or 'joh' (case insensitive) tree.filterNodes(/^jo[eh]/i); // Functions are called with the node as an argument and can test for any // condition tree.filterNodes((node) => { return node.data.price >= 99; }); ``` Additional options can be passed as a second argument to override the default `tree.filter` settings: ```js tree.filterNodes("Joe", { mode: "hide" }); ``` See [FilterNodesOptions](https://mar10.github.io/wunderbaum/api/types/types.FilterNodesOptions.html)): Examples ```js // Match all nodes with a title that does contain 'Joe' (case insensitive) and // dim the rest: tree.filterNodes("Joe"); // Match all nodes with a title that does contain 'Joe' (case insensitive) and // hide the rest: tree.filterNodes("Joe", { mode: "hide" }); // Match all nodes with a custom property 'age' > 30: tree.filterNodes((node) => { return node.data.age <= 30; }); // Match all nodes with a a title that contains 'foo' or 'fox': const re = /.*fo[ox].*/i; tree.filterNodes((node) => { return re.test(node.title); }); ``` !!! info A filter callback may return a boolean value, or the string values 'skip' or 'branch'. The latter will skip or match the node and all its descendants.
    Note that highlighting matches is not supported for function filters. ### Display Count of Matches as Badges Show a badge with number of matching child nodes near parent icons. If no matchin children exist or the node is expanded, the badge is hidden. ```js const tree = new Wunderbaum({ ... iconBadge: (e) => { const node = e.node; if (node.children?.length > 0 && !node.expanded && node.subMatchCount > 0) { return { badge: node.subMatchCount, badgeTooltip: `${node.subMatchCount} matches`, badgeClass: "match-count", }; } }, ... }); ``` ### Connect to Search Input Define some html elements as filter controls: ```html ``` and connect them to the `tree.filterNodes()` method: ```js const queryInput = document.querySelector("input#filter-query"); queryInput.addEventListener( "input", Wunderbaum.util.debounce((e) => { tree.filterNodes(queryInput.value.trim(), {}); }, 700) ); ... // For example: dynamically toggle hide/dim mode tree.setOption("filter.mode", hideMode ? "hide" : "dim"); ``` An even simpler way is to use the `options.filter.connect` option, like [in the demo](https://mar10.github.io/wunderbaum/demo/#demo-plain). ```js const tree = new Wunderbaum({ ... filter: { mode: "hide", autoExpand: true, connect: { inputElem: "#filter-query", modeButton: "#filter-hide", nextButton: "#filter-next", prevButton: "#filter-prev", matchInfoElem: "#filter-match-info", } }, }); ``` !!! info "See also" See also a [live demo](https://mar10.github.io/wunderbaum/demo/#demo-plain) and enter some text in the _Filter_ control at the top. ### Add a Filter Button to the Column Header Add a filter button to the column header to toggle the filter mode: ```js const tree = new Wunderbaum({ ... columns: [ { title: "Title", filterable: true, }, ... ], buttonClick: (e) => { tree.log(e.type, e); if (e.command === "filter") { // ... ... // Update the button state e.info.colDef.filterActive = !e.info.colDef.filterActive; tree.update("colStructure"); } }, ... }); ``` ### Related Methods - `tree.clearFilter()` - `tree.countMatches()` - `tree.filterNodes()` - `tree.findAll()` - `tree.findFirst()` - `tree.iconBadge()` - `tree.isFilterActive()` - `tree.updateFilter()` ### Related CSS Rules ```scss &.wb-ext-filter-dim, &.wb-ext-filter-hide { div.wb-node-list div.wb-row { color: $filter-dim-color; &.wb-submatch { color: $filter-submatch-color; } &.wb-match { color: $node-text-color; } } } ``` ================================================ FILE: docs/tutorial/tutorial_grid.md ================================================ # Grid !!! abstract "TL;DR" Wunderbaum implements native support for treegrids (in fact, a plain tree is only a special case).
    Rendering and editing of grid cells requires custom event handlers. !!! info "See also" See also a [live demo](https://mar10.github.io/wunderbaum/demo/#demo-editable). Wunderbaum works as a treegrid out of the box if we specify column definitions.
    In a treegrid, there is also general support for embedded input elements in column cells, like checkboxes, text fields, etc. Note that the treegrid is not editable by default however. It does not even render cell content for columns other than the main (first) node column. This has to be implemented in the `render(e)` callback instead.
    Wunderbaum does _not_ implement fancy input controls. Rather think of it as a framework that makes it easy to use standard or custom HTML controls:
    Create HTML controls in the `render(e)` callback and implement the `change(e)` event to enable editing. ## Column Definitions !!! info Column definitions are required to turn a plain Wunderbaum tree into a treegrid. A list of column definitions is specified in the `columns` option. `title` and `id` are required. `width` is optional, but recommended. The `id` is used to identify the column in the `render` event.
    The special id `"*"` is used for the main node column with checkbox, connectors, icon, and title). It is required and must be the first column in the list. The `width` is either specified in absolute pixels (`"100px"`) or relative weights (`"2.5"`). Column widths default to `"*"`, which is equivalent to `"1.0"`
    Absolute widths are applied first, then the remaining space is distributed among the relative weights. The `classes` property can be used to add CSS classes to the column header and cells. The `html` property can be used to define cell markup that is rendered by default. ```js const tree = new Wunderbaum({ ... columns: [ { id: "*", title: "Product", width: "250px" }, { id: "author", title: "Author", width: "200px" }, { id: "year", title: "Year", width: "50px", classes: "wb-helper-end" }, { id: "qty", title: "Qty", width: "50px", classes: "wb-helper-end" }, { id: "price", title: "Price ($)", width: "80px", classes: "wb-helper-end", // (1) }, { id: "details", title: "Details", width: "*" }, ], ... }); ``` 1. This classes are added to all header and row cells of that column. In this case: right align the content of the column. !!! info See also [ColumnDefinition](https://mar10.github.io/wunderbaum/api/interfaces/types.ColumnDefinition.html) for details. ## Rendering Wunderbaum renders the first column (the main node column) by default. To render additional columns, implement the `render(e)` callback. The render event receives a [WbRenderEventType](https://mar10.github.io/wunderbaum/api/interfaces/types.WbRenderEventType.html) object that contains useful properties for this purpose. We can use the `e.renderColInfosById` property to iterate over all columns and render the content of each column.
    This can be simplified by following the convention to name the column id after the node data property that should be rendered in that column. ```js const tree = new Wunderbaum({ ... types: {}, columns: [ { id: "*", title: "Product", width: "250px" }, { id: "author", title: "Author", width: "200px" }, { id: "year", title: "Year", width: "50px", classes: "wb-helper-end" }, { id: "qty", title: "Qty", width: "50px", classes: "wb-helper-end" }, { id: "price", title: "Price ($)", width: "80px", classes: "wb-helper-end", }, { id: "details", title: "Details", width: "*" }, ], ... render: function (e) { const node = e.node; for (const col of Object.values(e.renderColInfosById)) { switch (col.id) { default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = node.data[col.id]; break; } } }, }); ``` If we want to render formatted values, we can do this explicitly for each column: ```js const tree = new Wunderbaum({ ... render: function (e) { const node = e.node; for (const col of Object.values(e.renderColInfosById)) { const val = node.data[col.id]; switch (col.id) { case "date": if (val) { const dt = new Date(val); col.elem.textContent = dt.toISOString().slice(0, 10); } else { col.elem.textContent = "n.a."; } break; case "state": { const map = { h: "Happy", s: "Sad" }; col.elem.textContent = map[val] || "n.a."; } break; case "avail": col.elem.textContent = val ? "Yes" : "No"; break; default: // Assumption: we named column.id === node.data.NAME col.elem.textContent = val; break; } } }, }); ``` !!! info See the [Edit Tutorial](tutorial_edit.md) for examples how to render embedded controls. ## Editing Editing cells — other than the node title column — is not supported by default. Instead we have to 1. Implement the `render(e)` callback to render the cell's content as an HTML element that can be edited, like a text field, checkbox, etc. 2. Implement the `change(e)` callback to update the node data when the user has finished editing a cell. !!! info See the [Edit Tutorial](tutorial_edit.md) for details. ## Navigation A treegrid can have one of two navigation modes. We can toggle using the keyboard: Row Mode ↔ Cell-Nav Mode !!! info See the [Keyboard Tutorial](tutorial_keyboard.md) for details. ## Configuration and Customization !!! note Todo. ### Related Tree Options ```js const tree = new Wunderbaum({ ... navigationModeOption: "startRow", // | "cell" | "startCell" | "row" columns: [], ... // --- Events --- render: (e) => { // Return false to prevent default behavior } ... edit: { trigger: ["F2", "macEnter", ...], ... }, }); ``` ### Related Methods - `tree.setNavigationOption(mode: NavModeEnum)` - `tree.setColumn(colIdx: number|string, options?: SetColumnOptions)` ### Related CSS Rules ```css ``` ### Code Hacks #### Redefine Columns For example to append a new column: ```js tree.columns.push({ title: "New Col", id: "col_" + sequence++, width: "100px", }); tree.update("colStructure"); ``` #### Add a Menu Button to the Column Header Add a filter button to the column header to toggle the filter mode: ```js const tree = new Wunderbaum({ ... columns: [ { title: "Title", menu: true, ... }, ... ], buttonClick: (e) => { if (e.command === "menu") { alert("Open menu..."); } }, ... }); ``` #### Add a Sort Button to the Column Header Add a sort button to the column header and handle click events to toggle the order: ```js const tree = new Wunderbaum({ ... columns: [ { title: "Title", sortable: true, // or set in column definition ... }, ... ], columnsSortable: true, // or set in column definition buttonClick: (e) => { if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } }, ... }); ``` ================================================ FILE: docs/tutorial/tutorial_initialize.md ================================================ # Loading and Initialization !!! abstract "TL;DR" Wunderbaum is included on a page and initialized by instantiating a Wunderbaum object with custom options. ## Preparation We need to include the library, stylesheets, and fonts as a precondition.
    See [Quick Start](quick_start.md) for details. ## Passing Options There are many more options and callbacks available. Here are some of the frequently used ones: ```js document.addEventListener("DOMContentLoaded", (event) => { const tree = new mar10.Wunderbaum({ element: document.getElementById("demo-tree"), id: "demo", types: {}, columns: [{ id: "*", title: "Product", width: "250px" }], source: "get/root/nodes", // Event handlers: init: (e) => { // e.tree.setFocus(); e.tree.findFirst("Foo")?.setActive(true, { colIdx: "*", edit: true, focusTree: true, }); }, receive: function (e) {}, load: function (e) {}, lazyLoad: function (e) { return { url: 'get/child/nodes', params: { parentKey: e.node.key } }; }, activate: function (e) {}, render: function (e) {}, ... }); }); ``` Common options include:
    element
    Selector or HTML Element of the target div tag.
    id
    The identifier of this tree. Used to reference the instance, especially when multiple trees are present (e.g. `tree = mar10.Wunderbaum.getTree("demo")`).
    source
    Define the initial tree data. Typically a URL of an endpoint that serves a JSON formatted structure, but also a callback, Promise, or static data is allowed.
    types
    Define shared attributes for multiple nodes of the same `node.type`. This allows for more compact data models. Type definitions can be passed as tree option, or be part of a `source` response.
    columns
    A list of maps that define column headers. If this option is set, Wunderbaum becomes a treegrid control instead of a plain tree. Column definitions can be passed as tree option, or be part of a `source` response.
    !!! info See [WunderbaumOptions](https://mar10.github.io/wunderbaum/api/interfaces/wb_options.WunderbaumOptions.html) for a complete list of options. ### Dynamic Options Some node options can be defined in a flexible way, using a dynamic pattern. Consider for example the `checkbox` option, which may be true, false, or "radio". If omitted, it will default to false. Globally enabling checkboxes for all nodes can be configured like so: ```js const tree = new mar10.Wunderbaum({ ... checkbox: true, ... ``` This global setting may be overridden per node by the concrete source data, if a property of the same name is present: ```js [ { title: "Node 1" }, { title: "Node 2", checkbox: false }, { title: "Node 3", checkbox: "radio" }, ]; ``` If the global setting is a callback, it will be called for every node, thus allowing to dynamically define option values: ```js const tree = new mar10.Wunderbaum({ checkbox: (e) => { // Hide checkboxes for folders return e.node.type === "folder" ? false : true; }, tooltip: (e) => { // Create dynamic tooltips return `${e.node.title} (${e.node.key})`; }, icon: (e) => { // Create custom icons if( e.node.data.critical ) { return "foo-icon-class"; } // Exit without returning a value: continue with default processing. }, ... ``` Currently the following options are evaluated as dynamic options: `checkbox`, `icon`, `iconTooltip`, `tooltip`, `unselectable`. See method `node.getOption()` for details. ## Event Handlers Event handlers can be used to control tree behavior and react on status changes. Common event handlers include: `init(e)`, `lazyLoad(e)`, `receive(e)`, `render(e)`, and more. !!! info Event Handlers are described in detail in the [Events chapter](tutorial_events.md). ## Data Source !!! info See examples of JSON data and learn more in the next section, [Data Formats](tutorial_source.md) Typically we load the tree nodes in a separate Ajax request like so: ```js const tree = new mar10.Wunderbaum({ ... source: "path/to/request", ... }); ``` The example above issues a simple GET request. For more controle, we can use the extended syntax: ```js const tree = new mar10.Wunderbaum({ ... source: { url: "path/to/request", params: {}, // key/value pairs converted to URL parameters body: {}, // key/value pairs converted to JSON body (defaults to method POST) options: {}, // passed to `fetch(url, OPTIONS)` } ... }); ``` The endpoint must return a node structure in JSON format. Note that - The structure may be nested, e.g. a child node may in turn contain a `children` list. - Some reserved attributes names are part of the node data model:
    `checkbox`, `classes`, `expanded`, `icon`, `iconTooltip`, `key`, `lazy`, `radiogroup`, `refKey`, `selected`, `statusNodeType`, `title`, `tooltip`, `type`, `unselectable`.
    They can be accessed as `node.title`, for example, - All other properties are stored under the _data_ namespace and are accessed like `node.data.author`, for example. - Only `title` is mandatory - Node titles are escaped in order to prevent [XSS](https://owasp.org/www-community/attacks/xss/). For example if JSON source contains `"title": "
    ================================================ FILE: docs/unittest/test-dist.html ================================================ Test Suite (DIST) | Wunderbaum
    ================================================ FILE: eslint.config.mjs ================================================ import typescriptEslint from "@typescript-eslint/eslint-plugin"; import tsParser from "@typescript-eslint/parser"; import path from "node:path"; import { fileURLToPath } from "node:url"; import js from "@eslint/js"; import { FlatCompat } from "@eslint/eslintrc"; import globals from "globals"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all, }); export default [ ...compat.extends( "eslint:recommended", "plugin:@typescript-eslint/recommended" ), { plugins: { "@typescript-eslint": typescriptEslint, }, languageOptions: { parser: tsParser, globals: { ...globals.browser, ...globals.nodeBuiltin, }, }, rules: { curly: ["error", "all"], "no-alert": "error", "no-console": "error", "prefer-const": [ "error", { destructuring: "all", }, ], "no-constant-condition": [ "error", { checkLoops: false, }, ], "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-expressions": "off", // "@typescript-eslint/no-unnecessary-type-assertion": "error", "one-var": [ "error", { const: "never", }, ], }, }, ]; ================================================ FILE: mkdocs.yml ================================================ site_name: Wunderbaum site_url: https://mar10.github.io/wunderbaum/ repo_url: https://github.com/mar10/wunderbaum # site_description: Set the site description. This will add a meta tag to the generated HTML header. site_author: Martin Wendt copyright: | Copyright © 2021-2025 Martin Wendt, Documentation generated with MkDocs. # remote_branch: gh-pages # remote_name: origin theme: name: material locale: en include_sidebar: true logo: assets/tree_logo_32.png favicon: assets/favicon/favicon.ico features: - content.code.annotate - content.code.copy - header.autohide # - navigation.anchors - navigation.bottom - navigation.breadcrumbs # - navigation.edit_url - navigation.expand - navigation.footer - navigation.indexes # - navigation.meta - navigation.path - navigation.prev_next # - navigation.scrollspy # - navigation.search - navigation.sections - navigation.tabs - navigation.tabs.sticky # - navigation.toc - navigation.top - navigation.tracking - toc.follow # - toc.integrate palette: # Palette toggle for automatic mode - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/brightness-4 name: Switch to system preference exclude_docs: | .* /templates/ /unittest/ nav: - "User Guide": - index.md - "Quick start": "tutorial/quick_start.md" - "Initialization": "tutorial/tutorial_initialize.md" - "Data Formats": "tutorial/tutorial_source.md" - Features: - "Render": "tutorial/tutorial_render.md" - "Styling": "tutorial/tutorial_styling.md" - "Grid": "tutorial/tutorial_grid.md" - "Edit": "tutorial/tutorial_edit.md" - "Drag'n'Drop": "tutorial/tutorial_dnd.md" - "Select": "tutorial/tutorial_select.md" - "Events": "tutorial/tutorial_events.md" - "Filter": "tutorial/tutorial_filter.md" - "Keyboard": "tutorial/tutorial_keyboard.md" - "API": "tutorial/tutorial_api.md" - "Concepts": "tutorial/concepts.md" - "Migration": "tutorial/migrate.md" - "Contribute": "tutorial/contribute.md" - "API Reference": "https://mar10.github.io/wunderbaum/api/index.html" - "Online Demos": "https://mar10.github.io/wunderbaum/demo/" - "Changelog": "https://github.com/mar10/wunderbaum/blob/main/CHANGELOG.md" # not_in_nav: | # /private.md validation: omitted_files: warn absolute_links: warn # Or 'relative_to_docs' unrecognized_links: warn anchors: warn markdown_extensions: - admonition - attr_list - pymdownx.details - pymdownx.superfences - pymdownx.highlight: use_pygments: true # auto_title: true # linenums: true pygments_lang_class: true - pymdownx.magiclink - toc: permalink: "#" # baselevel: 2 # separator: "_" plugins: - search # - sitemap extra: generator: false # Added to Copyright instead analytics: provider: google property: G-W8EGCZFQ00 ================================================ FILE: package.json ================================================ { "name": "wunderbaum", "version": "0.14.2-0", "title": "A treegrid control.", "description": "JavaScript tree/grid/treegrid control.", "homepage": "https://github.com/mar10/wunderbaum", "author": { "name": "Martin Wendt", "url": "https://github.com/mar10" }, "repository": { "type": "git", "url": "https://github.com/mar10/wunderbaum" }, "bugs": { "url": "https://github.com/mar10/wunderbaum/issues" }, "license": "MIT", "licenses": [ { "type": "MIT", "url": "https://raw.githubusercontent.com/mar10/wunderbaum/main/LICENSE.txt" } ], "main": "./dist/wunderbaum.umd.js", "module": "./dist/wunderbaum.esm.js", "types": "./dist/wunderbaum.d.ts", "exports": { ".": { "types": "./dist/wunderbaum.d.ts", "require": "./dist/wunderbaum.umd.js", "import": "./dist/wunderbaum.esm.js" }, "./dist/": "./dist/", "./src/": "./src/" }, "files": [ "src", "dist" ], "keywords": [ "grid", "tree", "treegrid", "typescript", "plugin", "control" ], "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.20.0", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.2", "@types/firebase": "^2.4.32", "@types/jest": "^29.5.14", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "concurrently": "^9.1.2", "eslint": "^9.20.1", "eslint-config-jquery": "^3.0.2", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.3", "grunt": "^1.6.1", "grunt-contrib-connect": "^5.0.1", "grunt-contrib-qunit": "^10.1.1", "grunt-contrib-watch": "^1.1.0", "grunt-exec": "^3.0.0", "grunt-yabs": "^1.3.0", "http-server": "^14.1.1", "nodemon": "^3.1.9", "postcss": "^8.5.2", "postcss-url": "^10.1.3", "prettier": "^3.5.1", "pretty-quick": "^4.0.0", "puppeteer": "^24.2.0", "qunit": "^2.24.1", "rollup": "^4.34.6", "rollup-plugin-scss": "^4.0.1", "sass": "^1.84.0", "terser": "^5.39.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", "typedoc": "^0.27.7", "typescript": "^5.7", "yarn-audit-fix": "^10.1.1" }, "nodemonConfig": { "watch": [ "src/" ], "ext": "ts,scss", "exec": "npm run build:js -s && npm run build:scss", "ignore": [ "node_modules/", ".git", "build/*", "dist/*", "test/*", "docs/*" ], "delay": "2500" }, "scripts": { "test": "npm run lint && npm run build:js && grunt ci --verbose", "api_docs": "typedoc && touch docs/api/.nojekyll && rm docs/unittest/*.*; cp test/unit/*.* docs/unittest", "format": "eslint src docs/demo --fix && prettier src docs/demo -w && npm run lint", "lint": "prettier src docs/demo --check && eslint src docs/demo && tsc -t esnext --moduleResolution node --noEmit src/wunderbaum.ts", "build:minjs:umd": "terser build/wunderbaum.umd.js --compress --mangle --source-map \"base='build',url='wunderbaum.umd.min.js.map',filename='wunderbaum.umd.js'\" --output build/wunderbaum.umd.min.js", "build:minjs:esm": "terser build/wunderbaum.esm.js --compress --mangle --source-map \"base='build',url='wunderbaum.esm.min.js.map',filename='wunderbaum.esm.js'\" --output build/wunderbaum.esm.min.js", "build:minjs": "npm run build:minjs:esm -s && npm run build:minjs:umd -s", "build:scss": "sass src/wunderbaum.scss build/wunderbaum.css", "build:js": "rollup -c rollup.config.mjs && npm run build:minjs", "build:types": "tsc -t esnext --moduleResolution node -d --emitDeclarationOnly --outFile build/wunderbaum.d.ts src/wunderbaum.ts", "build": "npm run format && mkdir build; rm build/*.*; ls build && npm run build:js -s && npm run build:scss && npm run build:types -s && npm run api_docs", "make_dist": "npm run build && rm dist/*.* ; cp build/*.* dist", "watch:umd": "nodemon --watch src --ext 'ts' -x \"npm run build:minjs\"", "watch": "nodemon", "serve": "http-server test -p 8080 -o /", "dev": "concurrently \"http-server . -p 8080 -o /docs/demo \" \"nodemon\"", "dev_mkdocs": "pipenv run mkdocs serve" }, "npmName": "wunderbaum", "npmFileMap": [ { "basePath": "dist", "files": [ "wunderbaum.css", "wunderbaum.esm.js", "wunderbaum.esm.min.js", "wunderbaum.esm.min.js.map", "wunderbaum.umd.js", "wunderbaum.umd.min.js", "wunderbaum.umd.min.js.map" ] } ], "packageManager": "yarn@4.4.1+sha512.f825273d0689cc9ead3259c14998037662f1dcd06912637b21a450e8da7cfeb4b1965bbee73d16927baa1201054126bc385c6f43ff4aa705c8631d26e12460f1" } ================================================ FILE: rollup.config.mjs ================================================ import fs from "fs"; // import postcss from "postcss"; // import postcss_url from "postcss-url"; import rup_replace from "@rollup/plugin-replace"; // import rup_scss from "rollup-plugin-scss"; // import rup_terser from "@rollup/plugin-terser"; import rup_typescript from "@rollup/plugin-typescript"; const package_json = JSON.parse(fs.readFileSync("package.json", "utf8")); export default { input: "src/wunderbaum.ts", output: [ { file: "build/wunderbaum.esm.js", format: "es", }, { file: "build/wunderbaum.umd.js", format: "umd", name: "mar10", }, // TODO: Minify with terser did produce invalid files (only first extension)? // running `terser` as npm script from package.json instead // { // file: "build/wunderbaum.esm.min.js", // format: "es", // plugins: [rup_terser()], // minify // sourcemap: true, // }, // { // file: "build/wunderbaum.umd.min.js", // format: "umd", // name: "mar10", // plugins: [rup_terser()], // minify // sourcemap: true, // }, ], plugins: [ rup_typescript(), // TODO: Minify with terser did produce invalid files (only first extension)? // rup_terser(), // TODO: Could not get this to work. It seems to be ignored. // Using a package.json script instead (build:scss). // However, now we cannot auto-inline images. :-( // rup_scss({ // fileName: "wunderbaum.css", // // Convert image URLs to inline data-uris // processor: () => // postcss().use( // postcss_url({ url: "inline", maxSize: 10, fallback: "copy" }) // ), // }), // Replace @VERSION and @DATE in build files rup_replace({ preventAssignment: true, delimiters: ["", ""], values: { "@VERSION": "v" + package_json.version, "@DATE": "" + new Date().toUTCString(), "const DEFAULT_DEBUGLEVEL = 4;": "const DEFAULT_DEBUGLEVEL = 3;", }, }), // TODO: additional minfied version? // rup_scss({ // fileName: "wunderbaum.min.css", // outputStyle: "compressed", // sourceMap: true, // // Convert image URLs to inline data-uris // processor: () => // postcss().use( // postcss_url({ url: "inline", maxSize: 10, fallback: "copy" }) // ), // }), ], }; ================================================ FILE: src/common.ts ================================================ /*! * Wunderbaum - common * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { ApplyCommandType, NavigationType, SourceListType, SourceObjectType, IconMapType, MatcherCallback, } from "./types"; import * as util from "./util"; import { WunderbaumNode } from "./wb_node"; export const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script /** * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`. */ export const DEFAULT_ROW_HEIGHT = 22; /** * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`. */ export const ICON_WIDTH = 20; /** * Adjust the width of the title span, so overflow ellipsis work. * (2 x `$col-padding-x` + 3px rounding errors). */ export const TITLE_SPAN_PAD_Y = 7; /** Render row markup for N nodes above and below the visible viewport. */ export const RENDER_MAX_PREFETCH = 5; /** Skip rendering new rows when we have at least N nodes rendeed above and below the viewport. */ export const RENDER_MIN_PREFETCH = 5; /** Minimum column width if not set otherwise. */ export const DEFAULT_MIN_COL_WIDTH = 4; /** * A value for `node.type` that by convention may be used to mark a node as directory. * It may be used to sort 'directories' to the top. */ export const NODE_TYPE_FOLDER = "folder"; /** Regular expression to detect if a string describes an image URL (in contrast * to a class name). Strings are considered image urls if they contain '.' or '/'. * `<` is ignored, because it is probably an html tag. */ export const TEST_FILE_PATH = /^(?!.*<).*[/.]/; /** Regular expression to detect if a string describes an HTML element. */ export const TEST_HTML = / Loading...
    ', // noData: "bi bi-search", noData: "bi bi-question-circle", expanderExpanded: "bi bi-chevron-down", // expanderExpanded: "bi bi-dash-square", expanderCollapsed: "bi bi-chevron-right", // expanderCollapsed: "bi bi-plus-square", expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander", // expanderLazy: "bi bi-chevron-bar-right", checkChecked: "bi bi-check-square", checkUnchecked: "bi bi-square", checkUnknown: "bi bi-dash-square-dotted", radioChecked: "bi bi-circle-fill", radioUnchecked: "bi bi-circle", radioUnknown: "bi bi-record-circle", folder: "bi bi-folder2", folderOpen: "bi bi-folder2-open", folderLazy: "bi bi-folder-symlink", doc: "bi bi-file-earmark", colSortable: "bi bi-chevron-expand", // colSortable: "bi bi-arrow-down-up", // colSortAsc: "bi bi-chevron-down", // colSortDesc: "bi bi-chevron-up", colSortAsc: "bi bi-arrow-down", colSortDesc: "bi bi-arrow-up", colFilter: "bi bi-filter-circle", colFilterActive: "bi bi-filter-circle-fill wb-helper-invalid", colMenu: "bi bi-three-dots-vertical", }, fontawesome6: { error: "fa-solid fa-triangle-exclamation", loading: "fa-solid fa-chevron-right fa-beat", noData: "fa-solid fa-circle-question", expanderExpanded: "fa-solid fa-chevron-down", expanderCollapsed: "fa-solid fa-chevron-right", expanderLazy: "fa-solid fa-chevron-right wb-helper-lazy-expander", checkChecked: "fa-regular fa-square-check", checkUnchecked: "fa-regular fa-square", checkUnknown: "fa-regular fa-square-minus", radioChecked: "fa-solid fa-circle", radioUnchecked: "fa-regular fa-circle", radioUnknown: "fa-regular fa-circle-question", folder: "fa-regular fa-folder-closed", folderOpen: "fa-regular fa-folder-open", folderLazy: "fa-solid fa-folder-plus", doc: "fa-regular fa-file", colSortable: "fa-solid fa-fw fa-sort", colSortAsc: "fa-solid fa-fw fa-sort-up", colSortDesc: "fa-solid fa-fw fa-sort-down", colFilter: "fa-solid fa-fw fa-filter", colFilterActive: "fa-solid fa-fw fa-filter wb-helper-invalid", colMenu: "fa-solid fa-fw fa-ellipsis-v", }, }; export const KEY_NODATA = "__not_found__"; /** Define which keys are handled by embedded control, and should * *not* be passed to tree navigation handler in cell-edit mode. */ export const INPUT_KEYS: { [key: string]: Array } = { text: ["left", "right", "home", "end", "backspace"], number: ["up", "down", "left", "right", "home", "end", "backspace"], checkbox: [], link: [], radiobutton: ["up", "down"], "select-one": ["up", "down"], "select-multiple": ["up", "down"], }; /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */ export const RESERVED_TREE_SOURCE_KEYS: Set = new Set([ "_format", // reserved for future use "_keyMap", // Used for compressed data format "_positional", // Used for compressed data format "_typeList", // Used for compressed data format @deprecated "_valueMap", // Used for compressed data format "_version", // reserved for future use "children", "columns", "types", ]); // /** Key codes that trigger grid navigation, even when inside an input element. */ // export const INPUT_BREAKOUT_KEYS: Set = new Set([ // // "ArrowDown", // // "ArrowUp", // "Enter", // "Escape", // ]); /** Map `KeyEvent.key` to navigation action. */ export const KEY_TO_NAVIGATION_MAP: { [key: string]: NavigationType } = { ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", Backspace: "parent", End: "lastCol", Home: "firstCol", "Control+End": "last", "Control+Home": "first", "Meta+ArrowDown": "last", // macOs "Meta+ArrowUp": "first", // macOs PageDown: "pageDown", PageUp: "pageUp", }; /** Map `KeyEvent.key` to navigation action. */ export const KEY_TO_COMMAND_MAP: { [key: string]: ApplyCommandType } = { " ": "toggleSelect", "+": "expand", Add: "expand", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", Backspace: "parent", "/": "collapseAll", Divide: "collapseAll", End: "lastCol", Home: "firstCol", "Control+End": "last", "Control+Home": "first", "Meta+ArrowDown": "last", // macOs "Meta+ArrowUp": "first", // macOs "*": "expandAll", Multiply: "expandAll", PageDown: "pageDown", PageUp: "pageUp", "-": "collapse", Subtract: "collapse", }; /** Return a callback that returns true if the node title matches the string * or regular expression. * @see {@link WunderbaumNode.findAll} */ export function makeNodeTitleMatcher(match: string | RegExp): MatcherCallback { if (match instanceof RegExp) { return function (node: WunderbaumNode) { return (match).test(node.title); }; } util.assert( typeof match === "string", `Expected a string or RegExp: ${match}` ); // s = escapeRegex(s.toLowerCase()); return function (node: WunderbaumNode) { return node.title === match; // console.log("match " + node, node.title.toLowerCase().indexOf(match)) // return node.title.toLowerCase().indexOf(match) >= 0; }; } /** Return a callback that returns true if the node title starts with a string (case-insensitive). */ export function makeNodeTitleStartMatcher(s: string): MatcherCallback { s = util.escapeRegex(s); const reMatch = new RegExp("^" + s, "i"); return function (node: WunderbaumNode) { return reMatch.test(node.title); }; } /** Compare two nodes by title (case-insensitive). * @deprecated Use `key` option instead of `cmp` in sort methods. */ export function nodeTitleSorter(a: WunderbaumNode, b: WunderbaumNode): number { const x = a.title.toLowerCase(); const y = b.title.toLowerCase(); return x === y ? 0 : x > y ? 1 : -1; } // /** Compare nodes by title (case-insensitive). */ // export function nodeTitleKeyGetter( // node: WunderbaumNode // ): string | number | Array { // return node.title.toLowerCase(); // } /** * Convert 'flat' to 'nested' format. * * Flat node entry format: * [PARENT_IDX, {KEY_VALUE_ARGS}] * or, if N _positional re defined: * [PARENT_IDX, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., POSITIONAL_ARG_N] * Even if _positional additional are defined, KEY_VALUE_ARGS can be appended: * [PARENT_IDX, POSITIONAL_ARG_1, ..., {KEY_VALUE_ARGS}] * * 1. Parent-referencing list is converted to a list of nested dicts with * optional `children` properties. * 2. `[POSITIONAL_ARGS]` are added as dict attributes. */ function unflattenSource(source: SourceObjectType): void { const { _format, _keyMap = {}, _positional = [], children } = source; const _positionalCount = _positional.length; if (_format !== "flat") { throw new Error(`Expected source._format: "flat", but got ${_format}`); } if (_positionalCount && _positional.includes("children")) { throw new Error( `source._positional must not include "children": ${_positional}` ); } let longToShort = _keyMap; if (_keyMap.t) { // Inverse keyMap was used (pre 0.7.0) // TODO: raise Error on final 1.x release const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console longToShort = {}; for (const [key, value] of Object.entries(_keyMap)) { longToShort[value] = key; } } const positionalShort = _positional.map((e: string) => longToShort[e] ?? e); const newChildren: SourceListType = []; const keyToNodeMap: { [key: string]: number } = {}; const indexToNodeMap: { [key: number]: any } = {}; const keyAttrName = longToShort["key"] ?? "key"; const childrenAttrName = longToShort["children"] ?? "children"; for (const [index, nodeTuple] of children.entries()) { // Node entry format: // [PARENT_ID, [POSITIONAL_ARGS]] // or // [PARENT_ID, POSITIONAL_ARG_1, POSITIONAL_ARG_2, ..., {KEY_VALUE_ARGS}] let kwargs; const [parentId, ...args] = nodeTuple; if (args.length === _positionalCount) { kwargs = {}; } else if (args.length === _positionalCount + 1) { kwargs = args.pop(); if (typeof kwargs !== "object") { throw new Error( `unflattenSource: Expected dict as last tuple element: ${nodeTuple}` ); } } else { throw new Error(`unflattenSource: unexpected tuple length: ${nodeTuple}`); } // Free up some memory as we go nodeTuple[1] = null; if (nodeTuple[2] != null) { nodeTuple[2] = null; } // We keep `kwargs` as our new node definition. Then we add all positional // values to this object: args.forEach((val: string, positionalIdx: number) => { kwargs[positionalShort[positionalIdx]] = val; }); args.length = 0; // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric // index of the source.children list. If PARENT_ID is a string, we search // a parent with node.key of this value. indexToNodeMap[index] = kwargs; const key = kwargs[keyAttrName]; if (key != null) { keyToNodeMap[key] = kwargs; } let parentNode = null; if (parentId === null) { // top-level node } else if (typeof parentId === "number") { parentNode = indexToNodeMap[parentId]; if (parentNode === undefined) { throw new Error( `unflattenSource: Could not find parent node by index: ${parentId}.` ); } } else { parentNode = keyToNodeMap[parentId]; if (parentNode === undefined) { throw new Error( `unflattenSource: Could not find parent node by key: ${parentId}` ); } } if (parentNode) { parentNode[childrenAttrName] ??= []; parentNode[childrenAttrName].push(kwargs); } else { newChildren.push(kwargs); } } source.children = newChildren; } /** * Decompresses the source data by * - converting from 'flat' to 'nested' format * - expanding short alias names to long names (if defined in _keyMap) * - resolving value indexes to value strings (if defined in _valueMap) * * @param source - The source object to be decompressed. * @returns void */ export function decompressSourceData(source: SourceObjectType): void { let { _format, _version = 1, _keyMap, _valueMap } = source; util.assert(_version === 1, `Expected file version 1 instead of ${_version}`); let longToShort = _keyMap; let shortToLong: { [key: string]: string } = {}; if (longToShort) { for (const [key, value] of Object.entries(longToShort)) { shortToLong[value] = key; } } // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction) // TODO: raise Error on final 1.x release if (longToShort && longToShort.t) { const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`; console.warn(msg); // eslint-disable-line no-console [longToShort, shortToLong] = [shortToLong, longToShort]; } // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap) // TODO: raise Error on final 1.x release if ((source)._typeList != null) { const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`; if (_valueMap != null) { throw new Error(msg); } else { console.warn(msg); // eslint-disable-line no-console _valueMap = { type: (source)._typeList }; delete (source)._typeList; } } if (_format === "flat") { unflattenSource(source); } delete source._format; delete source._version; delete source._keyMap; delete source._valueMap; delete source._positional; function _iter(childList: SourceListType) { for (const node of childList) { // Iterate over a list of names, because we modify inside the loop // (for ... of ... does not allow this) Object.getOwnPropertyNames(node).forEach((propName) => { const value: any = node[propName]; // Replace short names with long names if defined in _keyMap let longName = propName; if (_keyMap && shortToLong[propName] != null) { longName = shortToLong[propName]; if (longName !== propName) { node[longName] = value; delete node[propName]; } } // Replace type index with type name if defined in _valueMap if ( _valueMap && typeof value === "number" && _valueMap[longName] != null ) { const newValue = _valueMap[longName][value]; if (newValue == null) { throw new Error( `Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]` ); } node[longName] = newValue; } }); // Recursion if (node.children) { _iter(node.children); } } } if (_keyMap || _valueMap) { _iter(source.children); } } ================================================ FILE: src/debounce.ts ================================================ /*! * Wunderbaum - debounce.ts * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ /* * debounce & throttle, taken from https://github.com/lodash/lodash v4.17.21 * MIT License: https://raw.githubusercontent.com/lodash/lodash/4.17.21-npm/LICENSE * Modified for TypeScript type annotations. */ /* --- Custom defiitions for TypeScript --- */ type Procedure = (...args: any[]) => any; type DebounceOptions = { /** Specify invoking on the leading edge of the timeout. @default false */ leading?: boolean; /** The maximum time `func` is allowed to be delayed before it's invoked.*/ maxWait?: number; /** Specify invoking on the trailing edge of the timeout. @default true */ trailing?: boolean; }; type ThrottleOptions = { /** Specify invoking on the leading edge of the timeout. @default true */ leading?: boolean; /** Specify invoking on the trailing edge of the timeout. @default true */ trailing?: boolean; }; export interface DebouncedFunction { (this: ThisParameterType, ...args: Parameters): ReturnType; cancel: () => void; flush: () => any; pending: () => boolean; } /* --- */ /** Detect free variable `global` from Node.js. */ const freeGlobal = typeof global === "object" && global !== null && global.Object === Object && global; /** Detect free variable `globalThis` */ const freeGlobalThis = typeof globalThis === "object" && globalThis !== null && globalThis.Object == Object && globalThis; /** Detect free variable `self`. */ const freeSelf = typeof self === "object" && self !== null && self.Object === Object && self; /** Used as a reference to the global object. */ const root = freeGlobalThis || freeGlobal || freeSelf || Function("return this")(); /** * Checks if `value` is the * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) * * @since 0.1.0 * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is an object, else `false`. * @example * * isObject({}) * // => true * * isObject([1, 2, 3]) * // => true * * isObject(Function) * // => true * * isObject(null) * // => false */ function isObject(value: any) { const type = typeof value; return value != null && (type === "object" || type === "function"); } /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', debounce(calculateLayout, 150)) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })) * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * jQuery(source).on('message', debounced) * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ export function debounce( func: F, wait = 0, options: DebounceOptions = {} ): DebouncedFunction { let lastArgs: any, lastThis: any, maxWait: number | undefined, result: any, timerId: number | undefined, lastCallTime: number | undefined; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === "function"; if (typeof func !== "function") { throw new TypeError("Expected a function"); } wait = +wait || 0; if (isObject(options)) { leading = !!options.leading; maxing = "maxWait" in options; maxWait = maxing ? Math.max(+options.maxWait! || 0, wait) : maxWait; trailing = "trailing" in options ? !!options.trailing : trailing; } function invokeFunc(time: number) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; } function startTimer(pendingFunc: any, wait: number) { if (useRAF) { root.cancelAnimationFrame(timerId); return root.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } function cancelTimer(id: number) { if (useRAF) { return root.cancelAnimationFrame(id); } clearTimeout(id); } function leadingEdge(time: number) { // Reset any `maxWait` timer. lastInvokeTime = time; // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(time) : result; } function remainingWait(time: number) { const timeSinceLastCall = time - lastCallTime!; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall; return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting; } function shouldInvoke(time: number) { const timeSinceLastCall = time - lastCallTime!; const timeSinceLastInvoke = time - lastInvokeTime; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait!) ); } function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)); } function trailingEdge(time: number) { timerId = undefined; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastInvokeTime = 0; lastArgs = lastCallTime = lastThis = timerId = undefined; } function flush() { return timerId === undefined ? result : trailingEdge(Date.now()); } function pending() { return timerId !== undefined; } function debounced(this: any, ...args: any[]) { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs = args; // eslint-disable-next-line @typescript-eslint/no-this-alias lastThis = this; lastCallTime = time; if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; } /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds (or once per browser frame). The throttled function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `throttle` and `debounce`. * * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] * The number of milliseconds to throttle invocations to; if omitted, * `requestAnimationFrame` is used (if available). * @param [options={}] The options object. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', throttle(updatePosition, 100)) * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * const throttled = throttle(renewToken, 300000, { 'trailing': false }) * jQuery(element).on('click', throttled) * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel) */ export function throttle( func: F, wait = 0, options: ThrottleOptions = {} ) { let leading = true; let trailing = true; if (typeof func !== "function") { throw new TypeError("Expected a function"); } if (isObject(options)) { leading = "leading" in options ? !!options.leading : leading; trailing = "trailing" in options ? !!options.trailing : trailing; } return debounce(func, wait, { leading, trailing, maxWait: wait, }); } ================================================ FILE: src/deferred.ts ================================================ /*! * Wunderbaum - deferred * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ type PromiseCallbackType = (val: any) => void; type finallyCallbackType = () => void; /** * Implement a ES6 Promise, that exposes a resolve() and reject() method. * * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}. * Example: * ```js * function foo() { * let dfd = new Deferred(), * ... * dfd.resolve('foo') * ... * return dfd.promise(); * } * ``` */ export class Deferred { private _promise: Promise; protected _resolve: any; protected _reject: any; constructor() { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } /** Resolve the Promise. */ resolve(value?: any) { this._resolve(value); } /** Reject the Promise. */ reject(reason?: any) { this._reject(reason); } /** Return the native Promise instance.*/ promise() { return this._promise; } /** Call Promise.then on the embedded promise instance.*/ then(cb: PromiseCallbackType) { return this._promise.then(cb); } /** Call Promise.catch on the embedded promise instance.*/ catch(cb: PromiseCallbackType) { return this._promise.catch(cb); } /** Call Promise.finally on the embedded promise instance.*/ finally(cb: finallyCallbackType) { return this._promise.finally(cb); } } ================================================ FILE: src/drag_observer.ts ================================================ /*! * Wunderbaum - drag_observer * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ export type DragCallbackArgType = { /** "dragstart", "drag", or "dragstop". */ type: string; /** Original mousedown or touch event that triggered the dragstart event. */ startEvent: MouseEvent | TouchEvent; /** Original mouse or touch event that triggered the current drag event. * Note that this is not the same as `startEvent`, but a mousemove in case of * a dragstart threshold. */ event: MouseEvent | TouchEvent; /** Custom data that was passed to the DragObserver, typically on dragstart. */ customData: any; /** Element which is currently dragged. */ dragElem: HTMLElement | null; /** Relative horizontal drag distance since start. */ dx: number; /** Relative vertical drag distance since start. */ dy: number; /** False if drag was canceled. */ apply?: boolean; }; export type DragCallbackType = (e: DragCallbackArgType) => boolean | void; type DragObserverOptionsType = { /**Event target (typically `window.document`). */ root: EventTarget; /**Event delegation selector.*/ selector?: string; /**Minimum drag distance in px. */ thresh?: number; /**Return `false` to cancel drag. */ dragstart: DragCallbackType; drag?: DragCallbackType; dragstop?: DragCallbackType; }; /** * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'. */ export class DragObserver { protected _handler; protected root: EventTarget; protected start: { event: MouseEvent | TouchEvent | null; x: number; y: number; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; } = { event: null, x: 0, y: 0, altKey: false, ctrlKey: false, metaKey: false, shiftKey: false, }; protected dragElem: HTMLElement | null = null; protected dragging: boolean = false; protected customData: object = {}; // TODO: touch events protected events = ["mousedown", "mouseup", "mousemove", "keydown"]; protected opts: DragObserverOptionsType; constructor(opts: DragObserverOptionsType) { if (!opts.root) { throw new Error("Missing `root` option."); } this.opts = Object.assign({ thresh: 5 }, opts); this.root = opts.root; this._handler = this.handleEvent.bind(this) as EventListener; this.events.forEach((type) => { this.root.addEventListener(type, this._handler); }); } /** Unregister all event listeners. */ disconnect() { this.events.forEach((type) => { this.root.removeEventListener(type, this._handler); }); } public getDragElem(): HTMLElement | null { return this.dragElem; } public isDragging(): boolean { return this.dragging; } public stopDrag(cb_event?: DragCallbackArgType): void { if (this.dragging && this.opts.dragstop && cb_event) { cb_event.type = "dragstop"; try { this.opts.dragstop(cb_event); } catch (err) { console.error("dragstop error", err); // eslint-disable-line no-console } } this.dragElem = null; this.dragging = false; this.start.event = null; this.customData = {}; } protected handleEvent(e: MouseEvent): boolean | void { const type = e.type; const opts = this.opts; const cb_event: DragCallbackArgType = { type: e.type, startEvent: type === "mousedown" ? e : this.start.event!, event: e, customData: this.customData, dragElem: this.dragElem, dx: e.pageX - this.start.x, dy: e.pageY - this.start.y, apply: undefined, }; // console.log("handleEvent", type, cb_event); switch (type) { case "keydown": this.stopDrag(cb_event); break; case "mousedown": if (this.dragElem) { this.stopDrag(cb_event); break; } if (opts.selector) { let elem = e.target as HTMLElement; if (elem.matches(opts.selector)) { this.dragElem = elem; } else { elem = elem.closest(opts.selector) as HTMLElement; if (elem) { this.dragElem = elem; } else { break; // no event delegation selector matched } } } this.start.event = e; this.start.x = e.pageX; this.start.y = e.pageY; this.start.altKey = e.altKey; this.start.ctrlKey = e.ctrlKey; this.start.metaKey = e.metaKey; this.start.shiftKey = e.shiftKey; break; case "mousemove": // TODO: debounce/throttle? // TODO: horizontal mode: ignore if dx unchanged if (!this.dragElem) { break; } if (!this.dragging) { if (opts.thresh) { const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy; if (dist2 < opts.thresh * opts.thresh) { break; } } cb_event.type = "dragstart"; if (opts.dragstart(cb_event) === false) { this.stopDrag(cb_event); break; } this.dragging = true; } if (this.dragging && this.opts.drag) { cb_event.type = "drag"; this.opts.drag(cb_event); } break; case "mouseup": if (!this.dragging) { this.stopDrag(cb_event); break; } if (e.button === 0) { cb_event.apply = true; } else { cb_event.apply = false; } this.stopDrag(cb_event); break; } } } ================================================ FILE: src/types.ts ================================================ /*! * Wunderbaum - types * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { WunderbaumNode } from "./wb_node"; import { Wunderbaum } from "./wunderbaum"; import { WunderbaumOptions } from "./wb_options"; /** A value that can either be true, false, or undefined. */ export type TristateType = boolean | undefined; /** Show/hide checkbox or display a radiobutton icon instead. */ export type CheckboxOption = boolean | "radio"; /** A value that can either be true, false, or undefined. */ export type SortOrderType = "asc" | "desc" | undefined; /** An icon may either be * a string-tag that references an entry in the `iconMap` (e.g. `"folderOpen"`)), * an HTML string that contains a `<` and is used as-is, * an image URL string that contains a `.` or `/` and is rendered as ``, * a class string such as `"bi bi-folder"`, * or a boolean value that indicates if the default icon should be used or hidden. */ export type IconOption = boolean | string; /** Show/hide default tooltip or display a string. */ export type TooltipOption = boolean | string; /* * `source` can be a url, an array of nodes, or an object with `url`, ... */ export interface SourceAjaxType { url: string; params?: any; body?: any; options?: RequestInit; } export type SourceListType = Array; export interface SourceObjectType { _format?: "nested" | "flat"; _version?: number; types?: NodeTypeDefinitionMap; columns?: ColumnDefinitionList; children: SourceListType; _keyMap?: { [key: string]: string }; _positional?: Array; // _typeList?: Array; _valueMap?: { [key: string]: Array }; } /** Possible initilization for tree nodes. */ export type SourceType = | string | SourceListType | SourceAjaxType | SourceObjectType; /** Passed to `find...()` methods. Should return true if node matches. */ export type MatcherCallback = (node: WunderbaumNode) => boolean; /** Used for `tree.iconBadge` event. */ export type WbIconBadgeCallback = ( e: WbIconBadgeEventType ) => WbIconBadgeEventResultType; /** * Passed to `sort()` methods. Should return -1, 0, or 1. * @deprecated Use SortKeyCallback instead */ export type SortCallback = (a: WunderbaumNode, b: WunderbaumNode) => number; /** Passed to `sort()` methods. Should return a representation that can be compared using `<`. */ export type SortKeyCallback = (node: WunderbaumNode) => string | number | any[]; /** When set as option, called when the value is needed (e.g. `colspan` type definition). */ export type BoolOptionResolver = (node: WunderbaumNode) => boolean; /** When set as option, called when the value is needed (e.g. `icon` type definition). */ export type BoolOrStringOptionResolver = ( node: WunderbaumNode ) => boolean | string; /** A callback that receives a node instance and returns an arbitrary value type. */ export type NodeAnyCallback = (node: WunderbaumNode) => any; /** A callback that receives a node instance and returns a string value. */ export type NodeStringCallback = (node: WunderbaumNode) => string; /** A callback that receives a node instance and property name returns a value. */ export type NodePropertyGetterCallback = ( node: WunderbaumNode, propName: string ) => any; /** A callback that receives a node instance and returns an iteration modifier. */ export type NodeVisitCallback = (node: WunderbaumNode) => NodeVisitResponse; /** * Returned by `NodeVisitCallback` to control iteration. * `false` stops iteration, `skip` skips descendants but continues. * All other values continue iteration. */ export type NodeVisitResponse = "skip" | boolean | void; /** * A callback that receives a node-data dictionary and a node instance and * returns an iteration modifier. */ export type NodeToDictCallback = ( dict: WbNodeData, node: WunderbaumNode ) => NodeVisitResponse; /** * A callback that receives a node instance and may returnsa `false` to prevent * (de)selection. */ export type NodeSelectCallback = (node: WunderbaumNode) => boolean | void; /** @internal */ export type DeprecationOptions = { since?: string; hint?: string; }; /** * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. */ export type DynamicBoolOption = boolean | BoolOptionResolver; export type DynamicStringOption = string | BoolOptionResolver; export type DynamicBoolOrStringOption = | boolean | string | BoolOrStringOptionResolver; export type DynamicCheckboxOption = CheckboxOption | BoolOrStringOptionResolver; export type DynamicIconOption = IconOption | BoolOrStringOptionResolver; export type DynamicTooltipOption = TooltipOption | BoolOrStringOptionResolver; // type WithWildcards = T & { [key: string]: unknown }; /** A plain object (dictionary) that represents a node instance. */ export interface WbNodeData { /** Defines if the `selected` state is displayed as checkbox, radio button, * or hidden. * Defaults to {@link WunderbaumOptions.checkbox}. */ checkbox?: CheckboxOption; /** Optional list of child nodes. * If `children` is an empty array, the node is considered a leaf. * If `lazy` is true and `children is undefined or null, the node, is * considered unloaded. Otherwise, the node is considered a leaf. */ children?: Array; /** Additional classes that are added to `
    `. */ classes?: string; /** Only show title in a single, merged column. */ colspan?: boolean; /** Expand this node. */ expanded?: boolean; /** Defaults to standard icons (doc, folder, folderOpen, ...) * from {@link WunderbaumOptions.iconMap}. * Can be overridden by {@link WunderbaumOptions.icon}. */ icon?: IconOption; /** Tooltip for the node icon only. Defaults to {@link WunderbaumOptions.iconTooltip}. */ iconTooltip?: TooltipOption; /** The node's key. Must be unique for the whole tree. Defaults to a sequence number. */ key?: string; /** If true (and children are undefined or null), the node is considered lazy * and {@link WunderbaumOptions.lazyLoad} is called when expanded. */ lazy?: boolean; /** Make child nodes single-select radio buttons. */ radiogroup?: boolean; /** Node's reference key. Unlike {@link WunderbaumNode.key}, this value * may be non-unique. Nodes within the tree that share the same refKey are considered * clones. */ refKey?: string; /** The node's selection status, typically displayed as a checkbox. */ selected?: boolean; /** The node's status, typically displayed as merged single row. * @see {@link Wunderbaum.setStatus} */ statusNodeType?: NodeStatusType; /** The node's title. Will be html escaped to prevent XSS. */ title: string; /** Pass true to set node tooltip to the node's title. Defaults to {@link WunderbaumOptions.tooltip}. */ tooltip?: TooltipOption; /** Inherit shared settings from the matching entry in `InitWunderbaumOptions.types`. */ type?: string; /** Set to `true` to prevent selection. Defaults to {@link WunderbaumOptions.unselectable}. */ unselectable?: boolean; /** @internal */ _treeId?: string; /** Other data is passed to `node.data` and can be accessed via `node.data.NAME` */ [key: string]: unknown; } /** A plain object (dictionary) that defines node icons. */ export interface IconMapType { error: string; loading: string; noData: string; expanderExpanded: string; expanderCollapsed: string; expanderLazy: string; checkChecked: string; checkUnchecked: string; checkUnknown: string; radioChecked: string; radioUnchecked: string; radioUnknown: string; folder: string; folderOpen: string; folderLazy: string; doc: string; colSortable: string; colSortAsc: string; colSortDesc: string; colFilter: string; colFilterActive: string; colMenu: string; [key: string]: string; } /* ----------------------------------------------------------------------------- * EVENT CALLBACK TYPES * ---------------------------------------------------------------------------*/ /** Retuen value of an event handler that can return `false` to prevent the default action. */ export type WbCancelableEventResultType = false | void; export interface WbTreeEventType { /** Name of the event. */ type: string; /** The affected tree instance. */ tree: Wunderbaum; /** Exposed utility module methods * (see [API docs](https://mar10.github.io/wunderbaum/api/modules/util.html)). */ util: any; /** Originating HTML event if any (e.g. `click`). */ event?: Event; // [key: string]: unknown; } export interface WbNodeEventType extends WbTreeEventType { /** The affected target node. */ node: WunderbaumNode; /** * Contains the node's type information, i.e. `tree.types[node.type]` if * defined. Set to `{}` otherwise. @see {@link Wunderbaum.types} */ typeInfo: NodeTypeDefinition; } export interface WbActivateEventType extends WbNodeEventType { prevNode: WunderbaumNode; /** The original event. */ event: Event; } export interface WbChangeEventType extends WbNodeEventType { /** Additional information derived from the original change event. */ info: WbEventInfo; /** The embedded element that fired the change event. */ inputElem: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; /** The new value of the embedded element, depending on the input element type. */ inputValue: any; /** Result of `inputElem.checkValidity()`. */ inputValid: boolean; } export interface WbClickEventType extends WbTreeEventType { /** The original event. */ event: MouseEvent; /** The clicked node if any. */ node: WunderbaumNode; /** Additional information derived from the original mouse event. */ info: WbEventInfo; } export interface WbDeactivateEventType extends WbNodeEventType { nextNode: WunderbaumNode; /** The original event. */ event: Event; } export interface WbEditApplyEventType extends WbNodeEventType { /** Additional information derived from the original change event. */ info: WbEventInfo; /** The input element of the node title that fired the change event. */ inputElem: HTMLInputElement; /** The previous node title. */ oldValue: string; /** The new node title. */ newValue: string; /** Result of `inputElem.checkValidity()`. */ inputValid: boolean; } export interface WbEditEditEventType extends WbNodeEventType { /** The input element of the node title that was just created. */ inputElem: HTMLInputElement; } // export interface WbEnhanceTitleEventType extends WbNodeEventType { // titleSpan: HTMLSpanElement; // } export interface WbErrorEventType extends WbNodeEventType { error: any; } export interface WbExpandEventType extends WbNodeEventType { flag: boolean; } export interface WbFocusEventType extends WbTreeEventType { /** The original event. */ event: FocusEvent; /** True if `focusin`, false if `focusout`. */ flag: boolean; } export interface WbIconBadgeEventType extends WbNodeEventType { iconSpan: HTMLElement; } export interface WbIconBadgeEventResultType { /** Content of the badge `` if any. */ badge: string | number | HTMLSpanElement | null | false; /** Additional class name(s), separate with space. */ badgeClass?: string; /** Tooltip for the badge. */ badgeTooltip?: string; } export interface WbInitEventType extends WbTreeEventType { error?: any; } export interface WbKeydownEventType extends WbTreeEventType { /** The original event. */ event: KeyboardEvent; node: WunderbaumNode; /** Additional information derived from the original keyboard event. */ info: WbEventInfo; } export interface WbModifyChildEventType extends WbNodeEventType { /** Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ operation: string; child: WunderbaumNode; } export interface WbReceiveEventType extends WbNodeEventType { response: any; } export interface WbSelectEventType extends WbNodeEventType { flag: boolean; } export interface WbButtonClickEventType extends WbTreeEventType { info: WbEventInfo; /** The associated command, e.g. 'menu', 'sort', 'filter', ... */ command: string; } export interface WbRenderEventType extends WbNodeEventType { /** * True if the node's markup was not yet created. In this case the render * event should create embedded input controls (in addition to update the * values according to to current node data).
    * False if the node's markup was already created. In this case the render * event should only update the values according to to current node data. */ isNew: boolean; /** The node's `` element. */ nodeElem: HTMLSpanElement; /** True if the node only displays the title and is stretched over all remaining columns. */ isColspan: boolean; /** * Array of node's `` elements. * The first element is ``, which contains the * node title and icon (`idx: 0`, id: '*'`). */ allColInfosById: ColumnEventInfoMap; /** * Array of node's `` elements, * *that should be rendered by the event handler*. * In contrast to `allColInfosById`, the node title is not part of this array. * If node.isColspan() is true, this array is empty (`[]`). * This allows to iterate over all relevant in a simple loop: * ``` * for (const col of Object.values(e.renderColInfosById)) { * switch (col.id) { * default: * // Assumption: we named column.id === node.data.NAME * col.elem.textContent = node.data[col.id]; * break; * } * } */ renderColInfosById: ColumnEventInfoMap; } /** * Contains the node's type information, i.e. `tree.types[node.type]` if * defined. * @see {@link Wunderbaum.types} and {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. */ export interface NodeTypeDefinition { /** En/disable checkbox for matching nodes. */ checkbox?: CheckboxOption; /** Optional class names that are added to all `div.wb-row` elements of matching nodes. */ classes?: string; /** Only show title and hide other columns if any. */ colspan?: boolean; /** Default icon for matching nodes. */ icon?: IconOption; /** Default icon tooltip for matching nodes. */ iconTooltip?: TooltipOption; // and more [key: string]: unknown; } /* ----------------------------------------------------------------------------- * DATA TYPES * ---------------------------------------------------------------------------*/ export type NodeTypeDefinitionMap = { [type: string]: NodeTypeDefinition }; /** * Column type definitions. * @see {@link Wunderbaum.columns} */ export interface ColumnDefinition { /** Column ID as defined in `tree.columns` definition ("*" for title column). */ id: string; /** Column header (defaults to id) */ title: string; /** Column header tooltip (optional) */ tooltip?: string; /** Column width or weight. * Either an absolute pixel value (e.g. `"50px"`) or a relative weight (e.g. `1`) * that is used to calculate the width inside the remaining available space. * Default: `"*"`, which is interpreted as `1`. */ width?: string | number; /** Only used for columns with a relative weight. * Default: `4px`. */ minWidth?: string | number; /** Allow user to resize the column. * @default false (or global tree option `columnsSortable`) * @see {@link WunderbaumOptions.columnsResizable}. * @since 0.10.0 */ resizable?: boolean; /** Optional custom column width when user resized by mouse drag. * Default: unset. */ customWidthPx?: number; /** Display a 'filter' button in the column header. Default: false.
    * Note: The actual filtering must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsFilterable`) * @since 0.11.0 */ filterable?: boolean; /** . * Default: inactive.
    * Note: The actual filtering must be implemented in the `buttonClick()` event. */ filterActive?: boolean; /** Display a 'sort' button in the column header. Default: false.
    * Note: The actual sorting must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsSortable`) * @see {@link WunderbaumOptions.columnsSortable}. * @since 0.11.0 */ sortable?: boolean; /** Optional custom column sort orde when user clicked the sort icon. * Default: unset, e.g. not sorted.
    * Note: The actual sorting must be implemented in the `buttonClick()` event. * @since 0.11.0 */ sortOrder?: SortOrderType; /** Display a menu icon that may open a context menu for this column. * Note: The actual functionality must be implemented in the `buttonClick()` event. * @default false (or global tree option `columnsMenu`) * @see {@link WunderbaumOptions.columnsMenu}. * @since 0.11.0 */ menu?: boolean; /** Optional class names that are added to all `span.wb-col` header AND data * elements of that column. Separate multiple classes with space. */ classes?: string; /** If `headerClasses` is a set, it will be used for the header element only * (unlike `classes`, which is used for body and header cells). * Separate multiple classes with space. */ headerClasses?: string; // /** A list of icon definitions added to the column header. // */ // headerIcons?: string; /** Optional HTML content that is rendered into all `span.wb-col` elements of that column.*/ html?: string; /** @internal */ _weight?: number; /** @internal */ _widthPx?: number; /** @internal */ _ofsPx?: number; // ... and more [key: string]: unknown; } export type ColumnDefinitionList = Array; /** * Column information (passed to the `render` event). */ export interface ColumnEventInfo { /** Column ID as defined in `tree.columns` definition ("*" for title column). */ id: string; /** Column index (0: leftmost title column). */ idx: number; /** The cell's `` element (null for plain trees). */ elem: HTMLSpanElement | null; /** The value of `tree.columns[]` for the current index. */ info: ColumnDefinition; } export type ColumnEventInfoMap = { [colId: string]: ColumnEventInfo }; /** * Additional information derived from mouse or keyboard events. * @see {@link Wunderbaum.getEventInfo} */ export interface WbEventInfo { /** The original HTTP Event.*/ event: MouseEvent | KeyboardEvent; /** Canonical descriptive string of the event type including modifiers, * e.g. `Ctrl+Down`. @see {@link util.eventToString} */ canonicalName: string; /** The tree instance. */ tree: Wunderbaum; /** The affected node instance instance if any. */ node: WunderbaumNode | null; /** The affected part of the node span (e.g. title, expander, ...). */ region: NodeRegion; /** The definition of the affected column if any. */ colDef?: ColumnDefinition; /** The index of affected column or -1. */ colIdx: number; /** The column definition ID of affected column if any. */ colId?: string; /** The affected column's span tag if any. */ colElem?: HTMLSpanElement; } // export type WbTreeCallbackType = (e: WbTreeEventType) => any; // export type WbNodeCallbackType = (e: WbNodeEventType) => any; // export type WbRenderCallbackType = (e: WbRenderEventType) => void; export type FilterModeType = null | "mark" | "dim" | "hide"; export type SelectModeType = "single" | "multi" | "hier"; export type NavigationType = | "down" | "first" | "firstCol" | "last" | "lastCol" | "left" | "nextMatch" | "pageDown" | "pageUp" | "parent" | "prevMatch" | "right" | "up"; export type ApplyCommandType = | NavigationType | "addChild" | "addSibling" | "collapse" | "collapseAll" | "copy" | "cut" | "edit" | "expand" | "expandAll" | "indent" | "moveDown" | "moveUp" | "outdent" | "paste" | "remove" | "rename" | "toggleSelect"; export type NodeFilterResponse = "skip" | "branch" | boolean | void; export type NodeFilterCallback = (node: WunderbaumNode) => NodeFilterResponse; /** * Possible values for {@link WunderbaumNode.update} and {@link Wunderbaum.update}. */ export enum ChangeType { /** Re-render the whole viewport, headers, and all rows. */ any = "any", /** A node's title, icon, columns, or status have changed. Update the existing row markup. */ data = "data", /** The `tree.columns` definition has changed beyond simple width adjustments. */ colStructure = "colStructure", /** The viewport/window was resized. Adjust layout attributes for all elements. */ resize = "resize", /** A node's definition has changed beyond status and data. Re-render the whole row's markup. */ row = "row", /** Nodes have been added, removed, etc. Update markup. */ structure = "structure", /** A node's status has changed. Update current row's classes, to reflect active, selected, ... */ status = "status", /** Vertical scroll event. Update the 'top' property of all rows. */ scroll = "scroll", } /** @internal */ export enum RenderFlag { clearMarkup = "clearMarkup", header = "header", redraw = "redraw", scroll = "scroll", } /** Possible values for {@link WunderbaumNode.setStatus}. */ export enum NodeStatusType { ok = "ok", loading = "loading", error = "error", noData = "noData", paging = "paging", } /** Define the subregion of a node, where an event occurred. */ export enum NodeRegion { unknown = "", checkbox = "checkbox", column = "column", expander = "expander", icon = "icon", prefix = "prefix", title = "title", } /** Initial navigation mode and possible transition. */ export enum NavModeEnum { /** Start with row mode, but allow cell-nav mode */ startRow = "startRow", /** Cell-nav mode only */ cell = "cell", /** Start in cell-nav mode, but allow row mode */ startCell = "startCell", /** Row mode only */ row = "row", } /** Translatable strings. */ export type TranslationsType = { /** @default "Loading..." */ loading: string; /** @default "Error" */ loadError: string; /** @default "No data" */ noData: string; /** @default " » " */ breadcrumbDelimiter: string; /** @default "Found ${matches} of ${count}" */ queryResult: string; /** @default "No result" */ noMatch: string; /** @default "${match} of ${matches}" */ matchIndex: string; }; /* ----------------------------------------------------------------------------- * METHOD OPTIONS TYPES * ---------------------------------------------------------------------------*/ /** Possible values for {@link WunderbaumNode.addChildren}. */ export interface AddChildrenOptions { /** Insert children before this node (or index) * @default undefined or null: append as last child */ before?: WunderbaumNode | number | null; /** * Set `node.expanded = true` according to tree.options.minExpandLevel. * This does *not* load lazy nodes. * @default true */ applyMinExpanLevel?: boolean; /** (@internal Internal use, do not set! ) */ _level?: number; } /** Possible values for {@link Wunderbaum.applyCommand} and {@link WunderbaumNode.applyCommand}. */ export interface ApplyCommandOptions { [key: string]: unknown; } /** Possible values for {@link Wunderbaum.expandAll} and {@link WunderbaumNode.expandAll}. */ export interface ExpandAllOptions { /** Expand and load lazy nodes @default false */ loadLazy?: boolean; /** Unload lazily loaded children if any (if collapsing). @default false */ resetLazy?: boolean; /** Ignore tree's `minExpandLevel` option @default false */ force?: boolean; /** Restrict expand level. * Pass 0 to make only toplevel nodes visible, 1 to expand one level deeper, etc. * @default unset (unlimited) */ depth?: number; /** * Also collapse child nodes beyond the `depth` level. * Otherwise only the `depth` level is collapsed and the expand state of the * descendants is retained. * Only in combination with collapse and `depth`. * Expanding with `deep` option is not supported as recursion depth implied by * the `depth` option. However a `deep` option will be considered if * `collapseOthers` is set. * @default false */ deep?: boolean; /** * Expand up to level=depth and collapse all other branches. * Only in combination with `flag == true`, `depth > 0`. * @default false */ collapseOthers?: boolean; /** Keep active node visible @default true */ keepActiveNodeVisible?: boolean; } /** * Possible option values for {@link Wunderbaum.filterNodes}. * The defaults are inherited from the tree instances ´tree.options.filter` * settings (see also {@link FilterOptionsType}). */ export interface FilterNodesOptions { /** Expand all branches that contain matches while filtered @default false */ autoExpand?: boolean; /** Whether to implicitly match all children of matched nodes @default false */ matchBranch?: boolean; /** Match single characters in order, e.g. 'fb' will match 'FooBar' @default false */ fuzzy?: boolean; /**Hide expanders if all child nodes are hidden by filter @default false */ hideExpanders?: boolean; /** Highlight matches by wrapping inside `` tags. * Does not work for filter callbacks. @default true */ highlight?: boolean; /** Match end nodes only @default false */ leavesOnly?: boolean; /** Grayout unmatched nodes (pass 'hide' to remove instead) @default 'dim' */ mode?: FilterModeType; /** Display a 'no data' status node if result is empty @default true */ noData?: boolean | string; } /** Possible values for {@link Wunderbaum.getState}. */ export interface GetStateOptions { /** Include the active node's key (and expand its parents). @default true */ activeKey?: boolean; /** Include the expanded keys. @default false */ expandedKeys?: boolean; /** Include the selected keys. @default false */ selectedKeys?: boolean; } /** Possible values for {@link Wunderbaum.setState}. */ export interface SetStateOptions { /** Recursively load lazy nodes as needed. @default false */ expandLazy?: boolean; } /** Used by {@link Wunderbaum.getState} and {@link Wunderbaum.setState}. */ export interface TreeStateDefinition { /** List of expanded node's keys. */ expandedKeys: Array | undefined; /** The active node's key if any. */ activeKey: string | null; /** The active column index if any. */ activeColIdx: number | null; /** List of selected node's keys. */ selectedKeys: Array | undefined; } /** Possible values for {@link Wunderbaum.loadLazyNodes} `options` argument. */ export interface LoadLazyNodesOptions { /** Expand node (otherwise load, but keep collapsed). @default true */ expand?: boolean; /** Force reloading even if already loaded. @default false */ force?: boolean; /** Do not send events. @default false */ noEvents?: boolean; } /** Possible values for {@link WunderbaumNode.makeVisible}. */ export interface MakeVisibleOptions { /** Do not animate expand (currently not implemented). @default false */ noAnimation?: boolean; /** Scroll node into visible viewport area if required. @default true */ scrollIntoView?: boolean; /** Do not send events. @default false */ noEvents?: boolean; } /** Possible values for {@link WunderbaumNode.navigate}. */ export interface NavigateOptions { /** Activate the new node (otherwise focus only). @default true */ activate?: boolean; /** Originating event (e.g. KeyboardEvent) if any. */ event?: Event; } /** Possible values for {@link WunderbaumNode._render}. */ export interface RenderOptions { /** Which parts need update? @default ChangeType.data */ change?: ChangeType; /** Where to append a new node. @default 'last' */ after?: any; /** @internal. @default false */ isNew?: boolean; /** @internal. @default false */ preventScroll?: boolean; /** @internal. @default false */ isDataChange?: boolean; /** @internal. @default false */ top?: number; /** @internal. @default true */ resizeCols?: boolean; } /** Possible values for {@link WunderbaumNode.scrollIntoView} `options` argument. */ export interface ScrollIntoViewOptions { /** Do not animate (currently not implemented). @default false */ noAnimation?: boolean; /** Do not send events. @default false */ noEvents?: boolean; /** Keep this node visible at the top in any case. */ topNode?: WunderbaumNode; /** Add N pixel offset at top. */ ofsY?: number; } /** Possible values for {@link Wunderbaum.scrollTo} `options` argument. */ export interface ScrollToOptions extends ScrollIntoViewOptions { /** Which node to scroll into the viewport.*/ node: WunderbaumNode; } /** Possible values for {@link WunderbaumNode.setActive} `options` argument. */ export interface SetActiveOptions { /** Generate (de)activate event, even if node already has this status (@default: false). */ retrigger?: boolean; /** Do not generate (de)activate event (@default: false). */ noEvents?: boolean; // /** Mark node as focused node (default: true). // * Combine with `focusTree: true` to set keyboard focus to tree container. // */ // focusNode?: boolean; /** Call `tree.setFocus()` to acquire keyboard focus (@default: false). */ focusTree?: boolean; /** Optional original event that will be passed to the (de)activate handler. */ event?: Event; /** Also call {@link Wunderbaum.setColumn}. */ colIdx?: number | string; /** * Focus embedded input control of the grid cell if any (requires colIdx >= 0). * If colIdx is 0 or '*', the node title is put into edit mode. * Implies `focusTree: true`, requires `colIdx`. */ edit?: boolean; } /** Possible values for {@link Wunderbaum.setColumn} `options` argument. */ export interface SetColumnOptions { /** * Focus embedded input control of the grid cell if any . * If colIdx is 0 or '*', the node title is put into edit mode. * @default false */ edit?: boolean; /** Horizontically scroll into view. @default: true */ scrollIntoView?: boolean; } /** Possible values for {@link WunderbaumNode.setExpanded} `options` argument. */ export interface SetExpandedOptions { /** Ignore {@link WunderbaumOptions}.minExpandLevel. @default false */ force?: boolean; /** Immediately update viewport (async otherwise). @default false */ immediate?: boolean; /** Do not animate expand (currently not implemented). @default false */ noAnimation?: boolean; /** Do not send events. @default false */ noEvents?: boolean; /** Unload lazily loaded children if any (if collapsing). @default false */ resetLazy?: boolean; /** Scroll up to bring expanded nodes into viewport. @default false */ scrollIntoView?: boolean; } /** Possible values for {@link WunderbaumNode.update} `options` argument. */ export interface UpdateOptions { /** Force immediate redraw instead of throttled/async mode. @default false */ immediate?: boolean; // /** Remove HTML markup of all rendered nodes before redraw. @default false */ // removeMarkup?: boolean; } /** Possible values for {@link WunderbaumNode.setSelected} `options` argument. */ export interface SetSelectedOptions { /** Ignore restrictions, e.g. (`unselectable`). @default false */ force?: boolean; /** Do not send `beforeSelect` or `select` events. @default false */ noEvents?: boolean; /** Apply to all descendant nodes (only for `selectMode: 'multi'`). @default false */ propagateDown?: boolean; // /** Apply to all ancestor nodes. @default false */ // propagateUp?: boolean; /** Called for every node. May return false to prevent action. @default null */ callback?: NodeSelectCallback; } /** Possible values for {@link WunderbaumNode.setStatus} `options` argument. */ export interface SetStatusOptions { /** Displayed as status node title. */ message?: string; /** Used as tooltip. */ details?: string; } /** * Possible values for {@link Wunderbaum.reload} `options` argument. */ export interface ReloadOptions { /** Load this source instead. @default initial source (if loaded via ajax) */ source?: SourceType; /** Reactivate currently active node if any. @default true */ reactivate?: boolean; } /** * Possible values for {@link WunderbaumNode.resetNativeChildOrder} `options` argument. */ export interface ResetOrderOptions { /** Sort descendants recursively. @default true */ recursive?: boolean; /** The name of the node property that will be renumbered. * @default `_nativeIndex`. */ propName?: string; } /** * Possible values for {@link Wunderbaum.sort} and {@link WunderbaumNode.sort} * `options` argument. */ export interface SortOptions { /** The name of the node property that will be used for sorting. * Mandatory, unless {@link key} or {@link colId} are given. */ propName?: string; /** Callback that determines a node representation for comparison. * @default {@link common.nodeTitleKeyGetter} */ key?: SortKeyCallback; /** Callback that determines the order. @default {@link common.nodeTitleSorter} * @deprecated use {@link key} instead */ cmp?: SortCallback; /** Sort order 'asc' or 'desc'. * @default 'asc' (or if `updateColInfo` is true, the rotated status of the * column definition. * See also {@link WunderbaumOptions.sortFoldersFirst}. */ order?: SortOrderType; /** Sort descendants recursively. @default true */ deep?: boolean; /** Sort string values case insensitive. @default false */ caseInsensitive?: boolean; /** * Sort by this property if order is `undefined`. * See also {@link WunderbaumNode.resetNativeChildOrder}. * @default `_nativeIndex`. */ nativeOrderPropName?: string; /** * Rotate sort order (asc -> desc -> none) before sorting. * Update the sort icons in the column header * Note: * Sorting is done in-place. There is no 'unsorted' state, but we can * call `setCurrentSortOrder()` to renumber the `node._sortIdx` property, * which will be used as sort key, when `order` is `undefined`. * @default false */ updateColInfo?: boolean; /** Column ID as defined in `tree.columns` definition. Required if updateColInfo is true.*/ colId?: string; } /** * Possible values for {@link WunderbaumNode.sortByProperty} `options` argument. * @deprecated */ export type SortByPropertyOptions = SortOptions; /** Options passed to {@link Wunderbaum.visitRows}. */ export interface VisitRowsOptions { /** Skip filtered nodes and children of collapsed nodes. @default false */ includeHidden?: boolean; /** Return the start node as first result. @default true */ includeSelf?: boolean; /** Traverse in opposite direction, i.e. bottom up. @default false */ reverse?: boolean; /** Start traversal at this node @default first (topmost) tree node */ start?: WunderbaumNode | null; /** Wrap around at last node and continue at the top, * until the start node is reached again @default false */ wrap?: boolean; } /* ----------------------------------------------------------------------------- * wb_ext_filter * ---------------------------------------------------------------------------*/ /** * Passed as tree option.filer.connect to configure automatic integration of * filter UI controls. @experimental */ export interface FilterConnectType { inputElem: string | HTMLInputElement | null; modeButton?: string | HTMLButtonElement | null; nextButton?: string | HTMLButtonElement | HTMLAnchorElement | null; prevButton?: string | HTMLButtonElement | HTMLAnchorElement | null; matchInfoElem?: string | HTMLElement | null; } /** * Passed as tree options to configure default filtering behavior. * * @see {@link Wunderbaum.filterNodes} * @see {@link FilterNodesOptions} */ export type FilterOptionsType = { /** * Element or selector of input controls and buttons for filter query strings. * @experimental * @since 0.13 * @default null */ connect?: null | FilterConnectType; /** * Re-apply last filter if lazy data is loaded * @default true */ autoApply?: boolean; } & FilterNodesOptions; /* ----------------------------------------------------------------------------- * wb_ext_edit * ---------------------------------------------------------------------------*/ /** * Note:
    * This options are used for renaming node titles.
    * There is also the `tree.change` event to handle modifying node data from * input controls that are embedded in grid cells. */ export type EditOptionsType = { /** * Used to debounce the `change` event handler for grid cells [ms]. * @default 100 */ debounce?: number; /** * Minimum number of characters required for node title input field. * @default 1 */ minlength?: number; /** * Maximum number of characters allowed for node title input field. * @default null; */ maxlength?: null | number; /** * Array of strings to determine which user input should trigger edit mode. * E.g. `["clickActive", "F2", "macEnter"]`:
    * 'clickActive': single click on active node title
    * 'F2': press F2 key
    * 'macEnter': press Enter (on macOS only)
    * Pass an empty array to disable edit mode. * @default [] */ trigger?: string[]; /** * Trim whitespace before saving a node title. * @default true */ trim?: boolean; /** * Select all text of a node title, so it can be overwritten by typing. * @default true */ select?: boolean; /** * Handle 'clickActive' only if last click is less than this ms old (0: always) * @default 1000 */ slowClickDelay?: number; /** * Permanently apply node title input validations (CSS and tooltip) on keydown. * @default true */ validity?: boolean; // --- Events --- /** * `beforeEdit(e)` may return an input HTML string. Otherwise use a default. * @category Callback */ beforeEdit?: null | ((e: WbNodeEventType) => boolean) | string; /** * * @category Callback */ edit?: | null | ((e: WbNodeEventType & { inputElem: HTMLInputElement }) => void); /** * * @category Callback */ apply?: | null | ((e: WbNodeEventType & { inputElem: HTMLInputElement }) => any) | Promise; }; /* ----------------------------------------------------------------------------- * wb_ext_dnd * ---------------------------------------------------------------------------*/ export type InsertNodeType = | "before" | "after" | "prependChild" | "appendChild"; export type DropEffectType = "none" | "copy" | "link" | "move"; export type DropEffectAllowedType = | "none" | "copy" | "copyLink" | "copyMove" | "link" | "linkMove" | "move" | "all"; export type DropRegionType = "over" | "before" | "after"; export type DropRegionTypeSet = Set; export type DropRegionTypeList = Array; // type AllowedDropRegionType = // | "after" // | "afterBefore" // // | "afterBeforeOver" // == all == true // | "afterOver" // | "all" // == true // | "before" // | "beforeOver" // | "none" // == false == "" == null // | "over"; // == "child" export interface DragEventType extends WbNodeEventType { /** The original event. */ event: DragEvent; /** The source node. */ node: WunderbaumNode; } export interface DropEventType extends WbNodeEventType { /** The original event. */ event: DragEvent; /** The target node. */ node: WunderbaumNode; /** The source node if any. */ sourceNode: WunderbaumNode; /** The DataTransfer object. */ dataTransfer: DataTransfer; } export type DndOptionsType = { /** * Expand nodes after n milliseconds of hovering * @default 1500 */ autoExpandMS?: 1500; // /** // * Additional offset for drop-marker with hitMode = "before"/"after" // * @default // */ // dropMarkerInsertOffsetX: -16; // /** // * Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) // * @default // */ // dropMarkerOffsetX: -24; // /** // * Root Container used for drop marker (could be a shadow root) // * (#1021 `document.body` is not available yet) // * @default // */ // dropMarkerParent: "body"; /** * true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed * @default false */ multiSource?: false; /** * Restrict the possible cursor shapes and modifier operations * (can also be set in the dragStart event) * @default "all" */ effectAllowed?: DropEffectAllowedType; /** * Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed. * Overidable in the dragEnter or dragOver event. * @default "move" */ dropEffectDefault?: DropEffectType; /** * Use opinionated heuristics to determine the dropEffect ('copy', 'link', or 'move') * based on `effectAllowed`, `dropEffectDefault`, and modifier keys. * This is recalculated before each dragEnter and dragOver event and can be * overridden there. * * @default true */ guessDropEffect: boolean; /** * Prevent dropping nodes from different Wunderbaum trees * @default false */ preventForeignNodes?: boolean; /** * Prevent dropping items on unloaded lazy Wunderbaum tree nodes * @default true */ preventLazyParents?: boolean; /** * Prevent dropping items other than Wunderbaum tree nodes * @default false */ preventNonNodes?: boolean; /** * Prevent dropping nodes on own descendants * @default true */ preventRecursion?: boolean; /** * Prevent dropping nodes under same direct parent * @default false */ preventSameParent?: boolean; /** * Prevent dropping nodes 'before self', etc. (move only) * @default true */ preventVoidMoves?: boolean; /** * Serialize Node Data to datatransfer object * @default true */ serializeClipboardData?: | boolean | ((nodeData: WbNodeData, node: WunderbaumNode) => string); /** * Enable auto-scrolling while dragging * @default true */ scroll?: boolean; /** * Active top/bottom margin in pixel * @default 20 */ scrollSensitivity?: 20; // /** // * Scroll events every N microseconds // * @default 50 // */ // scrollnterval: 50; /** * Pixel per event * @default 5 */ scrollSpeed?: 5; // /** // * Allow dragging of nodes to different IE windows // * @default false // */ // setTextTypeJson: boolean; /** * Optional callback passed to `toDict` on dragStart * @default null * @category Callback */ sourceCopyHook?: null; // Events (drag support) /** * Callback(sourceNode, data), return true, to enable dnd drag * @default null * @category Callback */ dragStart?: null | ((e: DragEventType) => boolean); /** * Callback(sourceNode, data) * @default null * @category Callback */ drag?: null | ((e: DragEventType) => void); /** * Callback(sourceNode, data) * @default null * @category Callback */ dragEnd?: null | ((e: DragEventType) => void); // Events (drop support) /** * Callback(targetNode, data), return true, to enable dnd drop * @default null * @category Callback */ dragEnter?: | null | (( e: DropEventType ) => DropRegionType | DropRegionTypeSet | DropRegionTypeList | boolean); /** * Callback(targetNode, data) * @default null * @category Callback */ dragOver?: null | ((e: DropEventType) => void); /** * Callback(targetNode, data), return false to prevent autoExpand * @default null * @category Callback */ dragExpand?: null | ((e: DropEventType) => boolean); /** * Callback(targetNode, data) * @default null * @category Callback */ drop?: | null | (( e: WbNodeEventType & { event: DragEvent; region: DropRegionType; suggestedDropMode: InsertNodeType; suggestedDropEffect: DropEffectType; sourceNode: WunderbaumNode; sourceNodeData: WbNodeData | null; } ) => void); /** * Callback(targetNode, data) * @default null * @category Callback */ dragLeave?: null | ((e: DropEventType) => void); }; /* ----------------------------------------------------------------------------- * wb_ext_grid * ---------------------------------------------------------------------------*/ export type GridOptionsType = object; /* ----------------------------------------------------------------------------- * wb_ext_keynav * ---------------------------------------------------------------------------*/ export type KeynavOptionsType = object; /* ----------------------------------------------------------------------------- * wb_ext_loger * ---------------------------------------------------------------------------*/ export type LoggerOptionsType = object; ================================================ FILE: src/util.ts ================================================ /*! * Wunderbaum - util * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ /** @module util */ import { DebouncedFunction, debounce, throttle } from "./debounce"; export { debounce, throttle }; /** Readable names for `MouseEvent.button` */ export const MOUSE_BUTTONS: { [key: number]: string } = { 0: "", 1: "left", 2: "middle", 3: "right", 4: "back", 5: "forward", }; export const MAX_INT = 9007199254740991; const userInfo = _getUserInfo(); /**True if the client is using a macOS platform. */ export const isMac = userInfo.isMac; const REX_HTML = /[&<>"'/]/g; // Escape those characters const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips const ENTITY_MAP: { [key: string]: string } = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/", }; export type NotPromise = T extends Promise ? never : T; export type FunctionType = (...args: any[]) => any; export type EventCallbackType = (e: Event) => boolean | void; type PromiseCallbackType = (val: any) => void; /** A generic error that can be thrown to indicate a validation error when * handling the `apply` event for a node title or the `change` event for a * grid cell. */ export class ValidationError extends Error { constructor(message: string) { super(message); this.name = "ValidationError"; } } /** * A ES6 Promise, that exposes the resolve()/reject() methods. * * TODO: See [Promise.withResolvers()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers#description) * , a proposed standard, but not yet implemented in any browser. */ export class Deferred { private thens: PromiseCallbackType[] = []; private catches: PromiseCallbackType[] = []; private status = ""; private resolvedValue: any; private rejectedError: any; constructor() {} resolve(value?: any) { if (this.status) { throw new Error("already settled"); } this.status = "resolved"; this.resolvedValue = value; this.thens.forEach((t) => t(value)); this.thens = []; // Avoid memleaks. } reject(error?: any) { if (this.status) { throw new Error("already settled"); } this.status = "rejected"; this.rejectedError = error; this.catches.forEach((c) => c(error)); this.catches = []; // Avoid memleaks. } then(cb: any) { if (status === "resolved") { cb(this.resolvedValue); } else { this.thens.unshift(cb); } } catch(cb: any) { if (this.status === "rejected") { cb(this.rejectedError); } else { this.catches.unshift(cb); } } promise() { return { then: this.then, catch: this.catch, }; } } /**Throw an `Error` if `cond` is falsey. */ export function assert(cond: any, msg: string) { if (!cond) { msg = msg || "Assertion failed."; throw new Error(msg); } } function _getUserInfo() { const nav = navigator; // const ua = nav.userAgentData; const res = { isMac: /Mac/.test(nav.platform), }; return res; } /** Run `callback` when document was loaded. */ export function documentReady(callback: () => void): void { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { callback(); } } /** Resolve when document was loaded. */ export function documentReadyPromise(): Promise { return new Promise((resolve) => { documentReady(resolve); }); } /** * Iterate over Object properties or array elements. * * @param obj `Object`, `Array` or null * @param callback called for every item. * `this` also contains the item. * Return `false` to stop the iteration. */ export function each( obj: any, callback: (index: number | string, item: any) => void | boolean ): any { if (obj == null) { // accept `null` or `undefined` return obj; } const length = obj.length; let i = 0; if (typeof length === "number") { for (; i < length; i++) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } else { for (const k in obj) { if (callback.call(obj[i], k, obj[k]) === false) { break; } } } return obj; } /** Shortcut for `throw new Error(msg)`. */ export function error(msg: string) { throw new Error(msg); } /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. */ export function escapeHtml(s: string): string { return ("" + s).replace(REX_HTML, function (s) { return ENTITY_MAP[s]; }); } // export function escapeRegExp(s: string) { // return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string // } /**Convert a regular expression string by escaping special characters (e.g. `"$"` -> `"\$"`) */ export function escapeRegex(s: string) { return ("" + s).replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); } /** Convert `<`, `>`, `"`, `'`, and `/` (but not `&`) to the equivalent entities. */ export function escapeTooltip(s: string): string { return ("" + s).replace(REX_TOOLTIP, function (s) { return ENTITY_MAP[s]; }); } /** TODO */ export function extractHtmlText(s: string) { if (s.indexOf(">") >= 0) { error("Not implemented"); // return $("
    ").html(s).text(); } return s; } /** * Read the value from an HTML input element. * * If a `` is passed, the first child input is used. * Depending on the target element type, `value` is interpreted accordingly. * For example for a checkbox, a value of true, false, or null is returned if * the element is checked, unchecked, or indeterminate. * For datetime input control a numerical value is assumed, etc. * * Common use case: store the new user input in a `change` event handler: * * ```ts * change: (e) => { * const tree = e.tree; * const node = e.node; * // Read the value from the input control that triggered the change event: * let value = tree.getValueFromElem(e.element); * // and store it to the node model (assuming the column id matches the property name) * node.data[e.info.colId] = value; * }, * ``` * @param elem `` or `` or ``. */ startEditTitle(node?: WunderbaumNode | null) { node = node ?? this.tree.getActiveNode(); const validity = this.getPluginOption("validity"); const select = this.getPluginOption("select"); if (!node) { return; } if (node.isStatusNode()) { node.logWarn("Cannot edit status node."); return; } this.tree.logDebug(`startEditTitle(node=${node})`); let inputHtml = node._callEvent("edit.beforeEdit"); if (inputHtml === false) { node.logDebug("beforeEdit canceled operation."); return; } // `beforeEdit(e)` may return an input HTML string. Otherwise use a default // (we also treat a `true` return value as 'use default'): if (inputHtml === true || !inputHtml) { const title = escapeHtml(node.title); let opt = this.getPluginOption("maxlength"); const maxlength = opt ? ` maxlength="${opt}"` : ""; opt = this.getPluginOption("minlength"); const minlength = opt ? ` minlength="${opt}"` : ""; const required = opt > 0 ? " required" : ""; inputHtml = ``; } const titleSpan = node .getColElem(0)! .querySelector(".wb-title") as HTMLSpanElement; titleSpan.innerHTML = inputHtml; const inputElem = titleSpan.firstElementChild as HTMLInputElement; if (validity) { // Permanently apply input validations (CSS and tooltip) inputElem.addEventListener("keydown", (e) => { inputElem.setCustomValidity(""); if (!inputElem.reportValidity()) { node!.logWarn(`Invalid input: '${inputElem.value}'`); } }); } inputElem.focus(); if (select) { inputElem.select(); } this.curEditNode = node; node._callEvent("edit.edit", { inputElem: inputElem, }); } /** * * @param apply * @returns */ stopEditTitle(apply: boolean) { return this._stopEditTitle(apply, {}); } /* * * @param apply * @param opts.canKeepOpen */ _stopEditTitle(apply: boolean, options: any) { options ??= {}; const focusElem = document.activeElement as HTMLInputElement; let newValue = focusElem ? getValueFromElem(focusElem) : null; const node = this.curEditNode; const forceClose = !!options.forceClose; const validity = this.getPluginOption("validity"); if (newValue && this.getPluginOption("trim")) { newValue = newValue.trim(); } if (!node) { // this.tree.logDebug("stopEditTitle: not in edit mode."); return; } node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue); if (apply && newValue !== null && newValue !== node.title) { const errMsg = focusElem.validationMessage; if (errMsg) { // input element's native validation failed throw new Error( `Input validation failed for "${newValue}": ${errMsg}.` ); } const colElem = node.getColElem(0)!; this._applyChange("edit.apply", node, colElem, focusElem, { oldValue: node.title, newValue: newValue, inputElem: focusElem, inputValid: focusElem.checkValidity(), }).then((value) => { const errMsg = focusElem.validationMessage; if (validity && errMsg && value !== false) { // Handler called 'inputElem.setCustomValidity()' to signal error throw new Error( `Edit apply validation failed for "${newValue}": ${errMsg}.` ); } // Discard the embedded `` // node.logDebug("applyChange:", value, forceClose) if (!forceClose && value === false) { // Keep open return; } node?.setTitle(newValue); // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. this.curEditNode?._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; this.tree.setFocus(); // restore focus that was in the input element }); // .catch((err) => { // node.logError(err); // }); // Trigger 'change' event for embedded `` // focusElem.blur(); } else { // Discard the embedded `` // NOTE: At least on Safari, this render call triggers a scroll event // probably because the focused input is replaced. this.curEditNode?._render({ preventScroll: true }); this.curEditNode = null; this.relatedNode = null; // We discarded the , so we have to acquire keyboard focus again this.tree.setFocus(); } } /** * Create a new child or sibling node and start edit mode. */ createNode( mode: InsertNodeType = "after", node?: WunderbaumNode | null, init?: string | WbNodeData ) { const tree = this.tree; node = node ?? (tree.getActiveNode() as WunderbaumNode); assert(node, "No node was passed, or no node is currently active."); // const validity = this.getPluginOption("validity"); mode = mode || "prependChild"; if (init == null) { init = { title: "" }; } else if (typeof init === "string") { init = { title: init }; } else { assert(isPlainObject(init), `Expected a plain object: ${init}`); } // Make sure node is expanded (and loaded) in 'child' mode if ( (mode === "prependChild" || mode === "appendChild") && node?.isExpandable(true) ) { node.setExpanded().then(() => { this.createNode(mode, node, init); }); return; } const newNode = node.addNode(init, mode); newNode.setClass("wb-edit-new"); this.relatedNode = node; // Don't filter new nodes: newNode.match = -1; newNode.makeVisible({ noAnimation: true }).then(() => { this.startEditTitle(newNode); }); } } ================================================ FILE: src/wb_ext_filter.ts ================================================ /*! * Wunderbaum - ext-filter * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { assert, elemFromSelector, escapeHtml, escapeRegex, extend, onEvent, } from "./util"; import { FilterConnectType, FilterNodesOptions, FilterOptionsType, NodeFilterCallback, NodeStatusType, } from "./types"; import { Wunderbaum } from "./wunderbaum"; import { WunderbaumNode } from "./wb_node"; import { WunderbaumExtension } from "./wb_extension_base"; import { debounce } from "./debounce"; const START_MARKER = "\uFFF7"; const END_MARKER = "\uFFF8"; const RE_START_MARKER = new RegExp(escapeRegex(START_MARKER), "g"); const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g"); export class FilterExtension extends WunderbaumExtension { public queryInput: HTMLInputElement | null = null; public prevButton: HTMLElement | HTMLAnchorElement | null = null; public nextButton: HTMLElement | HTMLAnchorElement | null = null; public modeButton: HTMLButtonElement | null = null; public matchInfoElem: HTMLElement | null = null; public lastFilterArgs: IArguments | null = null; constructor(tree: Wunderbaum) { super(tree, "filter", { autoApply: true, // Re-apply last filter if lazy data is loaded autoExpand: false, // Expand all branches that contain matches while filtered matchBranch: false, // Whether to implicitly match all children of matched nodes connect: null, // Element or selector of an input control for filter query strings fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' hideExpanders: false, // Hide expanders if all child nodes are hidden by filter highlight: true, // Highlight matches by wrapping inside tags leavesOnly: false, // Match end nodes only mode: "dim", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) noData: true, // Display a 'no data' status node if result is empty }); } init() { super.init(); const connect: FilterConnectType = this.getPluginOption("connect"); if (connect) { this._connectControls(); } } setPluginOption(name: string, value: any): void { super.setPluginOption(name, value); switch (name) { case "mode": this.tree.filterMode = value === "hide" ? "hide" : value === "mark" ? "mark" : "dim"; this.tree.updateFilter(); break; } } _updatedConnectedControls() { const filterActive = this.tree.filterMode !== null; const activeNode = this.tree.getActiveNode(); const matchCount = filterActive ? this.countMatches() : 0; const strings = this.treeOpts.strings; let matchIdx: string | number = "?"; if (this.matchInfoElem) { if (filterActive) { let info; if (matchCount === 0) { info = strings.noMatch; } else if (activeNode && activeNode.match! >= 1) { matchIdx = activeNode.match ?? "?"; info = strings.matchIndex; } else { info = strings.queryResult; } info = info .replace("${count}", this.tree.count().toLocaleString()) .replace("${match}", "" + matchIdx) .replace("${matches}", matchCount.toLocaleString()); this.matchInfoElem.textContent = info; } else { this.matchInfoElem.textContent = ""; } } if (this.nextButton instanceof HTMLButtonElement) { this.nextButton.disabled = !matchCount; } if (this.prevButton instanceof HTMLButtonElement) { this.prevButton.disabled = !matchCount; } if (this.modeButton) { this.modeButton.disabled = !filterActive; this.modeButton.classList.toggle( "wb-filter-hide", this.tree.filterMode === "hide" ); } } _connectControls() { const tree = this.tree; const connect: FilterConnectType = this.getPluginOption("connect"); if (!connect) { return; } this.queryInput = elemFromSelector(connect.inputElem); if (!this.queryInput) { throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`); } this.prevButton = elemFromSelector(connect.prevButton!); this.nextButton = elemFromSelector(connect.nextButton!); this.modeButton = elemFromSelector(connect.modeButton!); this.matchInfoElem = elemFromSelector(connect.matchInfoElem!); if (this.prevButton) { onEvent(this.prevButton, "click", () => { tree.findRelatedNode( tree.getActiveNode() || tree.getFirstChild()!, "prevMatch" ); this._updatedConnectedControls(); }); } if (this.nextButton) { onEvent(this.nextButton, "click", () => { tree.findRelatedNode( tree.getActiveNode() || tree.getFirstChild()!, "nextMatch" ); this._updatedConnectedControls(); }); } if (this.modeButton) { onEvent(this.modeButton, "click", (e) => { if (!this.tree.filterMode) { return; } this.setPluginOption( "mode", tree.filterMode === "dim" ? "hide" : "dim" ); }); } onEvent( this.queryInput, "input", debounce((e) => { this.filterNodes(this.queryInput!.value.trim(), {}); }, 700) ); this._updatedConnectedControls(); } _applyFilterNoUpdate( filter: string | RegExp | NodeFilterCallback, _opts: FilterNodesOptions ): number { return this.tree.runWithDeferredUpdate(() => { return this._applyFilterImpl(filter, _opts); }); } _applyFilterImpl( filter: string | RegExp | NodeFilterCallback, _opts: FilterNodesOptions ): number { let //temp, count = 0; const start = Date.now(); const tree = this.tree; const treeOpts = tree.options; const prevAutoCollapse = treeOpts.autoCollapse; // Use default options from `tree.options.filter`, but allow to override them const opts: FilterOptionsType = extend({}, treeOpts.filter, _opts); const hideMode = opts.mode === "hide"; const matchBranch = !!opts.matchBranch; const leavesOnly = !!opts.leavesOnly && !matchBranch; let filterRegExp: RegExp; let highlightRegExp: RegExp; // Default to 'match title substring (case insensitive)' if (typeof filter === "string" || filter instanceof RegExp) { if (filter === "") { tree.logInfo( "Passing an empty string as a filter is handled as clearFilter()." ); this.clearFilter(); return 0; } if (opts.fuzzy) { assert(typeof filter === "string", "fuzzy filter must be a string"); // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905 // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed // and http://www.dustindiaz.com/autocomplete-fuzzy-matching const matchReString = (filter) .split("") // Escaping the `filter` will not work because, // it gets further split into individual characters. So, // escape each character after splitting .map(escapeRegex) .reduce(function (a, b) { // create capture groups for parts that comes before // the character return a + "([^" + b + "]*)" + b; }, ""); filterRegExp = new RegExp(matchReString, "i"); // highlightRegExp = new RegExp(escapeRegex(filter), "gi"); } else if (filter instanceof RegExp) { filterRegExp = filter; highlightRegExp = filter; } else { const matchReString = escapeRegex(filter); // make sure a '.' is treated literally filterRegExp = new RegExp(matchReString, "i"); highlightRegExp = new RegExp(matchReString, "gi"); } tree.logDebug(`Filtering nodes by '${filterRegExp}'`); // const re = new RegExp(match, "i"); // const reHighlight = new RegExp(escapeRegex(filter), "gi"); filter = (node: WunderbaumNode) => { if (!node.title) { return false; } // let text = escapeTitles ? node.title : extractHtmlText(node.title); const text = node.title; // `.match` instead of `.test` to get the capture groups // const res = text.match(filterRegExp); const res = filterRegExp.exec(text); if (res && opts.highlight) { let highlightString: string; if (opts.fuzzy) { highlightString = _markFuzzyMatchedChars(text, res, true); } else { // #740: we must not apply the marks to escaped entity names, e.g. `"` // Use some exotic characters to mark matches: highlightString = text.replace(highlightRegExp, function (s) { return START_MARKER + s + END_MARKER; }); } // now we can escape the title... node.titleWithHighlight = escapeHtml(highlightString) // ... and finally insert the desired `` tags .replace(RE_START_MARKER, "") .replace(RE_END_MARTKER, ""); } return !!res; }; } tree.filterMode = opts.mode ?? "dim"; // eslint-disable-next-line prefer-rest-params this.lastFilterArgs = arguments; tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode); tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim"); tree.element.classList.toggle( "wb-ext-filter-hide-expanders", !!opts.hideExpanders ); // Reset current filter tree.root.subMatchCount = 0; tree.visit((node) => { delete node.match; delete node.titleWithHighlight; node.subMatchCount = 0; }); tree.setStatus(NodeStatusType.ok); // Adjust node.hide, .match, and .subMatchCount properties treeOpts.autoCollapse = false; // #528 tree.visit((node) => { if (leavesOnly && node.children != null) { return; } let res = (filter)(node); if (res === "skip") { node.visit(function (c) { c.match = undefined; }, true); return "skip"; } let matchedByBranch = false; if ((matchBranch || res === "branch") && node.parent.match) { res = true; matchedByBranch = true; } if (res) { count++; node.match = count; node.visitParents((p) => { if (p !== node) { p.subMatchCount! += 1; } // Expand match (unless this is no real match, but only a node in a matched branch) if (opts.autoExpand && !matchedByBranch && !p.expanded) { p.setExpanded(true, { noAnimation: true, noEvents: true, }); p._filterAutoExpanded = true; } }, true); } }); treeOpts.autoCollapse = prevAutoCollapse; if (count === 0 && opts.noData && hideMode) { if (typeof opts.noData === "string") { tree.root.setStatus(NodeStatusType.noData, { message: opts.noData }); } else { tree.root.setStatus(NodeStatusType.noData); } } // Redraw whole tree tree.logDebug( `Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.` ); this._updatedConnectedControls(); return count; } /** * [ext-filter] Dim or hide nodes. */ filterNodes( filter: string | RegExp | NodeFilterCallback, options: FilterNodesOptions ): number { return this._applyFilterNoUpdate(filter, options); } /** * [ext-filter] Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches( filter: string | NodeFilterCallback, options: FilterNodesOptions ) { assert( options.matchBranch === undefined, "filterBranches() is deprecated." ); this.tree.logDeprecate("filterBranches()", { since: "0.9.0", hint: "Use `filterNodes` instead and set `options.matchBranch: true`", }); options.matchBranch = true; return this._applyFilterNoUpdate(filter, options); } /** * [ext-filter] Return the number of matched nodes. */ countMatches(): number { let n = 0; this.tree.visit((node) => { if (node.match && !node.statusNodeType) { n++; } }); return n; } /** * [ext-filter] Re-apply current filter. */ updateFilter() { const tree = this.tree; if ( tree.filterMode && this.lastFilterArgs && tree.options.filter?.autoApply ) { // eslint-disable-next-line prefer-spread this._applyFilterNoUpdate.apply(this, this.lastFilterArgs); } else { tree.logWarn("updateFilter(): no filter active."); } this._updatedConnectedControls(); } /** * [ext-filter] Reset the filter. */ clearFilter() { const tree = this.tree; tree.enableUpdate(false); tree.setStatus(NodeStatusType.ok); // we also counted root node's subMatchCount delete tree.root.match; delete tree.root.subMatchCount; tree.visit((node) => { delete node.match; delete node.subMatchCount; delete node.titleWithHighlight; if (node._filterAutoExpanded && node.expanded) { node.setExpanded(false, { noAnimation: true, noEvents: true, }); } delete node._filterAutoExpanded; }); tree.filterMode = null; this.lastFilterArgs = null; tree.element.classList.remove( // "wb-ext-filter", "wb-ext-filter-dim", "wb-ext-filter-hide" ); this._updatedConnectedControls(); tree.enableUpdate(true); } } /** * @description Marks the matching characters of `text` either by `mark` or * by exotic*Chars (if `escapeTitles` is `true`) based on `matches` * which is an array of matching groups. * @param {string} text * @param {RegExpMatchArray} matches */ function _markFuzzyMatchedChars( text: string, matches: RegExpMatchArray, escapeTitles = true ) { const matchingIndices = []; // get the indices of matched characters (Iterate through `RegExpMatchArray`) for ( let _matchingArrIdx = 1; _matchingArrIdx < matches.length; _matchingArrIdx++ ) { const _mIdx: number = // get matching char index by cumulatively adding // the matched group length matches[_matchingArrIdx].length + (_matchingArrIdx === 1 ? 0 : 1) + (matchingIndices[matchingIndices.length - 1] || 0); matchingIndices.push(_mIdx); } // Map each `text` char to its position and store in `textPoses`. const textPoses = text.split(""); if (escapeTitles) { // If escaping the title, then wrap the matching char within exotic chars matchingIndices.forEach(function (v) { textPoses[v] = START_MARKER + textPoses[v] + END_MARKER; }); } else { // Otherwise, Wrap the matching chars within `mark`. matchingIndices.forEach(function (v) { textPoses[v] = "" + textPoses[v] + ""; }); } // Join back the modified `textPoses` to create final highlight markup. return textPoses.join(""); } ================================================ FILE: src/wb_ext_grid.ts ================================================ /*! * Wunderbaum - ext-grid * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { Wunderbaum } from "./wunderbaum"; import { WunderbaumExtension } from "./wb_extension_base"; import { DragCallbackArgType, DragObserver } from "./drag_observer"; import { ChangeType, ColumnDefinition, GridOptionsType } from "./types"; import { DEFAULT_MIN_COL_WIDTH } from "./common"; import { toBool, toPixel } from "./util"; export class GridExtension extends WunderbaumExtension { protected observer: DragObserver; constructor(tree: Wunderbaum) { super(tree, "grid", { // throttle: 200, }); this.observer = new DragObserver({ root: window.document, selector: "span.wb-col-resizer-active", thresh: 4, // throttle: 400, dragstart: (e) => { const info = Wunderbaum.getEventInfo(e.startEvent); const colDef = info.colDef!; const allow = colDef && this.tree.element.contains(e.dragElem) && toBool(colDef.resizable, tree.options.columnsResizable, false); // this.tree.log("dragstart", colDef, e, info); this.tree.element.classList.toggle("wb-col-resizing", !!allow); info.colElem!.classList.toggle("wb-col-resizing", !!allow); // We start dagging, so we remember the actual width in *pixels* // (which may be 'auto' or '100%'). // Since we we re-create the markup on each update, we also cannot store // the original event or DOM element, but only the colDef object. if (allow) { // Store initial target column infos in customData e.customData.colDef = colDef; e.customData.orgCustomWidthPx = colDef.customWidthPx; const curWidthPx = Number.parseInt(info.colElem!.style.width, 10); e.customData.orgWidthPx = curWidthPx; // Set custom width to current width, so that we can modify it colDef.customWidthPx = curWidthPx; // this.tree.log( // `dragstart customWidthPx=${colDef.customWidthPx}`, // e, // info // ); this.tree.update(ChangeType.colStructure); // this.tree.log( // `dragstart 2 customWidthPx=${colDef.customWidthPx}`, // e, // info // ); } return allow; }, drag: (e) => { // TODO: throttle return this.handleDrag(e); }, dragstop: (e) => { return this.handleDrag(e); }, }); } init() { super.init(); } /** * Handles drag and sragstop events for column resizing. */ protected handleDrag(e: DragCallbackArgType): void { const custom = e.customData; const colDef = custom.colDef!; // this.tree.log(`${e.type} (dx=${e.dx})`, e, info); if (e.type === "dragstop" || e.type === "drag") { this.tree.element.classList.remove("wb-col-resizing"); // info.colElem!.classList.remove("wb-col-resizing"); if (e.apply || e.type === "drag") { const minWidth = toPixel(colDef.minWidth, DEFAULT_MIN_COL_WIDTH); const newWidth = Math.max(minWidth, custom.orgWidthPx + e.dx); colDef.customWidthPx = newWidth; // this.tree.log( // `${e.type} minWidth=${minWidth}, newWidth=${newWidth}`, // colDef // ); } else { // Drag was cancelled this.tree.log("Column resize cancelled", e); colDef.customWidthPx = custom.orgCustomWidthPx; // Restore original width or undefined } this.tree.update(ChangeType.colStructure); } } } ================================================ FILE: src/wb_ext_keynav.ts ================================================ /*! * Wunderbaum - ext-keynav * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { KeynavOptionsType, NavModeEnum } from "./types"; import { eventToString } from "./util"; import { Wunderbaum } from "./wunderbaum"; import { WunderbaumNode } from "./wb_node"; import { WunderbaumExtension } from "./wb_extension_base"; const QUICKSEARCH_DELAY = 500; export class KeynavExtension extends WunderbaumExtension { constructor(tree: Wunderbaum) { super(tree, "keynav", {}); } protected _getEmbeddedInputElem(elem: any): HTMLInputElement | null { let input = null; if (elem && elem.type != null) { input = elem; } else { // ,[contenteditable] const ace = this.tree.getActiveColElem()?.querySelector("input,select"); if (ace) { input = ace as HTMLInputElement; } } return input; } // /* Return the current cell's embedded input that has keyboard focus. */ // protected _getFocusedInputElem(): HTMLInputElement | null { // const ace = this.tree // .getActiveColElem() // ?.querySelector("input:focus,select:focus"); // return ace || null; // } /* Return true if the current cell's embedded input has keyboard focus. */ protected _isCurInputFocused(): boolean { const ace = this.tree .getActiveColElem() ?.querySelector("input:focus,select:focus"); return !!ace; } onKeyEvent(data: any): boolean | undefined { const event = data.event; const tree = this.tree; const opts = data.options; const activate = !event.ctrlKey || opts.autoActivate; const curInput = this._getEmbeddedInputElem(event.target); const inputHasFocus = curInput && this._isCurInputFocused(); const navModeOption = opts.navigationModeOption as NavModeEnum; let focusNode, eventName = eventToString(event), node = data.node as WunderbaumNode, handled = true; // tree.log(`onKeyEvent: ${eventName}, curInput`, curInput); if (!tree.isEnabled()) { // tree.logDebug(`onKeyEvent ignored for disabled tree: ${eventName}`); return false; } // Let callback prevent default processing if (tree._callEvent("keydown", data) === false) { return false; } // Let ext-edit trigger editing if (tree._callMethod("edit._preprocessKeyEvent", data) === false) { return false; } // Set focus to active (or first node) if no other node has the focus yet if (!node) { const currentNode = tree.getFocusNode() || tree.getActiveNode(); const firstNode = tree.getFirstChild(); if (!currentNode && firstNode && eventName === "ArrowDown") { firstNode.logInfo("Keydown: activate first node."); firstNode.setActive(); return; } focusNode = currentNode || firstNode; if (focusNode) { focusNode.setFocus(); node = tree.getFocusNode()!; node.logInfo("Keydown: force focus on active node."); } } const isColspan = node.isColspan(); if (tree.isRowNav()) { // ----------------------------------------------------------------------- // --- Row Mode --- // ----------------------------------------------------------------------- if (inputHasFocus) { // If editing an embedded input control, let the control handle all // keys. Only Enter and Escape should apply / discard, but keep the // keyboard focus. switch (eventName) { case "Enter": curInput.blur(); tree.setFocus(); break; case "Escape": node._render(); tree.setFocus(); break; } return; } // --- Quick-Search if ( opts.quicksearch && eventName.length === 1 && /^\w$/.test(eventName) && !curInput ) { // Allow to search for longer streaks if typed in quickly const stamp = Date.now(); if (stamp - tree.lastQuicksearchTime > QUICKSEARCH_DELAY) { tree.lastQuicksearchTerm = ""; } tree.lastQuicksearchTime = stamp; tree.lastQuicksearchTerm += eventName; const matchNode = tree.findNextNode( tree.lastQuicksearchTerm, tree.getActiveNode() ); if (matchNode) { matchNode.setActive(true, { event: event }); } event.preventDefault(); return; } // Pre-Evaluate expand/collapse action for LEFT/RIGHT switch (eventName) { case "Enter": if (node.isActive()) { if (node.isExpanded()) { eventName = "Subtract"; // callapse } else if (node.isExpandable(true)) { eventName = "Add"; // expand } } break; case "ArrowLeft": if (node.expanded) { eventName = "Subtract"; // collapse } break; case "ArrowRight": if (!node.expanded && node.isExpandable(true)) { eventName = "Add"; // expand } else if ( navModeOption === NavModeEnum.startCell || navModeOption === NavModeEnum.startRow ) { event.preventDefault(); tree.setCellNav(); return false; } break; } // Standard navigation (row mode) switch (eventName) { case "+": case "Add": // case "=": // 187: '+' @ Chrome, Safari node.setExpanded(true); break; case "-": case "Subtract": node.setExpanded(false); break; case " ": // Space // if (node.isPagingNode()) { // tree._triggerNodeEvent("clickPaging", ctx, event); // } else if (node.getOption("checkbox")) { node.toggleSelected(); } else { node.setActive(true, { event: event }); } break; case "Enter": node.setActive(true, { event: event }); break; case "ArrowDown": case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "Backspace": case "End": case "Home": case "Control+End": case "Control+Home": case "Meta+ArrowDown": case "Meta+ArrowUp": case "PageDown": case "PageUp": node.navigate(eventName, { activate: activate, event: event }); break; default: handled = false; } } else { // ----------------------------------------------------------------------- // --- Cell Mode --- // ----------------------------------------------------------------------- // // Standard navigation (cell mode) // if (isCellEditMode && INPUT_BREAKOUT_KEYS.has(eventName)) { // } // const curInput = this._getEmbeddedInputElem(null); const curInputType = curInput ? curInput.type || curInput.tagName : ""; // const inputHasFocus = curInput && this._isCurInputFocused(); const inputCanFocus = curInput && curInputType !== "checkbox"; if (inputHasFocus) { if (eventName === "Escape") { node.logDebug(`Reset focused input on Escape`); // Discard changes and reset input validation state curInput.setCustomValidity(""); node._render(); // Keep cell-nav mode tree.setFocus(); tree.setColumn(tree.activeColIdx); return; // } else if (!INPUT_BREAKOUT_KEYS.has(eventName)) { } else if (eventName !== "Enter") { if (curInput && curInput.checkValidity && !curInput.checkValidity()) { // Invalid input: ignore all keys except Enter and Escape node.logDebug(`Ignored ${eventName} inside invalid input`); return false; } // Let current `` handle it node.logDebug(`Ignored ${eventName} inside focused input`); return; } // const curInputType = curInput.type || curInput.tagName; // const breakoutKeys = INPUT_KEYS[curInputType]; // if (!breakoutKeys.includes(eventName)) { // node.logDebug(`Ignored ${eventName} inside ${curInputType} input`); // return; // } } else if (curInput) { // On a cell that has an embedded, unfocused if (eventName.length === 1 && inputCanFocus) { // Typing a single char curInput.focus(); curInput.value = ""; node.logDebug(`Focus input: ${eventName}`); return false; } } if (eventName === "Tab") { eventName = "ArrowRight"; handled = true; } else if (eventName === "Shift+Tab") { eventName = tree.activeColIdx > 0 ? "ArrowLeft" : ""; handled = true; } switch (eventName) { case "+": case "Add": // case "=": // 187: '+' @ Chrome, Safari node.setExpanded(true); break; case "-": case "Subtract": node.setExpanded(false); break; case " ": // Space if (tree.activeColIdx === 0 && node.getOption("checkbox")) { node.toggleSelected(); handled = true; } else if (curInput && curInputType === "checkbox") { curInput.click(); // toggleCheckbox(curInput) // new Event("change") // curInput.change handled = true; } break; case "F2": if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Enter": tree.setFocus(); // Blur prev. input if any if ((tree.activeColIdx === 0 || isColspan) && node.isExpandable()) { node.setExpanded(!node.isExpanded()); handled = true; } else if (curInput && !inputHasFocus && inputCanFocus) { curInput.focus(); handled = true; } break; case "Escape": tree.setFocus(); // Blur prev. input if any node.log(`keynav: focus tree...`); if (tree.isCellNav() && navModeOption !== NavModeEnum.cell) { node.log(`keynav: setCellNav(false)`); tree.setCellNav(false); // row-nav mode tree.setFocus(); // handled = true; } break; case "ArrowLeft": tree.setFocus(); // Blur prev. input if any if (isColspan && node.isExpanded()) { node.setExpanded(false); } else if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(tree.activeColIdx - 1); } else if (navModeOption !== NavModeEnum.cell) { tree.setCellNav(false); // row-nav mode } handled = true; break; case "ArrowRight": tree.setFocus(); // Blur prev. input if any if (isColspan && !node.isExpanded()) { node.setExpanded(); } else if ( !isColspan && tree.activeColIdx < tree.columns.length - 1 ) { tree.setColumn(tree.activeColIdx + 1); } handled = true; break; case "Home": // Generated by [Fn] + ArrowLeft on Mac // case "Meta+ArrowLeft": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx > 0) { tree.setColumn(0); } handled = true; break; case "End": // Generated by [Fn] + ArrowRight on Mac // case "Meta+ArrowRight": tree.setFocus(); // Blur prev. input if any if (!isColspan && tree.activeColIdx < tree.columns.length - 1) { tree.setColumn(tree.columns.length - 1); } handled = true; break; case "ArrowDown": case "ArrowUp": case "Backspace": case "Control+End": // Generated by Control + [Fn] + ArrowRight on Mac case "Control+Home": // Generated by Control + [Fn] + Arrowleft on Mac case "Meta+ArrowDown": // [⌘] + ArrowDown on Mac case "Meta+ArrowUp": // [⌘] + ArrowUp on Mac case "PageDown": // Generated by [Fn] + ArrowDown on Mac case "PageUp": // Generated by [Fn] + ArrowUp on Mac node.navigate(eventName, { activate: activate, event: event }); // if (isCellEditMode) { // this._getEmbeddedInputElem(null, true); // set focus to input // } handled = true; break; default: handled = false; } } if (handled) { event.preventDefault(); } return; } } ================================================ FILE: src/wb_ext_logger.ts ================================================ /*! * Wunderbaum - ext-logger * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { LoggerOptionsType } from "./types"; import { overrideMethod } from "./util"; import { WunderbaumExtension } from "./wb_extension_base"; import { Wunderbaum } from "./wunderbaum"; export class LoggerExtension extends WunderbaumExtension { readonly prefix: string; protected ignoreEvents = new Set([ "iconBadge", // "enhanceTitle", "render", "discard", ]); constructor(tree: Wunderbaum) { super(tree, "logger", {}); this.prefix = tree + ".ext-logger"; } init() { const tree = this.tree; // this.ignoreEvents.add(); if (tree.getOption("debugLevel") >= 4) { // const self = this; const ignoreEvents = this.ignoreEvents; const prefix = this.prefix; overrideMethod(tree, "callEvent", function (name, extra) { /* eslint-disable prefer-rest-params */ if (ignoreEvents.has(name)) { return (tree)._superApply(arguments); } const start = Date.now(); const res = (tree)._superApply(arguments); tree.logDebug( `${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`, arguments[1] ); return res; }); } } onKeyEvent(data: any): boolean | undefined { // this.tree.logInfo("onKeyEvent", eventToString(data.event), data); this.tree.logDebug(`${this.prefix}: onKeyEvent()`, data); return; } } ================================================ FILE: src/wb_extension_base.ts ================================================ /*! * Wunderbaum - wb_extension_base * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import * as util from "./util"; import { DndExtension } from "./wb_ext_dnd"; import { EditExtension } from "./wb_ext_edit"; import { FilterExtension } from "./wb_ext_filter"; import { GridExtension } from "./wb_ext_grid"; import { KeynavExtension } from "./wb_ext_keynav"; import { LoggerExtension } from "./wb_ext_logger"; import { WunderbaumOptions } from "./wb_options"; import { Wunderbaum } from "./wunderbaum"; export type ExtensionsDict = { dnd: DndExtension; edit: EditExtension; filter: FilterExtension; grid: GridExtension; keynav: KeynavExtension; logger: LoggerExtension; [key: string]: WunderbaumExtension; }; export abstract class WunderbaumExtension { public enabled = true; readonly id: string; readonly tree: Wunderbaum; readonly treeOpts: WunderbaumOptions; readonly extensionOpts: any; constructor(tree: Wunderbaum, id: string, defaults: TOptions) { this.tree = tree; this.id = id; this.treeOpts = tree.options; const opts = tree.options as any; if ((this.treeOpts)[id] === undefined) { opts[id] = this.extensionOpts = util.extend({}, defaults); } else { // TODO: do we break existing object instance references here? this.extensionOpts = util.extend({}, defaults, opts[id]); opts[id] = this.extensionOpts; } this.enabled = this.getPluginOption("enabled", true); } /** Called on tree (re)init after all extensions are added, but before loading.*/ init() { this.tree.element.classList.add("wb-ext-" + this.id); } // protected callEvent(type: string, extra?: any): any { // let func = this.extensionOpts[type]; // if (func) { // return func.call( // this.tree, // util.extend( // { // event: this.id + "." + type, // }, // extra // ) // ); // } // } getPluginOption(name: string, defaultValue?: any): any { return this.extensionOpts[name] ?? defaultValue; } setPluginOption(name: string, value: any): void { this.extensionOpts[name] = value; } setEnabled(flag = true) { return this.setPluginOption("enabled", !!flag); // this.enabled = !!flag; } onKeyEvent(data: any): boolean | undefined { return; } onRender(data: any): boolean | undefined { return; } } ================================================ FILE: src/wb_node.ts ================================================ /*! * Wunderbaum - wunderbaum_node * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import * as util from "./util"; import { Wunderbaum } from "./wunderbaum"; import { AddChildrenOptions, ApplyCommandOptions, ApplyCommandType, ChangeType, CheckboxOption, ColumnEventInfoMap, ExpandAllOptions, IconOption, InsertNodeType, MakeVisibleOptions, MatcherCallback, NavigateOptions, NavigationType, NodeAnyCallback, NodeStatusType, NodeStringCallback, NodeToDictCallback, NodeVisitCallback, NodeVisitResponse, RenderOptions, ResetOrderOptions, ScrollIntoViewOptions, SetActiveOptions, SetExpandedOptions, SetSelectedOptions, SetStatusOptions, SortByPropertyOptions, SortCallback, SortOptions, SourceType, TooltipOption, TristateType, WbNodeData, } from "./types"; import { decompressSourceData, ICON_WIDTH, KEY_TO_NAVIGATION_MAP, makeNodeTitleMatcher, NODE_TYPE_FOLDER, nodeTitleSorter, RESERVED_TREE_SOURCE_KEYS, TEST_FILE_PATH, TEST_HTML, TITLE_SPAN_PAD_Y, } from "./common"; import { Deferred } from "./deferred"; /** WunderbaumNode properties that can be passed with source data. * (Any other source properties will be stored as `node.data.PROP`.) */ const NODE_PROPS = new Set([ "checkbox", "classes", "expanded", "icon", "iconTooltip", "key", "lazy", "_partsel", "radiogroup", "refKey", "selected", "statusNodeType", "title", "tooltip", "type", "unselectable", ]); /** WunderbaumNode properties that will be returned by `node.toDict()`.) */ const NODE_DICT_PROPS = new Set(NODE_PROPS); NODE_DICT_PROPS.delete("_partsel"); NODE_DICT_PROPS.delete("unselectable"); // /** Node properties that are of type bool (or boolean & string). // * When parsing, we accept 0 for false and 1 for true for better JSON compression. // */ // export const NODE_BOOL_PROPS: Set = new Set([ // "checkbox", // "colspan", // "expanded", // "icon", // "iconTooltip", // "radiogroup", // "selected", // "tooltip", // "unselectable", // ]); /** * A single tree node. * * **NOTE:**
    * Generally you should not modify properties directly, since this may break * the internal bookkeeping. */ export class WunderbaumNode { static sequence = 0; /** Reference to owning tree. */ public tree: Wunderbaum; /** Parent node (null for the invisible root node `tree.root`). */ public parent: WunderbaumNode; /** Name of the node. * @see Use {@link setTitle} to modify. */ public title: string; /** Unique key. Passed with constructor or defaults to `SEQUENCE`. * @see Use {@link setKey} to modify. */ public readonly key: string; /** Reference key. Unlike {@link key}, a `refKey` may occur multiple * times within a tree (in this case we have 'clone nodes'). * @see Use {@link setKey} to modify. */ public readonly refKey: string | undefined = undefined; /** * Array of child nodes (null for leaf nodes). * For lazy nodes, this is `null` or ùndefined` until the children are loaded * and leaf nodes may be `[]` (empty array). * @see {@link hasChildren}, {@link addChildren}, {@link lazy}. */ public children: WunderbaumNode[] | null = null; /** Render a checkbox or radio button @see {@link selected}. */ public checkbox?: CheckboxOption; /** If true, this node's children are considerd radio buttons. * @see {@link isRadio}. */ public radiogroup?: boolean; /** If true, (in grid mode) no cells are rendered, except for the node title.*/ public colspan?: boolean; /** Icon definition. */ public icon?: IconOption; /** Lazy loading flag. * @see {@link isLazy}, {@link isLoaded}, {@link isUnloaded}. */ public lazy?: boolean; /** Expansion state. * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */ public expanded?: boolean; /** Selection state. * @see {@link isSelected}, {@link setSelected}, {@link toggleSelected}. */ public selected?: boolean; public unselectable?: boolean; /** Node type (used for styling). * @see {@link Wunderbaum.types}. */ public type?: string; /** Tooltip definition (`true`: use node's title). */ public tooltip?: TooltipOption; /** Icon tooltip definition (`true`: use node's title). */ public iconTooltip?: TooltipOption; /** Additional classes added to `div.wb-row`. * @see {@link hasClass}, {@link setClass}. */ public classes: Set | null = null; //new Set(); /** Custom data that was passed to the constructor */ public data: any = {}; // --- Node Status --- public statusNodeType?: NodeStatusType; _isLoading = false; _requestId = 0; _errorInfo: any | null = null; _partsel = false; _partload = false; // --- FILTER --- /** * > 0 if matched (-1 to keep system nodes visible); * Added and removed by filter code. */ public match?: number; public subMatchCount?: number = 0; // public subMatchBadge?: HTMLElement; /** @internal */ public titleWithHighlight?: string; public _filterAutoExpanded?: boolean; _rowIdx: number | undefined = 0; _rowElem: HTMLDivElement | undefined = undefined; constructor(tree: Wunderbaum, parent: WunderbaumNode, data: WbNodeData) { util.assert(!parent || parent.tree === tree, `Invalid parent: ${parent}`); util.assert(!data.children, "'children' not allowed here"); this.tree = tree; this.parent = parent; this.key = tree._calculateKey(data, parent); this.title = "" + (data.title ?? "<" + this.key + ">"); this.expanded = !!data.expanded; this.lazy = !!data.lazy; // We set the following node properties only if a matching data value is // passed data.refKey != null ? (this.refKey = "" + data.refKey) : 0; data.type != null ? (this.type = "" + data.type) : 0; data.icon != null ? (this.icon = util.intToBool(data.icon)) : 0; data.tooltip != null ? (this.tooltip = util.intToBool(data.tooltip)) : 0; data.iconTooltip != null ? (this.iconTooltip = util.intToBool(data.iconTooltip)) : 0; data.statusNodeType != null ? (this.statusNodeType = ("" + data.statusNodeType) as NodeStatusType) : 0; data.colspan != null ? (this.colspan = !!data.colspan) : 0; // Selection data.checkbox != null ? (this.checkbox = util.intToBool(data.checkbox) as CheckboxOption) : 0; data.radiogroup != null ? (this.radiogroup = !!data.radiogroup) : 0; data.selected != null ? (this.selected = !!data.selected) : 0; data.unselectable != null ? (this.unselectable = !!data.unselectable) : 0; if (data.classes) { this.setClass(data.classes); } // Store custom fields as `node.data` for (const [key, value] of Object.entries(data)) { if (!NODE_PROPS.has(key)) { this.data[key] = value; } } if (parent && !this.statusNodeType) { // Don't register root node or status nodes tree._registerNode(this); } } /** * Return readable string representation for this instance. * @internal */ toString() { return `WunderbaumNode@${this.key}<'${this.title}'>`; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link WunderbaumNode.visit}. * * Example: * ```js * for(const n of node) { * ... * } * ``` */ *[Symbol.iterator](): IterableIterator { // let node: WunderbaumNode | null = this; const cl = this.children; if (cl) { for (let i = 0, l = cl.length; i < l; i++) { const n = cl[i]; yield n; if (n.children) { yield* n; } } // Slower: // for (let node of this.children) { // yield node; // yield* node : 0; // } } } // /** Return an option value. */ // protected _getOpt( // name: string, // nodeObject: any = null, // treeOptions: any = null, // defaultValue: any = null // ): any { // return evalOption( // name, // this, // nodeObject || this, // treeOptions || this.tree.options, // defaultValue // ); // } /** Call event handler if defined in tree.options. * Example: * ```js * node._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type: string, extra?: any): any { return this.tree?._callEvent( type, util.extend( { node: this, typeInfo: this.type ? this.tree.types[this.type] : {}, }, extra ) ); } /** * Append (or insert) a list of child nodes. * * Tip: pass `{ before: 0 }` to prepend new nodes as first children. * * @returns first child added */ addChildren( nodeData: WbNodeData | WbNodeData[], options?: AddChildrenOptions ): WunderbaumNode { const tree = this.tree; let { before = null, applyMinExpanLevel = true, _level } = options ?? {}; // let { before, loadLazy=true, _level } = options ?? {}; // const isTopCall = _level == null; _level ??= this.getLevel(); const nodeList = []; try { tree.enableUpdate(false); if (util.isPlainObject(nodeData)) { nodeData = [nodeData]; } const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel; for (const child of nodeData) { const subChildren = child.children; // Remove children property from source data because it should not be // passed to the constructor of WunderbaumNode: delete child.children; const n = new WunderbaumNode(tree, this, child); // Set `children` property again, so it can be used in `reload()` if (subChildren != null) { child.children = subChildren; } if (forceExpand && !n.isUnloaded()) { n.expanded = true; } nodeList.push(n); if (subChildren) { n.addChildren(subChildren, { _level: _level + 1 }); } } if (!this.children) { this.children = nodeList; } else if (before == null || this.children.length === 0) { this.children = this.children.concat(nodeList); } else { // Returns null if before is not a direct child: before = this.findDirectChild(before)!; const pos = this.children.indexOf(before); util.assert( pos >= 0, `options.before must be a direct child of ${this}` ); // insert nodeList after children[pos] this.children.splice(pos, 0, ...nodeList); } // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null); tree.update(ChangeType.structure); } finally { // if (tree.options.selectMode === "hier") { // if (this.parent && this.parent.children) { // this.fixSelection3FromEndNodes(); // } else { // // may happen when loading __root__; // } // } tree.enableUpdate(true); } // if(isTopCall && loadLazy){ // this.logWarn("addChildren(): loadLazy is not yet implemented.") // } return nodeList[0]; } /** * Append or prepend a node, or append a child node. * * This a convenience function that calls addChildren() * * @param nodeData node definition * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') * @returns new node */ addNode( nodeData: WbNodeData, mode: InsertNodeType = "appendChild" ): WunderbaumNode { if (mode === "over") { mode = "appendChild"; // compatible with drop region } switch (mode) { case "after": return this.parent.addChildren(nodeData, { before: this.getNextSibling(), }); case "before": return this.parent.addChildren(nodeData, { before: this }); case "prependChild": // Insert before the first child if any // let insertBefore = this.children ? this.children[0] : undefined; return this.addChildren(nodeData, { before: 0 }); case "appendChild": return this.addChildren(nodeData); } util.assert(false, `Invalid mode: ${mode}`); return (undefined) as WunderbaumNode; } /** * Apply a modification (or navigation) operation. * * @see {@link Wunderbaum.applyCommand} */ applyCommand(cmd: ApplyCommandType, options: ApplyCommandOptions): any { return this.tree.applyCommand(cmd, this, options); } /** * Collapse all expanded sibling nodes if any. * (Automatically called when `autoCollapse` is true.) */ collapseSiblings(options?: SetExpandedOptions): any { for (const node of this.parent.children!) { if (node !== this && node.expanded) { node.setExpanded(false, options); } } } /** * Add/remove one or more classes to `
    `. * * This also maintains `node.classes`, so the class will survive a re-render. * * @param className one or more class names. Multiple classes can be passed * as space-separated string, array of strings, or set of strings. */ setClass( className: string | string[] | Set, flag: boolean = true ): void { const cnSet = util.toSet(className); if (flag) { if (this.classes === null) { this.classes = new Set(); } cnSet.forEach((cn) => { this.classes!.add(cn); this._rowElem?.classList.toggle(cn, flag); }); } else { if (this.classes === null) { return; } cnSet.forEach((cn) => { this.classes!.delete(cn); this._rowElem?.classList.toggle(cn, flag); }); if (this.classes.size === 0) { this.classes = null; } } } /** Start editing this node's title. */ startEditTitle(): void { this.tree._callMethod("edit.startEditTitle", this); } /** * Call `setExpanded()` on all descendant nodes. * * @param flag true to expand, false to collapse. * @param options Additional options. * @see {@link Wunderbaum.expandAll} * @see {@link WunderbaumNode.setExpanded} */ async expandAll(flag: boolean = true, options?: ExpandAllOptions) { const tree = this.tree; const { collapseOthers, deep, depth, force, keepActiveNodeVisible = true, loadLazy, resetLazy, } = options ?? {}; // limit expansion level to `depth` (or tree.minExpandLevel). Default: unlimited const treeLevel = this.tree.options.minExpandLevel || null; // 0 -> null const minLevel = depth ?? (force ? null : treeLevel); const expandOpts = { deep: deep, force: force, loadLazy: loadLazy, resetLazy: resetLazy, scrollIntoView: false, // don't scroll every node while iterating }; this.logInfo(`expandAll(${flag}, depth=${depth}, minLevel=${minLevel})`); util.assert( !(flag && deep != null && !collapseOthers), "Expanding with `deep` option is not supported (implied by the `depth` option)." ); // Expand all direct children in parallel: async function _iter(n: WunderbaumNode, level: number) { // n.logInfo(` _iter(level=${level})`); const promises: Promise[] = []; n.children?.forEach((cn) => { if (flag) { if ( !cn.expanded && (minLevel == null || level < minLevel) && (cn.children || (loadLazy && cn.lazy)) ) { // Node is collapsed and may be expanded (i.e. has children or is lazy) // Expanding may be async, so we store the promise. // Also the recursion is delayed until expansion finished. const p = cn.setExpanded(true, expandOpts); promises.push(p); if (depth == null) { p.then(async () => { await _iter(cn, level + 1); }); } } else { // We don't expand the node, but still visit descendants. // There we may find lazy nodes, so we promises.push(_iter(cn, level + 1)); } } else { // Collapsing is always synchronous, so no promises required // Do not collapse until minExpandLevel if (minLevel == null || level >= minLevel) { cn.setExpanded(false, expandOpts); } if ((minLevel != null && level < minLevel) || deep) { _iter(cn, level + 1); // recursion, even if cn was already collapsed } } }); return new Promise((resolve) => { Promise.all(promises).then(() => { resolve(true); }); }); } const tag = tree.logTime(`${this}.expandAll(${flag}, depth=${depth})`); try { tree.enableUpdate(false); await _iter(this, 0); if (collapseOthers) { util.assert(flag, "Option `collapseOthers` requires flag=true"); util.assert( minLevel != null, "Option `collapseOthers` requires `depth` or `minExpandLevel`" ); this.expandAll(false, { depth: minLevel! }); } } finally { tree.enableUpdate(true); tree.logTimeEnd(tag); } if (tree.activeNode && keepActiveNodeVisible) { tree.activeNode.scrollIntoView(); } } /** * Find all descendant nodes that match condition (excluding self). * * If `match` is a string, search for exact node title. * If `match` is a RegExp expression, apply it to node.title, using * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test). * If `match` is a callback, match all nodes for that the callback(node) returns true. * * Returns an empty array if no nodes were found. * * Examples: * ```js * // Match all node titles that match exactly 'Joe': * nodeList = node.findAll("Joe") * // Match all node titles that start with 'Joe' case sensitive: * nodeList = node.findAll(/^Joe/) * // Match all node titles that contain 'oe', case insensitive: * nodeList = node.findAll(/oe/i) * // Match all nodes with `data.price` >= 99: * nodeList = node.findAll((n) => { * return n.data.price >= 99; * }) * ``` */ findAll(match: string | RegExp | MatcherCallback): WunderbaumNode[] { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); const res: WunderbaumNode[] = []; this.visit((n) => { if (matcher(n)) { res.push(n); } }); return res; } /** Return the direct child with a given key, index or null. */ findDirectChild( ptr: number | string | WunderbaumNode ): WunderbaumNode | null { const cl = this.children; if (!cl) { return null; } if (typeof ptr === "string") { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].key === ptr) { return cl[i]; } } } else if (typeof ptr === "number") { return cl[ptr]; } else if (ptr.parent === this) { // Return null if `ptr` is not a direct child return ptr; } return null; } /** * Find first descendant node that matches condition (excluding self) or null. * * @see {@link WunderbaumNode.findAll} for examples. */ findFirst(match: string | RegExp | MatcherCallback): WunderbaumNode | null { const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match); let res = null; this.visit((n) => { if (matcher(n)) { res = n; return false; } }); return res; } /** Find a node relative to self. * * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()} */ findRelatedNode(where: NavigationType, includeHidden = false) { return this.tree.findRelatedNode(this, where, includeHidden); } /** * Iterator version of {@link WunderbaumNode.format}. */ *format_iter( name_cb?: NodeStringCallback, connectors?: string[] ): IterableIterator { connectors ??= [" ", " | ", " ╰─ ", " ├─ "]; name_cb ??= (node: WunderbaumNode) => "" + node; function _is_last(node: WunderbaumNode): boolean { const ca = node.parent.children!; return node === ca[ca.length - 1]; } const _format_line = (node: WunderbaumNode) => { // https://www.measurethat.net/Benchmarks/Show/12196/0/arr-unshift-vs-push-reverse-small-array const parts = [name_cb!(node)]; parts.unshift(connectors![_is_last(node) ? 2 : 3]); let p = node.parent; while (p && p !== this) { // `this` is the top node parts.unshift(connectors![_is_last(p) ? 0 : 1]); p = p.parent; } return parts.join(""); }; yield name_cb(this); for (const node of this) { yield _format_line(node); } } /** * Return a multiline string representation of a node/subnode hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.getActiveNode().format((n)=>n.title)); * ``` * logs * ``` * Books * ├─ Art of War * ╰─ Don Quixote * ``` * @see {@link WunderbaumNode.format_iter} */ format(name_cb?: NodeStringCallback, connectors?: string[]): string { const a = []; for (const line of this.format_iter(name_cb, connectors)) { a.push(line); } return a.join("\n"); } /** Return the `` element with a given index or id. * @returns {WunderbaumNode | null} */ getColElem(colIdx: number | string) { if (typeof colIdx === "string") { colIdx = this.tree.columns.findIndex((value) => value.id === colIdx); } const colElems = this._rowElem?.querySelectorAll("span.wb-col"); return colElems ? (colElems[colIdx] as HTMLSpanElement) : null; } /** * Return all nodes with the same refKey. * * @param includeSelf Include this node itself. * @see {@link Wunderbaum.findByRefKey} */ getCloneList(includeSelf = false): WunderbaumNode[] { if (!this.refKey) { return []; } const clones = this.tree.findByRefKey(this.refKey); if (includeSelf) { return clones; } return [...clones].filter((n) => n !== this); } /** Return the first child node or null. * @returns {WunderbaumNode | null} */ getFirstChild() { return this.children ? this.children[0] : null; } /** Return the last child node or null. * @returns {WunderbaumNode | null} */ getLastChild() { return this.children ? this.children[this.children.length - 1] : null; } /** Return node depth (starting with 1 for top level nodes). */ getLevel(): number { let i = 0, p = this.parent; while (p) { i++; p = p.parent; } return i; } /** Return the successive node (under the same parent) or null. */ getNextSibling(): WunderbaumNode | null { const ac = this.parent.children!; const idx = ac.indexOf(this); return ac[idx + 1] || null; } /** Return the parent node (null for the system root node). */ getParent(): WunderbaumNode | null { // TODO: return null for top-level nodes? return this.parent; } /** Return an array of all parent nodes (top-down). * @param includeRoot Include the invisible system root node. * @param includeSelf Include the node itself. */ getParentList(includeRoot = false, includeSelf = false) { const l = []; let dtn = includeSelf ? this : this.parent; while (dtn) { if (includeRoot || dtn.parent) { l.unshift(dtn); } dtn = dtn.parent; } return l; } /** Return a string representing the hierarchical node path, e.g. "a/b/c". * @param includeSelf * @param part property name or callback * @param separator */ getPath( includeSelf: boolean = true, part: keyof WunderbaumNode | NodeAnyCallback = "title", separator: string = "/" ) { let val; const path: string[] = []; const isFunc = typeof part === "function"; this.visitParents((n) => { if (n.parent) { val = isFunc ? (part)(n) : n[part]; path.unshift(val); } return undefined; // TODO remove this line }, includeSelf); return path.join(separator); } /** Return the preceding node (under the same parent) or null. */ getPrevSibling(): WunderbaumNode | null { const ac = this.parent.children!; const idx = ac.indexOf(this); return ac[idx - 1] || null; } /** Return true if node has children. * Return undefined if not sure, i.e. the node is lazy and not yet loaded. */ hasChildren() { if (this.lazy) { if (this.children == null) { return undefined; // null or undefined: Not yet loaded } else if (this.children.length === 0) { return false; // Loaded, but response was empty } else if ( this.children.length === 1 && this.children[0].isStatusNode() ) { return undefined; // Currently loading or load error } return true; // One or more child nodes } return !!(this.children && this.children.length); } /** Return true if node has className set. */ hasClass(className: string): boolean { return this.classes ? this.classes.has(className) : false; } /** Return true if node is the currently focused node. @since 0.9.0 */ hasFocus(): boolean { return this.tree.focusNode === this; } /** Return true if this node is the currently active tree node. */ isActive() { return this.tree.activeNode === this; } /** Return true if this node is a direct or indirect parent of `other`. * @see {@link WunderbaumNode.isParentOf} */ isAncestorOf(other: WunderbaumNode) { return other && other.isDescendantOf(this); } /** Return true if this node is a **direct** subnode of `other`. * @see {@link WunderbaumNode.isDescendantOf} */ isChildOf(other: WunderbaumNode) { return other && this.parent === other; } /** Return true if this node's refKey is used by at least one other node. */ isClone() { return !!this.refKey && this.tree.findByRefKey(this.refKey).length > 1; } /** Return true if this node's title spans all columns, i.e. the node has no * grid cells. */ isColspan() { return !!this.getOption("colspan"); } /** Return true if this node is a direct or indirect subnode of `other`. * @see {@link WunderbaumNode.isChildOf} */ isDescendantOf(other: WunderbaumNode) { if (!other || other.tree !== this.tree) { return false; } let p = this.parent; while (p) { if (p === other) { return true; } if (p === p.parent) { util.error(`Recursive parent link: ${p}`); } p = p.parent; } return false; } /** Return true if this node has children, i.e. the node is generally expandable. * If `andCollapsed` is set, we also check if this node is collapsed, i.e. * an expand operation is currently possible. */ isExpandable(andCollapsed = false): boolean { // `false` is never expandable (unofficial) if ((andCollapsed && this.expanded) || this.children === false) { return false; } if (this.children == null) { return !!this.lazy; // null or undefined can trigger lazy load } if (this.children.length === 0) { return !!this.tree.options.emptyChildListExpandable; } return true; } /** Return true if _this_ node is currently in edit-title mode. * * See {@link WunderbaumNode.startEditTitle}. */ isEditingTitle(): boolean { return this.tree._callMethod("edit.isEditingTitle", this); } /** Return true if this node is currently expanded. */ isExpanded(): boolean { return !!this.expanded; } /** Return true if this node is the first node of its parent's children. */ isFirstSibling(): boolean { const p = this.parent; return !p || p.children![0] === this; } /** Return true if this node is the last node of its parent's children. */ isLastSibling(): boolean { const p = this.parent; return !p || p.children![p.children!.length - 1] === this; } /** Return true if this node is lazy (even if data was already loaded) */ isLazy(): boolean { return !!this.lazy; } /** Return true if node is lazy and loaded. For non-lazy nodes always return true. */ isLoaded(): boolean { return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node } /** Return true if node is currently loading, i.e. a GET request is pending. */ isLoading(): boolean { return this._isLoading; } /** Return true if this node is a temporarily generated status node of type 'paging'. */ isPagingNode(): boolean { return this.statusNodeType === "paging"; } /** Return true if this node is a **direct** parent of `other`. * @see {@link WunderbaumNode.isAncestorOf} */ isParentOf(other: WunderbaumNode) { return other && other.parent === this; } /** Return true if this node is partially loaded. @experimental */ isPartload(): boolean { return !!this._partload; } /** Return true if this node is partially selected (tri-state). */ isPartsel(): boolean { return !this.selected && !!this._partsel; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRadio(): boolean { return !!this.parent.radiogroup || this.getOption("checkbox") === "radio"; } /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */ isRendered(): boolean { return !!this._rowElem; } /** Return true if this node is the (invisible) system root node. * @see {@link WunderbaumNode.isTopLevel} */ isRootNode(): boolean { return this.tree.root === this; } /** Return true if this node is selected, i.e. the checkbox is set. * `undefined` if partly selected (tri-state), false otherwise. */ isSelected(): TristateType { return this.selected ? true : this._partsel ? undefined : false; } /** Return true if this node is a temporarily generated system node like * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). */ isStatusNode(): boolean { return !!this.statusNodeType; } /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. */ isTopLevel(): boolean { return this.tree.root === this.parent; } /** Return true if node is marked lazy but not yet loaded. * For non-lazy nodes always return false. */ isUnloaded(): boolean { // Also checks if the only child is a status node: return this.hasChildren() === undefined; } /** Return true if all parent nodes are expanded. Note: this does not check * whether the node is scrolled into the visible part of the screen or viewport. */ isVisible(): boolean { const hasFilter = this.tree.filterMode === "hide"; const parents = this.getParentList(false, false); // TODO: check $(n.span).is(":visible") // i.e. return false for nodes (but not parents) that are hidden // by a filter if (hasFilter && !this.match && !this.subMatchCount) { // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); return false; } for (let i = 0, l = parents.length; i < l; i++) { const n = parents[i]; if (!n.expanded) { // this.debug("isVisible: HIDDEN (parent collapsed)"); return false; } // if (hasFilter && !n.match && !n.subMatchCount) { // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); // return false; // } } // this.debug("isVisible: VISIBLE"); return true; } protected _loadSourceObject(source: any, level?: number) { const tree = this.tree; level ??= this.getLevel(); // Let caller modify the parsed JSON response: const res = this._callEvent("receive", { response: source }); if (res != null) { source = res; } if (util.isArray(source)) { source = { children: source }; } util.assert( util.isPlainObject(source), `Expected an array or plain object: ${source}` ); const format: string = source.format ?? "nested"; util.assert( format === "nested" || format === "flat", `Expected source.format = 'nested' or 'flat': ${format}` ); // Pre-rocess for 'nested' or 'flat' format decompressSourceData(source); util.assert( source.children, "If `source` is an object, it must have a `children` property" ); if (source.types) { tree.logInfo("Redefine types", source.columns); tree.setTypes(source.types, false); delete source.types; } if (source.columns) { tree.logInfo("Redefine columns", source.columns); tree.columns = source.columns; delete source.columns; tree.update(ChangeType.colStructure); } this.addChildren(source.children); // Add extra data to `tree.data` for (const [key, value] of Object.entries(source)) { if (!RESERVED_TREE_SOURCE_KEYS.has(key)) { tree.data[key] = value; // tree.logDebug(`Add source.${key} to tree.data.${key}`); } } if (tree.options.selectMode === "hier") { this.fixSelection3FromEndNodes(); } // Allow to un-sort nodes after sorting this.resetNativeChildOrder(); this._callEvent("load"); } async _fetchWithOptions(source: any) { // Either a URL string or an object with a `.url` property. let url: string, params, body, options, rest; let fetchOpts: RequestInit = {}; if (typeof source === "string") { // source is a plain URL string: assume GET request url = source; fetchOpts.method = "GET"; } else if (util.isPlainObject(source)) { // source is a plain object with `.url` property. ({ url, params, body, options, ...rest } = source); util.assert( !rest || Object.keys(rest).length === 0, `Unexpected source properties: ${Object.keys( rest )}. Use 'options' instead.` ); util.assert(typeof url === "string", `expected source.url as string`); if (util.isPlainObject(options)) { fetchOpts = options; } if (util.isPlainObject(body)) { // we also accept 'body' as object... util.assert( !fetchOpts.body, "options.body should be passed as source.body" ); fetchOpts.body = JSON.stringify(fetchOpts.body); fetchOpts.method ??= "POST"; // set default } if (util.isPlainObject(params)) { url += "?" + new URLSearchParams(params); fetchOpts.method ??= "GET"; // set default } } else { url = ""; // keep linter happy util.error(`Unsupported source format: ${source}`); } this.setStatus(NodeStatusType.loading); const response = await fetch(url, fetchOpts); if (!response.ok) { util.error(`GET ${url} returned ${response.status}, ${response}`); } return await response.json(); } /** Download data from the cloud, then call `.update()`. */ async load(source: SourceType) { const tree = this.tree; const requestId = Date.now(); const prevParent = this.parent; const start = Date.now(); let elap = 0, elapLoad = 0, elapProcess = 0; // Check for overlapping requests if (this._requestId) { this.logWarn( `Recursive load request #${requestId} while #${this._requestId} is pending. ` + "The previous request will be ignored." ); } this._requestId = requestId; // const timerLabel = tree.logTime(this + ".load()"); try { const url: string = typeof source === "string" ? source : (source).url; if (!url) { // An array or a plain object (that does NOT contain a `.url` property) // will be treated as native Wunderbaum data if (typeof (source).then === "function") { const msg = tree.logTime(`Resolve thenable ${source}`); source = await Promise.resolve(source); tree.logTimeEnd(msg); } this._loadSourceObject(source); elapProcess = Date.now() - start; } else { // Either a URL string or an object with a `.url` property. const data = await this._fetchWithOptions(source); elapLoad = Date.now() - start; if (this._requestId && this._requestId > requestId) { this.logWarn( `Ignored load response #${requestId} because #${this._requestId} is pending.` ); return; } else { this.logDebug(`Received response for load request #${requestId}`); } if (this.parent === null && prevParent !== null) { this.logWarn( "Lazy parent node was removed while loading: discarding response." ); return; } this.setStatus(NodeStatusType.ok); // if (data.columns) { // tree.logInfo("Re-define columns", data.columns); // util.assert(!this.parent); // tree.columns = data.columns; // delete data.columns; // tree.updateColumns({ calculateCols: false }); // } const startProcess = Date.now(); this._loadSourceObject(data); elapProcess = Date.now() - startProcess; } } catch (error) { this.logError("Error during load()", source, error); this._callEvent("error", { error: error }); this.setStatus(NodeStatusType.error, { message: "" + error }); throw error; } finally { this._requestId = 0; elap = Date.now() - start; if (tree.options.debugLevel! >= 3) { tree.logInfo( `Load source took ${elap / 1000} seconds ` + `(transfer: ${elapLoad / 1000}s, ` + `processing: ${elapProcess / 1000}s)` ); } } } /** * Load content of a lazy node. * If the node is already loaded, nothing happens. * @param [forceReload=false] If true, reload even if already loaded. */ async loadLazy(forceReload: boolean = false) { const wasExpanded = this.expanded; util.assert(this.lazy, "load() requires a lazy node"); if (!forceReload && !this.isUnloaded()) { return; // Already loaded: nothing to do } if (this.isLoading()) { this.logWarn("loadLazy() called while already loading: ignored."); return; // Already loading: prevent duplicate requests } if (this.isLoaded()) { this.resetLazy(); // Also collapses if currently expanded } // `lazyLoad` may be long-running, so mark node as loading now. `this.load()` // will reset the status later. this.setStatus(NodeStatusType.loading); try { const source = await this._callEvent("lazyLoad"); if (source === false) { this.setStatus(NodeStatusType.ok); return; } util.assert( util.isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false." ); await this.load(source); this.setStatus(NodeStatusType.ok); // Also resets `this._isLoading` if (wasExpanded) { this.expanded = true; this.tree.update(ChangeType.structure); } else { this.update(); // Fix expander icon to 'loaded' } } catch (e) { this.logError("Error during loadLazy()", e); this._callEvent("error", { error: e }); // Also resets `this._isLoading`: this.setStatus(NodeStatusType.error, { message: "" + e }); } return; } /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4. * @see {@link WunderbaumNode.logDebug} */ log(...args: any[]) { if (this.tree.options.debugLevel! >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4 * and browser console level includes debug/verbose messages. * @see {@link WunderbaumNode.log} */ logDebug(...args: any[]) { if (this.tree.options.debugLevel! >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */ logError(...args: any[]) { if (this.tree.options.debugLevel! >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */ logInfo(...args: any[]) { if (this.tree.options.debugLevel! >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */ logWarn(...args: any[]) { if (this.tree.options.debugLevel! >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Expand all parents and optionally scroll into visible area as neccessary. * Promise is resolved, when lazy loading and animations are done. * @param {object} [options] passed to `setExpanded()`. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} */ async makeVisible(options?: MakeVisibleOptions) { let i; const dfd = new Deferred(); const deferreds = []; const parents = this.getParentList(false, false); const len = parents.length; const noAnimation = util.getOption(options, "noAnimation", false); const scroll = util.getOption(options, "scrollIntoView", true); // Expand bottom-up, so only the top node is animated for (i = len - 1; i >= 0; i--) { // self.debug("pushexpand" + parents[i]); const seOpts = { noAnimation: noAnimation }; deferreds.push(parents[i].setExpanded(true, seOpts)); } Promise.all(deferreds).then(() => { // All expands have finished // self.debug("expand DONE", scroll); // Note: this.tree may be none when switching demo trees if (scroll && this.tree) { // Make sure markup and _rowIdx is updated before we do the scroll calculations this.tree.updatePendingModifications(); this.scrollIntoView().then(() => { // self.debug("scroll DONE"); dfd.resolve(); }); } else { dfd.resolve(); } }); return dfd.promise(); } /** Move this node to targetNode. */ moveTo( targetNode: WunderbaumNode, mode: InsertNodeType = "appendChild", map?: NodeAnyCallback ) { if (mode === "over") { mode = "appendChild"; // compatible with drop region } if (mode === "prependChild") { if (targetNode.children && targetNode.children.length) { mode = "before"; targetNode = targetNode.children[0]; } else { mode = "appendChild"; } } let pos; const tree = this.tree; const prevParent = this.parent; const targetParent = mode === "appendChild" ? targetNode : targetNode.parent; if (this === targetNode) { return; } else if (!this.parent) { util.error("Cannot move system root"); } else if (targetParent.isDescendantOf(this)) { util.error("Cannot move a node to its own descendant"); } if (targetParent !== prevParent) { prevParent.triggerModifyChild("remove", this); } // Unlink this node from current parent if (this.parent.children!.length === 1) { if (this.parent === targetParent) { return; // #258 } this.parent.children = this.parent.lazy ? [] : null; this.parent.expanded = false; } else { pos = this.parent.children!.indexOf(this); util.assert(pos >= 0, "invalid source parent"); this.parent.children!.splice(pos, 1); } // Insert this node to target parent's child list this.parent = targetParent; if (targetParent.hasChildren()) { switch (mode) { case "appendChild": // Append to existing target children targetParent.children!.push(this); break; case "before": // Insert this node before target node pos = targetParent.children!.indexOf(targetNode); util.assert(pos >= 0, "invalid target parent"); targetParent.children!.splice(pos, 0, this); break; case "after": // Insert this node after target node pos = targetParent.children!.indexOf(targetNode); util.assert(pos >= 0, "invalid target parent"); targetParent.children!.splice(pos + 1, 0, this); break; default: util.error(`Invalid mode '${mode}'.`); } } else { targetParent.children = [this]; } // Let caller modify the nodes if (map) { targetNode.visit(map, true); } if (targetParent === prevParent) { targetParent.triggerModifyChild("move", this); } else { // prevParent.triggerModifyChild("remove", this); targetParent.triggerModifyChild("add", this); } // Handle cross-tree moves if (tree !== targetNode.tree) { // Fix node.tree for all source nodes // util.assert(false, "Cross-tree move is not yet implemented."); this.logWarn("Cross-tree moveTo is experimental!"); this.visit((n) => { // TODO: fix selection state and activation, ... n.tree = targetNode.tree; }, true); } // Make sure we update async, because discarding the markup would prevent // DragAndDrop to generate a dragend event on the source node setTimeout(() => { // Even indentation may have changed: tree.update(ChangeType.any); }, 0); // TODO: fix selection state // TODO: fix active state } /** Set focus relative to this node and optionally activate. * * 'left' collapses the node if it is expanded, or move to the parent * otherwise. * 'right' expands the node if it is collapsed, or move to the first * child otherwise. * * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the `event.key` that would normally trigger this move, * e.g. `ArrowLeft` = 'left'. * @param options */ async navigate(where: NavigationType | string, options?: NavigateOptions) { // Allow to pass 'ArrowLeft' instead of 'left' const navType = (KEY_TO_NAVIGATION_MAP[where] ?? where) as NavigationType; // Otherwise activate or focus the related node const node = this.findRelatedNode(navType); if (!node) { this.logWarn(`Could not find related node '${where}'.`); return Promise.resolve(this); } // setFocus/setActive will scroll later (if autoScroll is specified) try { node.makeVisible({ scrollIntoView: false }); } catch (e) { // ignore } node.setFocus(); if (options?.activate === false) { return Promise.resolve(this); } return node.setActive(true, { event: options?.event }); } /** Delete this node and all descendants. */ remove() { const tree = this.tree; const pos = this.parent.children!.indexOf(this); this.triggerModify("remove"); this.parent.children!.splice(pos, 1); this.visit((n) => { n.removeMarkup(); tree._unregisterNode(n); }, true); tree.update(ChangeType.structure); } /** Remove all descendants of this node. */ removeChildren() { const tree = this.tree; if (!this.children) { return; } if (tree.activeNode?.isDescendantOf(this)) { tree.activeNode.setActive(false); // TODO: don't fire events } if (tree.focusNode?.isDescendantOf(this)) { tree._setFocusNode(null); } // TODO: persist must take care to clear select and expand cookies // Unlink children to support GC // TODO: also delete this.children (not possible using visit()) this.triggerModifyChild("remove", null); this.visit((n) => { tree._unregisterNode(n); }); if (this.lazy) { // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes this.children = []; } else { this.children = null; } // util.assert(this.parent); // don't call this for root node if (!this.isRootNode()) { this.expanded = false; } this.tree.update(ChangeType.structure); } /** Remove all HTML markup from the DOM. */ removeMarkup() { if (this._rowElem) { delete (this._rowElem)._wb_node; this._rowElem.remove(); this._rowElem = undefined; } } protected _getRenderInfo(): any { const allColInfosById: ColumnEventInfoMap = {}; const renderColInfosById: ColumnEventInfoMap = {}; const isColspan = this.isColspan(); const colElems = this._rowElem ? ((( this._rowElem.querySelectorAll("span.wb-col") )) as HTMLSpanElement[]) : null; let idx = 0; for (const col of this.tree.columns) { allColInfosById[col.id] = { id: col.id, idx: idx, elem: colElems ? colElems[idx] : null, info: col, }; // renderColInfosById only contains columns that need rendering: if (!isColspan && col.id !== "*") { renderColInfosById[col.id] = allColInfosById[col.id]; } idx++; } return { allColInfosById: allColInfosById, renderColInfosById: renderColInfosById, }; } protected _createIcon( parentElem: HTMLElement, replaceChild: HTMLElement | null, showLoading: boolean ): HTMLElement | null { const iconElem = this.tree._createNodeIcon(this, showLoading, true); if (iconElem) { if (replaceChild) { parentElem.replaceChild(iconElem, replaceChild); } else { parentElem.appendChild(iconElem); } } return iconElem; } /** * Create a whole new `
    ` element. * @see {@link WunderbaumNode._render} */ protected _render_markup(opts: RenderOptions) { const tree = this.tree; const treeOptions = tree.options; const rowHeight = treeOptions.rowHeightPx; const checkbox = this.getOption("checkbox"); const columns = tree.columns; const level = this.getLevel(); const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx; let elem: HTMLElement; let rowDiv = this._rowElem; let checkboxSpan: HTMLElement | null = null; let expanderSpan: HTMLElement | null = null; const isNew = !rowDiv; util.assert(isNew, "Expected unrendered node"); util.assert( !isNew || (opts && opts.after), "opts.after expected, unless updating" ); util.assert(!this.isRootNode(), "Root node not allowed"); rowDiv = document.createElement("div"); rowDiv.classList.add("wb-row"); rowDiv.style.top = this._rowIdx! * rowHeight + "px"; this._rowElem = rowDiv; // Attach a node reference to the DOM Element: (rowDiv)._wb_node = this; const nodeElem: HTMLSpanElement = document.createElement("span"); nodeElem.classList.add("wb-node", "wb-col"); rowDiv.appendChild(nodeElem); let ofsTitlePx = 0; if (checkbox) { checkboxSpan = document.createElement("i"); checkboxSpan.classList.add("wb-checkbox"); if (checkbox === "radio" || this.parent.radiogroup) { checkboxSpan.classList.add("wb-radio"); } nodeElem.appendChild(checkboxSpan); ofsTitlePx += ICON_WIDTH; } for (let i = level - 1; i > 0; i--) { elem = document.createElement("i"); elem.classList.add("wb-indent"); nodeElem.appendChild(elem); ofsTitlePx += ICON_WIDTH; } if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) { expanderSpan = document.createElement("i"); expanderSpan.classList.add("wb-expander"); nodeElem.appendChild(expanderSpan); ofsTitlePx += ICON_WIDTH; } // Render the icon (show a 'loading' icon if we do not have an expander that // we would prefer). const iconSpan = this._createIcon(nodeElem, null, !expanderSpan); if (iconSpan) { ofsTitlePx += ICON_WIDTH; } const titleSpan = document.createElement("span"); titleSpan.classList.add("wb-title"); nodeElem.appendChild(titleSpan); // this._callEvent("enhanceTitle", { titleSpan: titleSpan }); // Store the width of leading icons with the node, so we can calculate // the width of the embedded title span later (nodeElem)._ofsTitlePx = ofsTitlePx; // Support HTML5 drag-n-drop if (tree.options.dnd!.dragStart) { nodeElem.draggable = true; } // Render columns const isColspan = this.isColspan(); if (!isColspan && columns.length > 1) { let colIdx = 0; for (const col of columns) { colIdx++; let colElem; if (col.id === "*") { colElem = nodeElem; } else { colElem = document.createElement("span"); colElem.classList.add("wb-col"); rowDiv.appendChild(colElem); } if (colIdx === activeColIdx) { colElem.classList.add("wb-active"); } // Add classes from `columns` definition to `` cells col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; if (isNew && col.html) { if (typeof col.html === "string") { colElem.innerHTML = col.html; } } } } // Attach to DOM as late as possible const after = opts ? opts.after : "last"; switch (after) { case "first": tree.nodeListElement.prepend(rowDiv); break; case "last": tree.nodeListElement.appendChild(rowDiv); break; default: opts.after.after(rowDiv); } // Now go on and fill in data and update classes opts.isNew = true; this._render_data(opts); } /** * Render `node.title`, `.icon` into an existing row. * * @see {@link WunderbaumNode._render} */ protected _render_data(opts: RenderOptions) { util.assert(this._rowElem, "No _rowElem"); const tree = this.tree; const treeOptions = tree.options; const rowDiv = this._rowElem!; const isNew = !!opts.isNew; // Called by _render_markup()? const preventScroll = !!opts.preventScroll; const columns = tree.columns; const isColspan = this.isColspan(); // Row markup already exists const nodeElem = rowDiv.querySelector("span.wb-node") as HTMLSpanElement; const titleSpan = nodeElem.querySelector( "span.wb-title" ) as HTMLSpanElement; const scrollTop = tree.element.scrollTop; if (this.titleWithHighlight) { titleSpan.innerHTML = this.titleWithHighlight; } else { titleSpan.textContent = this.title; // TODO: this triggers scroll events } const tooltip = this.getOption("tooltip", false); if (tooltip) { titleSpan.title = tooltip === true ? this.title : tooltip; } // NOTE: At least on Safari, this render call triggers a scroll event // probably when a focused input is replaced. if (preventScroll) { tree.element.scrollTop = scrollTop; } // Set the width of the title span, so overflow ellipsis work if (!treeOptions.skeleton) { if (isColspan) { const vpWidth = tree.element.clientWidth; titleSpan.style.width = vpWidth - (nodeElem)._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } else { titleSpan.style.width = columns[0]._widthPx! - (nodeElem)._ofsTitlePx - TITLE_SPAN_PAD_Y + "px"; } } // Update row classes opts.isDataChange = true; this._render_status(opts); // Let user modify the result if (this.statusNodeType) { this._callEvent("renderStatusNode", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, }); } else if (this.parent) { // Skip root node const renderInfo = this._getRenderInfo(); this._callEvent("render", { isNew: isNew, nodeElem: nodeElem, isColspan: isColspan, allColInfosById: renderInfo.allColInfosById, renderColInfosById: renderInfo.renderColInfosById, }); } } /** * Update row classes to reflect active, focuses, etc. * @see {@link WunderbaumNode._render} */ protected _render_status(opts: RenderOptions) { // this.log("_render_status", opts); const tree = this.tree; const iconMap = tree.iconMap; const treeOptions = tree.options; const typeInfo = this.type ? tree.types[this.type] : null; const rowDiv = this._rowElem!; // Row markup already exists const nodeSpan = rowDiv.querySelector("span.wb-node") as HTMLSpanElement; const expanderElem = nodeSpan.querySelector( "i.wb-expander" ) as HTMLLIElement; const checkboxElem = nodeSpan.querySelector( "i.wb-checkbox" ) as HTMLLIElement; const rowClasses = ["wb-row"]; this.expanded ? rowClasses.push("wb-expanded") : 0; this.lazy ? rowClasses.push("wb-lazy") : 0; this.selected ? rowClasses.push("wb-selected") : 0; this._partsel ? rowClasses.push("wb-partsel") : 0; this === tree.activeNode ? rowClasses.push("wb-active") : 0; this === tree.focusNode ? rowClasses.push("wb-focus") : 0; this._errorInfo ? rowClasses.push("wb-error") : 0; this._isLoading ? rowClasses.push("wb-loading") : 0; this.isColspan() ? rowClasses.push("wb-colspan") : 0; this.statusNodeType ? rowClasses.push("wb-status-" + this.statusNodeType) : 0; this.match ? rowClasses.push("wb-match") : 0; this.subMatchCount ? rowClasses.push("wb-submatch") : 0; treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0; // Replace previous classes: rowDiv.className = rowClasses.join(" "); // Add classes from `node.classes` this.classes ? rowDiv.classList.add(...this.classes) : 0; // Add classes from `tree.types[node.type]` if (typeInfo && typeInfo.classes) { rowDiv.classList.add(...typeInfo.classes); } if (expanderElem) { let image = null; if (this._isLoading) { image = iconMap.loading; } else if (this.isExpandable(false)) { if (this.expanded) { image = iconMap.expanderExpanded; } else { image = iconMap.expanderCollapsed; } } else if (this.lazy && this.children == null) { image = iconMap.expanderLazy; } if (image == null) { expanderElem.className = "wb-expander"; expanderElem.classList.add("wb-indent"); } else if (TEST_HTML.test(image)) { expanderElem.replaceWith(util.elemFromHtml(image)); } else if (TEST_FILE_PATH.test(image)) { expanderElem.style.backgroundImage = `url('${image}')`; } else { expanderElem.className = "wb-expander " + image; } } if (checkboxElem) { let cbclass = "wb-checkbox "; if (this.isRadio()) { cbclass += "wb-radio "; if (this.selected) { cbclass += iconMap.radioChecked; // } else if (this._partsel) { // cbclass += iconMap.radioUnknown; } else { cbclass += iconMap.radioUnchecked; } } else { if (this.selected) { cbclass += iconMap.checkChecked; } else if (this._partsel) { cbclass += iconMap.checkUnknown; } else { cbclass += iconMap.checkUnchecked; } } checkboxElem.className = cbclass; } // Fix active cell in cell-nav mode if (!opts.isNew) { let i = 0; for (const colSpan of rowDiv.children) { colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx); colSpan.classList.remove("wb-error", "wb-invalid"); } // Update icon (if not opts.isNew, which would rebuild markup anyway) const iconSpan = nodeSpan.querySelector("i.wb-icon") as HTMLElement; if (iconSpan) { this._createIcon(nodeSpan, iconSpan, !expanderElem); } } // Adjust column width if (opts.resizeCols !== false && !this.isColspan()) { const colElems = rowDiv.querySelectorAll("span.wb-col"); let idx = 0; let ofs = 0; for (const colDef of this.tree.columns) { const colElem = colElems[idx] as HTMLSpanElement; colElem.style.left = `${ofs}px`; colElem.style.width = `${colDef._widthPx}px`; idx++; ofs += colDef._widthPx!; } } } /* * Create or update node's markup. * * `options.change` defaults to ChangeType.data, which updates the title, * icon, and status. It also triggers the `render` event, that lets the user * create or update the content of embeded cell elements. * * If only the status or other class-only modifications have changed, * `options.change` should be set to ChangeType.status instead for best * efficiency. * * Calling `update()` is almost always a better alternative. * @see {@link WunderbaumNode.update} */ _render(options?: RenderOptions) { // this.log("render", options); const opts = Object.assign({ change: ChangeType.data }, options); if (!this._rowElem) { opts.change = ChangeType.row; } switch (opts.change) { case "status": this._render_status(opts); break; case "data": this._render_data(opts); break; case "row": // _rowElem is not yet created (asserted in _render_markup) this._render_markup(opts); break; default: util.error(`Invalid change type '${opts.change}'.`); } } /** * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad * event is triggered on next expand. */ resetLazy() { this.removeChildren(); this.expanded = false; this.lazy = true; this.children = null; this.tree.update(ChangeType.structure); } /** Convert node (or whole branch) into a plain object. * * The result is compatible with node.addChildren(). * * @param recursive include child nodes * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link Wunderbaum.toDictArray}. */ toDict(recursive = false, callback?: NodeToDictCallback): WbNodeData { const dict: any = {}; NODE_DICT_PROPS.forEach((propName: string) => { const val = (this)[propName]; if (val instanceof Set) { // Convert Set to string (or skip if set is empty) val.size ? (dict[propName] = Array.prototype.join.call(val.keys(), " ")) : 0; } else if (val || val === false || val === 0) { dict[propName] = val; } }); if (!util.isEmptyObject(this.data)) { dict.data = util.extend({}, this.data); if (util.isEmptyObject(dict.data)) { delete dict.data; } } if (callback) { const res = callback(dict, this); if (res === false) { // Note: a return value of `false` is only used internally return false; // Don't include this node nor its children } if (res === "skip") { recursive = false; // Include this node, but not the children } } if (recursive) { if (util.isArray(this.children)) { dict.children = []; for (let i = 0, l = this.children!.length; i < l; i++) { const node = this.children![i]; if (!node.isStatusNode()) { // Note: a return value of `false` is only used internally const res = node.toDict(true, callback); if (res !== false) { dict.children.push(res); } } } } } return dict; } /** Return an option value that has a default, but may be overridden by a * callback or a node instance attribute. * * Evaluation sequence: * * - If `tree.options.` is a callback that returns something, use that. * - Else if `node.` is defined, use that. * - Else if `tree.types[]` is a value, use that. * - Else if `tree.options.` is a value, use that. * - Else use `defaultValue`. * * @param name name of the option property (on node and tree) * @param defaultValue return this if nothing else matched * {@link Wunderbaum.getOption|Wunderbaum.getOption} */ getOption(name: string, defaultValue?: any) { const tree = this.tree; let opts: any = tree.options; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [opts, name] = name.split("."); } const value = opts[name]; // ?? defaultValue; // A callback resolver always takes precedence if (typeof value === "function") { const res = value.call(tree, { type: "resolve", tree: tree, node: this, // typeInfo: this.type ? tree.types[this.type] : {}, }); if (res !== undefined) { return res; } } // If this node has an explicit local setting, use it: if ((this)[name] !== undefined) { return (this)[name]; } // Use value from type definition if defined const typeInfo = this.type ? tree.types[this.type] : undefined; const res = typeInfo ? typeInfo[name] : undefined; if (res !== undefined) { return res; } // Use value from value options dict, fallback do default return value ?? defaultValue; } /** Make sure that this node is visible in the viewport. * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo} */ async scrollIntoView(options?: ScrollIntoViewOptions) { const opts = Object.assign({ node: this }, options); return this.tree.scrollTo(opts); } /** * Activate this node, deactivate previous, send events, activate column and * scroll into viewport. */ async setActive(flag: boolean = true, options?: SetActiveOptions) { const tree = this.tree; const prev = tree.getActiveNode(); const retrigger = options?.retrigger; // Default: false const focusTree = options?.focusTree; // Default: false // const focusNode = options?.focusNode !== false; // Default: true const noEvents = options?.noEvents; // Default: false const orgEvent = options?.event; // Default: null const colIdx = options?.colIdx; // Default: null const edit = options?.edit; // Default: false // util.assert(!colIdx || tree.isCellNav(), "colIdx requires cellNav"); util.assert(!edit || colIdx != null, "edit requires colIdx"); if (!noEvents) { if (flag) { if (prev !== this || retrigger) { if ( prev?._callEvent("deactivate", { nextNode: this, event: orgEvent, }) === false || this._callEvent("beforeActivate", { prevNode: prev, event: orgEvent, }) === false ) { return; } tree._setActiveNode(null); prev?.update(ChangeType.status); } } else if (prev === this || retrigger) { this._callEvent("deactivate", { nextNode: null, event: orgEvent }); } } if (prev !== this) { if (flag) { tree._setActiveNode(this); } prev?.update(ChangeType.status); this.update(ChangeType.status); } return this.makeVisible().then(() => { if (flag) { if (focusTree || edit) { tree.setFocus(); tree._setFocusNode(this); tree.focusNode!.setFocus(); } // if (focusNode || edit) { // tree.focusNode = this; // tree.focusNode.setFocus(); // } if (colIdx != null && tree.isCellNav()) { tree.setColumn(colIdx, { edit: edit }); } if (!noEvents) { this._callEvent("activate", { prevNode: prev, event: orgEvent }); } } }); } /** * Expand or collapse this node. */ async setExpanded(flag: boolean = true, options?: SetExpandedOptions) { const { force, scrollIntoView, immediate, resetLazy } = options ?? {}; const sendEvents = !options?.noEvents; // Default: send events if ( !flag && this.isExpanded() && this.getLevel() <= this.tree.getOption("minExpandLevel") && !force ) { this.logDebug("Ignored collapse request below minExpandLevel."); return; } if (!flag === !this.expanded) { return; // Nothing to do } if ( sendEvents && this._callEvent("beforeExpand", { flag: flag }) === false ) { return; } // this.log("setExpanded()"); if (flag && this.getOption("autoCollapse")) { this.collapseSiblings(options); } if (flag && this.lazy && this.children == null) { await this.loadLazy(); } else if (!flag && resetLazy && this.lazy && this.children) { this.resetLazy(); } this.expanded = flag; const updateOpts = { immediate: immediate }; // const updateOpts = { immediate: !!util.getOption(options, "immediate") }; this.tree.update(ChangeType.structure, updateOpts); if (flag && scrollIntoView) { const lastChild = this.getLastChild(); if (lastChild) { this.tree.updatePendingModifications(); lastChild.scrollIntoView({ topNode: this }); } } if (sendEvents) { this._callEvent("expand", { flag: flag }); } } /** * Set keyboard focus here. * @see {@link setActive} */ setFocus(flag: boolean = true) { util.assert(!!flag, "Blur is not yet implemented"); const prev = this.tree.focusNode; this.tree._setFocusNode(this); prev?.update(); this.update(); } /** Set a new icon path or class. */ setIcon(icon: string) { this.icon = icon; this.update(); } /** Change node's {@link key} and/or {@link refKey}. */ setKey(key: string | null, refKey: string | null) { throw new Error("Not yet implemented"); } // /** // * Calculate a *stable*, unique key for this node from its refKey (or title). // * We also add information from the parent, because a refKey may occur multiple // * times in a tree. // */ // calcUniqueKey() { // // Assuming that the parent's key was calculated the same way, we implicitly // // involve the whole refKey-path: // const s = this.key + (this.refKey || this.title); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // const h1 = util.murmurHash3(s, true); // return "id_" + h1 + util.murmurHash3(h1 + s, true); // // const l = []; // // // eslint-disable-next-line @typescript-eslint/no-this-alias // // let node: WunderbaumNode = this; // // while (node.parent) { // // l.unshift(node.refKey || node.key); // // node = node.parent; // // } // // const path = l.join("/"); // // 32-bit has a high probability of collisions, so we pump up to 64-bit // // https://security.stackexchange.com/q/209882/207588 // // const h1 = util.murmurHash3(path, true); // // return "id_" + h1 + util.murmurHash3(h1 + path, true); // } /** * Trigger a repaint, typically after a status or data change. * * `change` defaults to 'data', which handles modifcations of title, icon, * and column content. It can be reduced to 'ChangeType.status' if only * active/focus/selected state has changed. * * This method will eventually call {@link WunderbaumNode._render} with * default options, but may be more consistent with the tree's * {@link Wunderbaum.update} API. */ update(change: ChangeType = ChangeType.data) { util.assert( change === ChangeType.status || change === ChangeType.data, `Invalid change type ${change}` ); this.tree.update(change, this); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents: boolean = false): WunderbaumNode[] { const nodeList: WunderbaumNode[] = []; this.visit((node) => { if (node.selected) { nodeList.push(node); if (stopOnParents === true) { return "skip"; // stop processing this branch } } }); return nodeList; } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false): string[] { const refKeys = new Set(); this.visit((node) => { if (node.refKey != null && (!selected || node.selected)) { refKeys.add(node.refKey); } }); return Array.from(refKeys); } /** Toggle the check/uncheck state. */ toggleSelected(options?: SetSelectedOptions): TristateType { let flag = this.isSelected(); if (flag === undefined && !this.isRadio()) { flag = this._anySelectable(); } else { flag = !flag; } return this.setSelected(flag, options); } /** Return true if at least on selectable descendant end-node is unselected. @internal */ _anySelectable(): boolean { let found = false; this.visit((node) => { if ( node.selected === false && !node.unselectable && !node.hasChildren() && !node.parent.radiogroup ) { found = true; return false; // Stop iteration } }); return found; } /* Apply selection state to a single node. */ protected _changeSelectStatusProps(state: TristateType): boolean { let changed = false; switch (state) { case false: changed = this.selected || this._partsel; this.selected = false; this._partsel = false; break; case true: changed = !this.selected || !this._partsel; this.selected = true; this._partsel = true; break; case undefined: changed = this.selected || !this._partsel; this.selected = false; // #110: end nodess cannot have a `_partsel` flag this._partsel = this.hasChildren() ? true : false; break; default: util.error(`Invalid state: ${state}`); } if (changed) { this.update(); } return changed; } /** * Fix selection status, after this node was (de)selected in `selectMode: 'hier'`. * This includes (de)selecting all descendants. */ fixSelection3AfterClick(opts?: SetSelectedOptions): void { const force = !!opts?.force; const flag = this.isSelected(); this.visit((node) => { if (node.radiogroup) { return "skip"; // Don't (de)select this branch } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(flag); } }); this.fixSelection3FromEndNodes(); } /** * Fix selection status for multi-hier mode. * Only end-nodes are considered to update the descendants branch and parents. * Should be called after this node has loaded new children or after * children have been modified using the API. */ fixSelection3FromEndNodes(opts?: SetSelectedOptions): void { const force = !!opts?.force; util.assert( this.tree.options.selectMode === "hier", "expected selectMode 'hier'" ); // Visit all end nodes and adjust their parent's `selected` and `_partsel` // attributes. Return selection state true, false, or undefined. const _walk = (node: WunderbaumNode) => { let state; const children = node.children; if (children && children.length) { // check all children recursively let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; // the selection state of a node is not relevant; we need the end-nodes const s = _walk(child); if (s !== false) { someSelected = true; } if (s !== true) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; } else { // This is an end-node: simply report the status state = !!node.selected; } // #939: Keep a `_partsel` flag that was explicitly set on a lazy node if ( node._partsel && !node.selected && node.lazy && node.children == null ) { state = undefined; } if (force || !node.getOption("unselectable")) { node._changeSelectStatusProps(state); } return state; }; _walk(this); // Update parent's state this.visitParents((node) => { let state; const children = node.children!; let allSelected = true; let someSelected = false; for (let i = 0, l = children.length; i < l; i++) { const child = children[i]; state = !!child.selected; // When fixing the parents, we trust the sibling status (i.e. we don't recurse) if (state || child._partsel) { someSelected = true; } if (!state) { allSelected = false; } } state = allSelected ? true : someSelected ? undefined : false; node._changeSelectStatusProps(state); }); } /** Modify the check/uncheck state. */ setSelected( flag: boolean = true, options?: SetSelectedOptions ): TristateType { const tree = this.tree; const sendEvents = !options?.noEvents; // Default: send events const prev = this.isSelected(); const isRadio = this.parent && this.parent.radiogroup; const selectMode = tree.options.selectMode; const canSelect = options?.force || !this.getOption("unselectable"); flag = !!flag; // this.logDebug(`setSelected(${flag})`, this); if (!canSelect) { return prev; } if (options?.propagateDown && selectMode === "multi") { tree.runWithDeferredUpdate(() => { this.visit((node) => { node.setSelected(flag); }); }); return prev; } if ( flag === prev || (sendEvents && this._callEvent("beforeSelect", { flag: flag }) === false) ) { return prev; } tree.runWithDeferredUpdate(() => { if (isRadio) { // Radiobutton Group if (!flag && !options?.force) { return prev; // don't uncheck radio buttons } for (const sibling of this.parent.children!) { sibling.selected = sibling === this; } } else { this.selected = flag; if (selectMode === "hier") { this.fixSelection3AfterClick(); } else if (selectMode === "single" && flag) { tree.visit((n) => { if (n !== this) { n.selected = false; } }); } } }); if (sendEvents) { this._callEvent("select", { flag: flag }); } return prev; } /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */ setStatus( status: NodeStatusType, options?: SetStatusOptions ): WunderbaumNode | null { const tree = this.tree; const message = options?.message; const details = options?.details; let statusNode: WunderbaumNode | null = null; const _clearStatusNode = () => { // Remove dedicated dummy node, if any const children = this.children; if (children && children.length && children[0].isStatusNode()) { children[0].remove(); } }; const _setStatusNode = (data: any) => { // Create/modify the dedicated dummy node for 'loading...' or // 'error!' status. (only called for direct child of the invisible // system root) const children = this.children; const firstChild = children ? children[0] : null; util.assert(data.statusNodeType, "Not a status node"); util.assert( !firstChild || !firstChild.isStatusNode(), "Child must not be a status node" ); statusNode = this.addNode(data, "prependChild"); statusNode.match = -1; // Mark as 'match' to avoid hiding tree.update(ChangeType.structure); return statusNode; }; _clearStatusNode(); switch (status) { case "ok": this._isLoading = false; this._errorInfo = null; break; case "loading": this._isLoading = true; this._errorInfo = null; if (this.parent) { this.update(ChangeType.status); } else { // If this is the invisible root, add a visible top-level node _setStatusNode({ statusNodeType: status, title: tree.options.strings.loading + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, tooltip: details, }); } // this.update(); break; case "error": _setStatusNode({ statusNodeType: status, title: tree.options.strings.loadError + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, // classes: "wb-center", tooltip: details, }); this._isLoading = false; this._errorInfo = { message: message, details: details }; break; case "noData": _setStatusNode({ statusNodeType: status, title: message || tree.options.strings.noData, checkbox: false, colspan: true, tooltip: details, }); this._isLoading = false; this._errorInfo = null; break; default: util.error("invalid node status " + status); } tree.update(ChangeType.structure); return statusNode; } /** Rename this node. */ setTitle(title: string): void { this.title = title; this.update(); // this.triggerModify("rename"); // TODO } /** Set the node tooltip. */ setTooltip(tooltip: TooltipOption): void { this.tooltip = tooltip; this.update(); } /** * Sort child list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren( cmp: SortCallback | null = nodeTitleSorter, deep: boolean = false ): void { this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep }); } /** * Renumber nodes `_nativeIndex`. This is useful to allow to restore the * order after sorting a column. * This method is automatically called after loading new child nodes. * @since 0.11.0 */ resetNativeChildOrder(options?: ResetOrderOptions) { const { recursive = true, propName = "_nativeIndex" } = options ?? {}; if (this.children) { this.children.forEach((child, i) => { child.data[propName] = i; if (recursive && child.children) { child.resetNativeChildOrder(options); } }); } } /** * Convenience method to implement column sorting. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options: SortByPropertyOptions) { this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" }); return this.sort(options); } /** * Implement column sorting. * @since 0.14.0 */ sort(options: SortOptions) { const tree = this.tree; let { propName = undefined, deep = true, key = undefined, order = undefined, caseInsensitive = true, cmp = undefined, // Support click on column sort header: updateColInfo = false, nativeOrderPropName = "_nativeIndex", colId = undefined, } = options; propName ??= colId; if (propName === "*") { propName = "title"; } const isFolder = tree.options.sortFoldersFirst === true ? (node: WunderbaumNode) => node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER : tree.options.sortFoldersFirst; if (updateColInfo) { const colDef = this.tree["_columnsById"][options.colId!]; util.assert(colDef, `Invalid colId specified: ${options.colId}`); order ??= util.rotate(colDef.sortOrder, ["asc", "desc", undefined]); for (const col of this.tree.columns) { col.sortOrder = col === colDef ? order : undefined; } if (order === undefined) { propName = nativeOrderPropName; order = "asc"; } this.tree.update(ChangeType.colStructure); } else { propName ??= "title"; order ??= "asc"; } this.logDebug(`sort(), propName=${propName}, ${order}`, options); util.assert(propName || cmp || key, "No `propName` or `key` specified"); // Define a key callback from the parameters we have if (key == null && cmp == null) { key = (node) => { let val; if (NODE_DICT_PROPS.has(propName)) { val = node[propName as keyof WunderbaumNode]; } else { val = node.data[propName!]; } if (caseInsensitive && typeof val === "string") { val = val.toLowerCase(); } return val; }; } // Define a compare callback that uses the key callback if (cmp) { util.assert(!key, "`key` and `cmp` are mutually exclusive"); tree.logDeprecate("SortOptions.cmp", { since: "0.14.0", hint: "use the `key` callback instead", }); } else { if (options.propName || options.caseInsensitive) { tree.logWarn("sort(): ignoring propName, caseInsensitive"); } cmp = (a, b) => { if (isFolder) { const isFolderA = isFolder(a); if (isFolderA !== isFolder(b)) { return isFolderA ? -1 : 1; } } let x = key!(a); let y = key!(b); // Assure we have reasonable comparisons with null values: if (x == null) { x = typeof y === "string" ? "" : 0; } else if (typeof x === "boolean") { x = x ? 1 : 0; } if (y == null) { y = typeof x === "string" ? "" : 0; } else if (typeof y === "boolean") { y = y ? 1 : 0; } if (order === "desc") { return x === y ? 0 : x > y ? -1 : 1; } return x === y ? 0 : x > y ? 1 : -1; }; } function _sortChildren(cl: WunderbaumNode[]): void { if (!cl) { return; } cl.sort(cmp); if (deep) { for (let i = 0, l = cl.length; i < l; i++) { if (cl[i].children) { _sortChildren(cl[i].children!); } } } } if (this.children) { _sortChildren(this.children); } this.tree.update(ChangeType.structure); // this.triggerModify("sort"); // TODO } /** * Re-apply current sorting if any (use after lazy load). * Example: * ```js * load: function (e) { * // Whe loading a lazy branch, apply current sort order if any * e.node.resort(); * }, * ``` * @since 0.14.0 */ resort(options: SortOptions = {}): void { for (const colDef of this.tree.columns) { if (colDef.sortOrder) { options.colId = colDef.id; options.order = colDef.sortOrder; this.sort(options); break; } } } /** * Trigger `modifyChild` event on a parent to signal that a child was modified. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... */ triggerModifyChild( operation: string, child: WunderbaumNode | null, extra?: any ) { this.logDebug(`modifyChild(${operation})`, extra, child); if (!this.tree.options.modifyChild) { return; } if (child && child.parent !== this) { util.error("child " + child + " is not a child of " + this); } this._callEvent( "modifyChild", util.extend({ operation: operation, child: child }, extra) ); } /** * Trigger `modifyChild` event on node.parent(!). * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... * @param {object} [extra] */ triggerModify(operation: string, extra?: any) { // if (!this.parent) { // return; // } this.parent.triggerModifyChild(operation, this, extra); } /** * Call `callback(node)` for all descendant nodes in hierarchical order (depth-first, pre-order). * * Stop iteration, if fn() returns false. Skip current branch, if fn() * returns "skip".
    * Return false if iteration was stopped. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * its children only. * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link Wunderbaum.visit}. */ visit( callback: NodeVisitCallback, includeSelf: boolean = false ): NodeVisitResponse { let res: any = true; const children = this.children; if (includeSelf === true) { res = callback(this); if (res === false || res === "skip") { return res; } } if (children) { for (let i = 0, l = children.length; i < l; i++) { res = children[i].visit(callback, true); if (res === false) { break; } } } return res; } /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.
    * Stop iteration, if callback() returns false.
    * Return false if iteration was stopped. * * @param callback the callback function. Return false to stop iteration */ visitParents( callback: (node: WunderbaumNode) => boolean | void, includeSelf: boolean = false ): boolean { if (includeSelf && callback(this) === false) { return false; } let p = this.parent; while (p) { if (callback(p) === false) { return false; } p = p.parent; } return true; } /** * Call fn(node) for all sibling nodes.
    * Stop iteration, if fn() returns false.
    * Return false if iteration was stopped. * * @param callback the callback function. * Return false to stop iteration. * @param includeSelf include this node in the iteration. */ visitSiblings( callback: (node: WunderbaumNode) => boolean | void, includeSelf: boolean = false ): boolean { const ac = this.parent.children!; for (let i = 0, l = ac.length; i < l; i++) { const n = ac[i]; if (includeSelf || n !== this) { if (callback(n) === false) { return false; } } } return true; } /** * [ext-filter] Return true if this node is matched by current filter (or no filter is active). */ isMatched() { return !(this.tree.filterMode && !this.match); } } ================================================ FILE: src/wb_options.ts ================================================ /*! * Wunderbaum - options * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ import { ColumnDefinitionList, DndOptionsType, DynamicBoolOption, DynamicBoolOrStringOption, DynamicCheckboxOption, DynamicIconOption, EditOptionsType, FilterOptionsType, IconMapType, // GridOptionsType, // KeynavOptionsType, // LoggerOptionsType, NavModeEnum, NodeTypeDefinitionMap, SelectModeType, SourceType, TranslationsType, WbActivateEventType, WbButtonClickEventType, WbCancelableEventResultType, WbChangeEventType, WbClickEventType, WbDeactivateEventType, WbErrorEventType, WbExpandEventType, WbIconBadgeCallback, WbIconBadgeEventResultType, WbInitEventType, WbKeydownEventType, WbNodeEventType, WbReceiveEventType, WbRenderEventType, WbSelectEventType, WbTreeEventType, } from "./types"; /** * Properties of {@link wunderbaum.Wunderbaum.options}. * * This is similar, but not identical, to the options that can be passed to the * constructor(@see {@link InitWunderbaumOptions}). */ export interface WunderbaumOptions { /** * If true, add a `wb-skeleton` class to all nodes, that will result in a * 'glow' effect. Typically used with initial dummy nodes, while loading the * real data. * @default false. */ skeleton: boolean; /** * Translation map for some system messages. */ strings: TranslationsType; /** * 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose * @default 3 (4 in local debug environment) */ debugLevel: number; /** * Number of levels that are forced to be expanded, and have no expander icon. * E.g. 1 would keep all toplevel nodes expanded. * @default 0 */ minExpandLevel: number; /** * If true, allow to expand parent nodes, even if `node.children` conatains * an empty array (`[]`). This is the the behavior of macOS Finder, for example. * @default false */ emptyChildListExpandable: boolean; // escapeTitles: boolean; // /** // * Height of the header row div. // * @default 22 // */ // headerHeightPx: number; /** * Height of a node row div. * @default 22 */ rowHeightPx: number; /** * Icon font definition. May be a string (e.g. "fontawesome6" or "bootstrap") * or a map of `iconName: iconClass` pairs. * Note: the icon font must be loaded separately. * In order to only override some defauöt icons, use this pattern: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` * @default "bootstrap" */ iconMap: string | IconMapType; /** * Collapse siblings when a node is expanded. * @default false */ autoCollapse: boolean; /** * If true, the tree will automatically adjust its height to fit the parent * container. This is useful when the tree is embedded in a container with * a fixed or calculated sized. * If the parent container is unsized (e.g. grows with its content, `height: auto`), * then we can define a `height: ...` or `max_height: ...` style on the parent. * To avoid a recursive resize-loop, it may be helpful to set `overflow: hidden` * on the parent container. * * Set this option to `false` will disable auto-resizing. * * @default: true */ adjustHeight: boolean; /** * HTMLElement or selector that receives the top nodes breadcrumb. * @default undefined */ connectTopBreadcrumb: HTMLElement | string | null; /** * @default NavModeEnum.startRow */ navigationModeOption: NavModeEnum; /** * Show/hide header (default: null) * null: assume false for plain tree and true for grids. * string: use text as header (only for plain trees) * true: display a header (use tree's id as text for plain trees) * false: do not display a header */ header: boolean | string | null; /** * Show a `` element while loading data. * @default false. */ showSpinner: boolean; /** * Generate missing keys by hashing a combination of refKey (or title) and * the parent key. This is useful when the source data does not contain unique * keys but we want stable keys for persisting the active node, selection or * expansion state. Note that this still assumes that the same refKey must not * appear twice in the same parent node. * @default false. */ autoKeys: boolean; /** * If true, render a checkbox before the node tile to allow selection with the * mouse. Pass `"radio"` to render a radio button instead. * @default false. */ checkbox: DynamicCheckboxOption; /** Optional callback to render icons per node. */ icon?: DynamicIconOption; /** Optional callback to render a tooltip for the icon. */ iconTooltip?: DynamicBoolOrStringOption; /** Optional callback to render a tooltip for the node title. * Pass `true` to use the node's `title` property as tooltip. */ tooltip?: DynamicBoolOrStringOption; /** Optional callback to make a node unselectable. */ unselectable?: DynamicBoolOption; // /** // * @default 200 // */ // updateThrottleWait?: number; /** * @default true */ enabled: boolean; /** * * @default false */ fixedCol: boolean; /** * Default value for ColumnDefinition.filterable option. * @default false * @since 0.11.0 */ columnsFilterable: boolean; /** * Default value for ColumnDefinition.menu option. * @default false * @since 0.11.0 */ columnsMenu: boolean; /** * Default value for ColumnDefinition.resizable option. * @default false * @since 0.10.0 */ columnsResizable?: boolean; /** * Default value for ColumnDefinition.sortable option. * @default false * @since 0.11.0 */ columnsSortable?: boolean; /** * Group nodes with children or of `type: 'folder'` at the top when sorting. * If a function is passed, it is called with the node as argument to determine * whether the node is a folder or not. The function should return `true` for * folders. * and should return `true` for folders. * @default false * @since 0.14.0 */ sortFoldersFirst?: DynamicBoolOption; // --- Selection --- /** * @default "multi" */ selectMode: SelectModeType; // --- KeyNav --- /** * @default true */ quicksearch: boolean; /** * Scroll Node into view on Expand Click * @default true */ scrollIntoViewOnExpandClick: boolean; // --- Extensions ------------------------------------------------------------ /** Configuration options for the drag-and-drop extension. */ dnd: DndOptionsType; /** Configuration options for the edit-title extension. */ edit: EditOptionsType; /** Configuration options for the node-filter extension. */ filter: FilterOptionsType; // grid?: GridOptionsType; // keynav?: KeynavOptionsType; // logger?: LoggerOptionsType; // --- Events ---------------------------------------------------------------- /** * `e.node` was activated. * @category Callback */ activate?: (e: WbActivateEventType) => void; /** * `e.node` is about to be activated. * Return `false` to prevent default handling, i.e. activating the node. * See also `deactivate` event. * @category Callback */ beforeActivate?: (e: WbActivateEventType) => WbCancelableEventResultType; /** * `e.node` is about to be expanded/collapsed. * Return `false` to prevent default handling, i.e. expanding/collapsing the node. * @category Callback */ beforeExpand?: (e: WbExpandEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default handling, i.e. (de)selecting the node. * @category Callback */ beforeSelect?: (e: WbSelectEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default handling, i.e. (de)selecting the node. * @category Callback */ buttonClick?: (e: WbButtonClickEventType) => void; /** * * @category Callback */ change?: (e: WbChangeEventType) => void; /** * * Return `false` to prevent default behavior, e.g. expand/collapse, (de)selection, or activation. * @category Callback */ click?: (e: WbClickEventType) => WbCancelableEventResultType; /** * Return `false` to prevent default behavior, e.g. expand/collapse. * @category Callback */ dblclick?: (e: WbClickEventType) => WbCancelableEventResultType; /** * `e.node` was deactivated. * * Return `false` to prevent default handling, e.g. deactivating the node * and activating the next. * See also `activate` event. * @category Callback */ deactivate?: (e: WbDeactivateEventType) => WbCancelableEventResultType; /** * `e.node` was discarded from the viewport and its HTML markup removed. * @category Callback */ discard?: (e: WbNodeEventType) => void; /** * `e.node` is about to be rendered. We can add a badge to the icon cell here. * @category Callback */ iconBadge?: (e: WbIconBadgeCallback) => WbIconBadgeEventResultType; /** * An error occurred, e.g. during initialization or lazy loading. * @category Callback */ error?: (e: WbErrorEventType) => void; /** * `e.node` was expanded (`e.flag === true`) or collapsed (`e.flag === false`) * @category Callback */ expand?: (e: WbTreeEventType) => void; /** * The tree received or lost focus. * Check `e.flag` for status. * @category Callback */ focus?: (e: WbTreeEventType) => void; /** * Fires when the tree markup was created and the initial source data was loaded. * Typical use cases would be activating a node, setting focus, enabling other * controls on the page, etc.
    * Also sent if an error occured during initialization (check `e.error` for status). * @category Callback */ init?: (e: WbInitEventType) => void; /** * Fires when a key was pressed while the tree has focus. * `e.node` is set if a node is currently active. * Return `false` to prevent default navigation. * @category Callback */ keydown?: (e: WbKeydownEventType) => WbCancelableEventResultType; /** * Fires when a node that was marked 'lazy', is expanded for the first time. * Typically we return an endpoint URL or the Promise of a fetch request that * provides a (potentially nested) list of child nodes. * @category Callback */ lazyLoad?: (e: WbNodeEventType) => void; /** * Fires when data was loaded (initial request, reload, or lazy loading), * after the data is applied and rendered. * @category Callback */ load?: (e: WbNodeEventType) => void; /** * @category Callback */ modifyChild?: (e: WbNodeEventType) => void; /** * Fires when data was fetched (initial request, reload, or lazy loading), * but before the data is applied and rendered. * Here we can modify and adjust the received data, for example to convert an * external response to native Wunderbaum syntax. * @category Callback */ receive?: (e: WbReceiveEventType) => void; /** * Fires when a node is about to be displayed. * The default HTML markup is already created, but not yet added to the DOM. * Now we can tweak the markup, create HTML elements in this node's column * cells, etc. * See also `Custom Rendering` for details. * @category Callback */ render?: (e: WbRenderEventType) => void; /** * Same as `render(e)`, but for the status nodes, i.e. `e.node.statusNodeType`. * @category Callback */ renderStatusNode?: (e: WbRenderEventType) => void; /** *`e.node` was selected (`e.flag === true`) or deselected (`e.flag === false`) * @category Callback */ select?: (e: WbNodeEventType) => void; /** * Fires when the viewport content was updated, after scroling, expanding etc. * @category Callback */ update?: (e: WbTreeEventType) => void; } /** * Available options for {@link wunderbaum.Wunderbaum}. * * Options are passed to the constructor as plain object: * * ```js * const tree = new mar10.Wunderbaum({ * id: "demo", * element: document.getElementById("demo-tree"), * source: "url/of/data/request", * ... * }); * ``` * * Event handlers are also passed as callbacks * * ```js * const tree = new mar10.Wunderbaum({ * ... * init: (e) => { * console.log(`Tree ${e.tree} was initialized and loaded.`) * }, * activate: (e) => { * console.log(`Node ${e.node} was activated.`) * }, * ... * }); * ``` * * Most of the properties are optional and have resonable default. * They are then available as {@link Wunderbaum.options} property and can be * changed at runtime.
    * Only the `element` option is mandatory. * * Note that some options passed here, are *not* available as {@link Wunderbaum.options}. * They are moved to the `tree` instance instead: * - `tree.element` * - `tree.id` * - `tree.columns` * - `tree.types` * - ... * * Some options are only used during initialization and are not stored in the * tree instance: * - `source` * */ export interface InitWunderbaumOptions extends Partial { /** * The target `div` element (or selector) that shall become a Wunderbaum. */ element: string | HTMLDivElement; /** * The identifier of this tree. Used to reference the instance, especially * when multiple trees are present (e.g. `tree = mar10.Wunderbaum.getTree("demo")`). * * @default `"wb_" + COUNTER`. */ id?: string; /** * A list of maps that define column headers. If this option is set, * Wunderbaum becomes a treegrid control instead of a plain tree. * Column definitions can be passed as tree option, or be part of a `source` * response. * @default `[]` meaning this is a plain tree. */ columns?: ColumnDefinitionList; /** * Define shared attributes for multiple nodes of the same type. * This allows for more compact data models. Type definitions can be passed * as tree option, or be part of a `source` response. * * @default `{}`. */ types?: NodeTypeDefinitionMap; /** * Define the initial tree data. Typically a URL of an endpoint that serves * a JSON formatted structure, but also a callback, Promise, or static data * is allowed. * * @default `[]`. */ source?: SourceType; } ================================================ FILE: src/wunderbaum.scss ================================================ /*! * Wunderbaum style sheet (generated from wunderbaum.scss) * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ // @use "sass:meta"; @use "sass:color"; @use "sass:list"; // ---------------------------------------------------------------------------- // --- Define default colors and settings $font-stack: Helvetica, sans-serif; // Basic Theme Colors $error-color: #b5373b; $node-text-color: #56534c; $border-color: $node-text-color; $bg-highlight-color: #26a0da; $header-color: #dedede; $background-color: #ffffff; $alternate-row-color: #f7f7f7; // #fcfcfc; $alternate-row-color-hover: #f3f3f3; //#f7fcfe; $focus-border-color: #275dc5; // derived $drop-source-color: color.adjust($node-text-color, $lightness: 50%); $drop-target-color: color.adjust($bg-highlight-color, $lightness: 40%); $dim-color: color.adjust($node-text-color, $lightness: 20%); $error-background-color: color.adjust($error-color, $lightness: 45%); // @debug $dim-color; $hover-color: color.adjust($bg-highlight-color, $lightness: 48%); // #f7f7f7; $hover-border-color: $hover-color; $grid-color: #dedede; $active-color: #e5f3fb; $active-cell-color: color.adjust($bg-highlight-color, $lightness: 20%); $active-border-color: #70c0e7; $active-hover-color: #dceff8; $active-hover-border-color: $bg-highlight-color; $active-column-color: $hover-color; $active-header-column-color: color.adjust($header-color, $lightness: -10%); $active-color-grayscale: color.grayscale($active-color); $active-border-color-grayscale: color.grayscale($active-border-color); $active-hover-color-grayscale: color.grayscale($active-hover-color); $active-cell-color-grayscale: color.grayscale($active-cell-color); $grid-color-grayscale: color.grayscale($grid-color); // @debug $active-header-column-color; $filter-dim-color: #dedede; $filter-submatch-color: #868581; $row-outer-height: 22px; $row-inner-height: $row-outer-height - 2; // outer height minus border size $row-padding-y: calc(($row-outer-height - $row-inner-height) / 2); $col-padding-x: 2px; // on each side within span.wb-col $icon-outer-height: $row-inner-height; $icon-outer-width: 20px; $icon-height: 16px; $icon-width: 16px; $icon-padding-y: calc(($icon-outer-height - $icon-height) / 2); $icon-padding-x: calc(($icon-outer-width - $icon-width) / 2); $header-height: $row-outer-height; // PyCharm: // $level-rainbow: rgba(255, 255, 232, 1), rgba(240, 255, 240, 1), // rgba(255, 240, 255, 1), rgba(234, 253, 253, 1); // VS-Code_ // $level-rainbow: rgba(255, 255, 64, 0.07), rgba(127, 255, 127, 0.07), // rgba(255, 127, 255, 0.07), rgba(79, 236, 236, 0.07); // Slightly stronger* $level-rainbow: rgb(255, 255, 201), rgb(218, 255, 218), rgb(255, 217, 254), rgb(204, 250, 250); // ---------------------------------------------------------------------------- // --- Define CSS variables with calculated default values :root, :host { // :host needed for ShadowDom // TODO: do we need to use 'meta.inspect' in case a font name contains spaces? // --wb-font-stack: #{meta.inspect($font-stack)}; --wb-font-stack: #{$font-stack}; // Basic Theme Colors --wb-error-color: #{$error-color}; --wb-node-text-color: #{$node-text-color}; --wb-border-color: #{$border-color}; --wb-bg-highlight-color: #{$bg-highlight-color}; --wb-header-color: #{$header-color}; --wb-background-color: #{$background-color}; --wb-alternate-row-color: #{$alternate-row-color}; --wb-alternate-row-color-hover: #{$alternate-row-color-hover}; --wb-focus-border-color: #{$focus-border-color}; // derived --wb-drop-source-color: #{$drop-source-color}; --wb-drop-target-color: #{$drop-target-color}; --wb-dim-color: #{$dim-color}; --wb-error-background-color: #{$error-background-color}; --wb-hover-color: #{$hover-color}; --wb-hover-border-color: #{$hover-border-color}; --wb-grid-color: #{$grid-color}; --wb-active-color: #{$active-color}; --wb-active-cell-color: #{$active-cell-color}; --wb-active-border-color: #{$active-border-color}; --wb-active-hover-color: #{$active-hover-color}; --wb-active-hover-border-color: #{$active-hover-border-color}; --wb-active-column-color: #{$active-column-color}; --wb-active-header-column-color: #{$active-header-column-color}; --wb-active-color-grayscale: #{$active-color-grayscale}; --wb-active-border-color-grayscale: #{$active-border-color-grayscale}; --wb-active-hover-color-grayscale: #{$active-hover-color-grayscale}; --wb-active-cell-color-grayscale: #{$active-cell-color-grayscale}; --wb-grid-color-grayscale: #{$grid-color-grayscale}; --wb-filter-dim-color: #{$filter-dim-color}; --wb-filter-submatch-color: #{$filter-submatch-color}; --wb-row-outer-height: #{$row-outer-height}; --wb-row-inner-height: #{$row-inner-height}; --wb-row-padding-y: #{$row-padding-y}; --wb-col-padding-x: #{$col-padding-x}; --wb-icon-outer-height: #{$icon-outer-height}; --wb-icon-outer-width: #{$icon-outer-width}; --wb-icon-height: #{$icon-height}; --wb-icon-width: #{$icon-width}; --wb-icon-padding-y: #{$icon-padding-y}; --wb-icon-padding-x: #{$icon-padding-x}; --wb-header-height: #{$header-height}; } // ---------------------------------------------------------------------------- // --- SCSS Rules div.wunderbaum * { box-sizing: border-box; } div.wunderbaum { height: 100%; // fill parent container min-height: 4px; background-color: var(--wb-background-color); margin: 0; padding: 0; font-family: var(--wb-font-stack); font-size: 14px; color: var(--wb-node-text-color); border: 2px solid var(--wb-border-color); border-radius: 4px; background-clip: content-box; // Keep bg color outside rounded borders? overflow-x: auto; // TODO:auto would be better, but may cause unwanted effects when auto-resizing? // overflow-y: auto; overflow-y: scroll; // scroll-behavior: smooth; // Focus border is generated by the browsers per default: &:focus, &:focus-within { border-color: var(--wb-focus-border-color); // outline-style: none; } &.wb-disabled { opacity: 0.7; pointer-events: none; } div.wb-list-container { position: relative; // overflow: auto; min-height: 4px; // height: calc(100% - #{$header-height}); // didn't work. Using JS instead } /* --- FIXED-COLUMN --- */ div.wb-header { position: sticky; top: 0; z-index: 2; -webkit-user-select: none; /* Safari */ user-select: none; } div.wb-header, div.wb-list-container { overflow: unset; } // } // --- Header and node list --- div.wb-row { position: absolute; width: 100%; height: var(--wb-row-outer-height); line-height: var(--wb-row-outer-height); border: 1px solid transparent; } /* Fixed column must be opaque, i.e. have the bg color set. */ &.wb-fixed-col { // Sticky first column (header and nodes) span.wb-col:first-of-type { position: sticky; left: 0; z-index: 1; background-color: var(--wb-background-color); } div.wb-header { span.wb-col:first-of-type { background-color: var(--wb-header-color); } } div.wb-node-list div.wb-row { &.wb-active span.wb-col:first-of-type, &.wb-selected span.wb-col:first-of-type { background-color: var(--wb-active-color); } &.wb-active:hover span.wb-col:first-of-type, &.wb-selected:hover span.wb-col:first-of-type { background-color: var(--wb-active-hover-color); } &:hover span.wb-col:first-of-type { background-color: var(--wb-hover-color); } } &:not(:focus-within), &:not(:focus) { div.wb-node-list div.wb-row { &.wb-active span.wb-col:first-of-type, &.wb-selected span.wb-col:first-of-type { background-color: var(--wb-active-color-grayscale); border-color: var(--wb-active-border-color-grayscale); &:hover span.wb-col:first-of-type { background-color: var(--wb-active-hover-color-grayscale); } } } } } // --- Node List Only --- // Dim some colors if tree has no focus &:not(:focus-within), &:not(:focus) { div.wb-node-list div.wb-row { &.wb-active, &.wb-selected { background-color: var(--wb-active-color-grayscale); border-color: var(--wb-active-border-color-grayscale); &:hover { background-color: var(--wb-active-hover-color-grayscale); } } } } &.wb-alternate div.wb-node-list { div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected) { background-color: var(--wb-alternate-row-color); &:hover { background-color: var(--wb-alternate-row-color-hover); } } } div.wb-node-list { div.wb-row { &:hover { background-color: var(--wb-hover-color); } &.wb-active, &.wb-selected { background-color: var(--wb-active-color); // border-color: var(--wb-active-border-color); &:hover { background-color: var(--wb-active-hover-color); // border-color: var(--wb-active-hover-border-color); } } &.wb-focus:not(.wb-active) { border-style: dotted; border-color: var(--wb-active-border-color); } &.wb-active { // background-color: var(--wb-active-hover-color); border-style: solid; border-color: var(--wb-active-border-color); &:hover { // background-color: var(--wb-active-hover-color); border-color: var(--wb-active-hover-border-color); } } &.wb-loading { font-style: italic; } &.wb-busy, i.wb-busy, .wb-col.wb-busy { font-style: italic; // For our stripe pattern, we want a frictionless transition at // the bottom, when repeating in y-direction: // h: height of row = 22px // d: spacing between stripe pairs (white + gray) // Calculate the spacing that will lead to a minimal quadratic // pattern with a height of h pixels: // // (2 * d)^2 = 2 * h^2 // => d = sqrt(2 * h^2) / 2 // = 15.56px // We use the half distance to get a finer stripe pattern: // d/2 = 7.78 background: repeating-linear-gradient( 45deg, // $hover-color, // $hover-color 3.88px, transparent, transparent 3.88px, var(--wb-grid-color) 3.88px, var(--wb-grid-color) 7.78px ); animation: wb-busy-animation 2s linear infinite; } &.wb-error, &.wb-status-error { color: var(--wb-error-color); } } } // --- HEADER --- div.wb-header { position: sticky; height: var(--wb-header-height); border-bottom: 1px solid var(--wb-border-color); padding: 0; background-color: var(--wb-header-color); span.wb-col { font-weight: bold; // &::after { // content: "|"; // } overflow: visible; // allow resizer to overlap next col } // contains and span.wb-col-title { width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } span.wb-col-resizer { position: absolute; top: 0; // left: auto; right: -1px; width: 3px; // float: right; border: none; border-right: 2px solid var(--wb-border-color); height: 100%; -webkit-user-select: none; // Safari user-select: none; &.wb-col-resizer-active { cursor: col-resize; // border-right-color: red; } } i.wb-col-icon { float: inline-end; padding-left: 2px; &:hover { cursor: pointer; color: var(--wb-focus-border-color); } } } span.wb-col { position: absolute; display: inline-block; // text-overflow: ellipsis; overflow: hidden; height: var(--wb-row-inner-height); line-height: var(--wb-row-inner-height); padding: 0 var(--wb-col-padding-x); border-right: 1px solid var(--wb-grid-color); white-space: nowrap; &:last-of-type { border-right: none; } } span.wb-node { -webkit-user-select: none; // Safari user-select: none; // &:first-of-type { // margin-left: 8px; // leftmost icon gets a little margin // } i.wb-checkbox, i.wb-expander, i.wb-icon, i.wb-indent { height: var(--wb-icon-outer-height); width: var(--wb-icon-outer-width); padding: var(--wb-icon-padding-y) var(--wb-icon-padding-x); display: inline-block; } i.wb-expander, i.wb-icon { background-repeat: no-repeat; background-size: contain; } /* Fix Bootstrap Icon alignment */ i.bi::before { vertical-align: baseline; } img.wb-icon { width: var(--wb-icon-width); height: var(--wb-icon-height); padding: var(--wb-icon-padding-y) var(--wb-icon-padding-x); // margin-top: var(--wb-icon-padding-y); } i.wb-indent { // border-left: 1px solid gray; &::before { content: "\00a0"; } } i.wb-expander.wb-spin, i.wb-icon.wb-spin { height: unset; // Prevent wobble width: unset; padding: 0 3px; animation: wb-spin-animation 2s linear infinite; } span.wb-title { min-width: 1em; // display: inline-block; // fix vertical offset on Chrome? vertical-align: top; overflow-x: hidden; display: inline-block; white-space: nowrap; text-overflow: ellipsis; } } /* --- GRID --- */ &.wb-grid { div.wb-header { div.wb-row { span.wb-col { &:hover { background-color: var(--wb-active-header-column-color); } } } } &.wb-cell-mode div.wb-header div.wb-row span.wb-col { &.wb-active { background-color: var(--wb-active-hover-color); } } div.wb-node-list div.wb-row { border-bottom-color: var(--wb-grid-color); &:hover:not(.wb-active):not(.wb-selected) { background-color: var(--wb-hover-color); // border-color: var(--wb-hover-border-color); } &.wb-active { border-bottom-color: var(--wb-active-border-color); } span.wb-col { border-right: 1px solid var(--wb-grid-color); input.wb-input-edit, > input[type="color"], > input[type="date"], > input[type="datetime"], > input[type="datetime-local"], > input[type="email"], > input[type="month"], > input[type="number"], > input[type="password"], > input[type="search"], > input[type="tel"], > input[type="text"], > input[type="time"], > input[type="url"], > input[type="week"], > select { width: 100%; max-height: var(--wb-row-inner-height); border: none; } > input:focus, > select:focus { border: 1px dashed $active-border-color; } } // input[type="number"]::-webkit-inner-spin-button, // input[type="number"]::-webkit-outer-spin-button { // -webkit-appearance: none; // margin: 0; // } } &.wb-cell-mode { div.wb-row:not(.wb-colspan) { &.wb-active { span.wb-col.wb-active { background-color: var(--wb-active-cell-color-grayscale); } } } } &.wb-cell-mode:focus-within, &.wb-cell-mode:focus { div.wb-row:not(.wb-colspan):not(.wb-selected) { // background-color: var(--wb-background-color); span.wb-col.wb-active { background-color: var(--wb-active-column-color); } &.wb-active { background-color: var(--wb-active-column-color); span.wb-col.wb-active { background-color: var(--wb-active-cell-color); } } } } &.wb-alternate div.wb-node-list { div.wb-row:nth-of-type(even):not(.wb-active):not(.wb-selected) { background-color: var(--wb-alternate-row-color); &:hover { background-color: var(--wb-alternate-row-color-hover); } } } // Dim some colors if tree has no focus &:not(:focus-within), &:not(:focus) { div.wb-node-list div.wb-row { border-bottom-color: var(--wb-grid-color-grayscale); } } } /* --- FILTER --- */ &.wb-ext-filter-dim, &.wb-ext-filter-hide { div.wb-node-list div.wb-row { color: var(--wb-filter-dim-color); &.wb-submatch { color: var(--wb-filter-submatch-color); } &.wb-match { color: var(--wb-node-text-color); } } } // Hide expanders for nodes that don't have matching children &.wb-ext-filter-hide.wb-ext-filter-hide-expanders { div.wb-node-list div.wb-row:not(.wb-submatch) i.wb-expander { visibility: hidden; } } // /* --- INPUT controls in grid --- */ // &.wb-lean-input div.wb-node-list div.wb-row span.wb-col { // input { // border-color: red; // } // } /* --- DND --- */ div.wb-row.wb-drag-source { opacity: 0.5; .wb-node { background-color: var(--wb-drop-source-color); } } div.wb-row.wb-drop-target { overflow: visible; .wb-node { background-color: var(--wb-drop-target-color); overflow: visible; // z-index: 1000; .wb-icon { position: relative; overflow: visible; &::after { // use ::after, because ::before is used by icon fonts // vertical-align: baseline; position: absolute; z-index: 1000; // TODO: should be done automatically via rollup plugin: // content: url(../docs/assets/drop_marker_16x32.png); content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAMAAABA3o1rAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKUExURe/v9/f39+//7+f35+f/79bW5wgIawwYd97e55Tnpc731rjA2d7350LOY1LWa7Xvvf///wAQcyAze97e773vxnuczgA5pQBCpdb33rXvxu//9whjxgBaxlKU1oOz5ABz3gB73tbn99bW1rXe/wCM9xiU997v/97e3gCc/xil/9bv/wic/+/3/wAAALM9X5QAAAAudFJOU////////////////////////////////////////////////////////////wCCj3NVAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAqUlEQVQoU6WQ2w6CMAxA54agsCHq1HlFBREv/f/fs1tHAoaoiedlbXrWtGXwhV8FNqAXuAi4DwkShmE0cgGIcSwCCgkSkrAxpEonot0DhQxJptFsbnOpdNdgsFh6VtYwyqzTmG+oijDY7hr22E4qY7QybeGQe46nsxP0Wwc3Q1GWl+qKec8MlqKubxX+xzV7tkDuD1+3d+heigT2zGx/hCMUeUj4wL8CwAsW1kqCTugMCwAAAABJRU5ErkJggg==); left: 0; //-$icon-outer-width; top: calc(($row-outer-height - var(--wb-icon-height)) / 2); } } } } // div.wb-row.wb-drop-target.wb-drop-after .wb-node { // border-bottom: 1px dotted #b5373b; // translate: 0 3px; // } // div.wb-row.wb-drop-target.wb-drop-before .wb-node, // div.wb-row.wb-drop-target.wb-drop-after + div.wb-row .wb-node { // border-top: 1px dotted #b5373b; // translate: 0 -3px; // } div.wb-row.wb-drop-target.wb-drop-before .wb-node .wb-icon::after, div.wb-row.wb-drop-target.wb-drop-after .wb-node .wb-icon::after { // TODO: should be done automatically via rollup plugin: // content: url(../docs/assets/drop_marker_insert_16x64.png); content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAMAAACROYkbAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACNUExURe/v9/f39+//7+f35+f/79bW5wgIawwYd97e55Tnpc731rjA2d7350LOY1LWa7Xvvf///wAQcyAze97e773vxgAAAHuczgA5pQBCpdb33rXvxu//9whjxgBaxlKU1oOz5ABz3gB73tbn99bW1rXe/wCM9xiU997v/97e3gCc/xil/9bv/wic/+/3/wAAAParqS4AAAAvdFJOU/////////////////////////////////////////////////////////////8AWqU49wAAAAlwSFlzAAAOwwAADsMBx2+oZAAAALlJREFUOE/FktsSgiAQhglMS8WstKLzQTM77Ps/XguL16I208cFyzB8/LPAYCC/ErARzcCFx23pBgnGfjAxBYhpKDwq3SBB5DeGWCYz0SUDClIkmgeLpV7HMiNDbrbbYbBaWzbaoKTaJiHfQe5oYLA/NBwxTiyVyqTSghYwox4MTmfL5XozgqxjAtODoizv1QPXPXqgKer6WeH9+Iw9XgF5ve15/Q+6/SQSsE+q8yMcocoREgzg3wKAL4vrpBIKREShAAAAAElFTkSuQmCC); left: 0; // $icon-outer-width * 1.5; top: calc( ($row-outer-height - var(--wb-icon-height)) / 2 - $row-outer-height / 2 ); } div.wb-row.wb-drop-target.wb-drop-after .wb-node .wb-icon::after { top: calc( ($row-outer-height - var(--wb-icon-height)) / 2 + $row-outer-height / 2 ); } /* --- SPECIAL EFFECTS --- */ /* Colorize indentation levels. */ &.wb-rainbow { @for $i from 1 through list.length($level-rainbow) { i.wb-expander:nth-child(#{list.length($level-rainbow)}n + #{$i}), i.wb-indent:nth-child(#{list.length($level-rainbow)}n + #{$i}) { background: list.nth($level-rainbow, $i); } } } /* Fade out expanders, when container is not hovered or active */ &.wb-fade-expander { i.wb-expander { // only text-alpha is animated, since we want to keep the background color // transition: opacity 1.5s; // opacity: 0; transition: color 1.5s; color: rgba($node-text-color, 0); } div.wb-row.wb-loading i.wb-expander, &:hover i.wb-expander, &:focus i.wb-expander, &:focus-within i.wb-expander, [class*="wb-statusnode-"] i.wb-expander { transition: color 0.6s; color: var(--wb-node-text-color); } } /* Skeleton */ div.wb-row.wb-skeleton { span.wb-title, i.wb-icon { animation: wb-skeleton-animation 1s linear infinite alternate; border-radius: 0.25em; color: transparent; opacity: 0.7; // max-width: 20px; } } /* Auto-hide checkboxes unless selected or hovered */ &.wb-checkbox-auto-hide { i.wb-checkbox { visibility: hidden; } .wb-row:hover i.wb-checkbox, .wb-row.wb-selected i.wb-checkbox { visibility: unset; } &:focus, &:focus-within { .wb-row.wb-active i.wb-checkbox { visibility: unset; } } } } /* --- TOOL CLASSES --- */ a.wb-breadcrumb { cursor: pointer; text-decoration: none; } .wb-helper-center { text-align: center; } .wb-helper-disabled { color: var(--wb-dim-color); } .wb-helper-hidden { display: none; } .wb-helper-invalid { color: var(--wb-error-color); } .wb-helper-lazy-expander { color: var(--wb-bg-highlight-color); } .wb-helper-link { cursor: pointer; } .wb-no-select { -webkit-user-select: none; // Safari user-select: none; span.wb-title { -webkit-user-select: contain; // Safari user-select: contain; } } // .wb-helper-edit-text { // // content-editable: true; // } button.wb-filter-hide { font-weight: bolder; // background-color: var(--wb-active-color); // -webkit-appearance: auto; /* Removes default Safari styles */ } /* RTL support */ .wb-helper-start, .wb-helper-start > input { text-align: left; } .wb-helper-end, .wb-helper-end > input { text-align: right; } .wb-rtl { .wb-helper-start, .wb-helper-start > input { text-align: right; } .wb-helper-end, .wb-helper-end > input { text-align: left; } } i.wb-icon { position: relative; > span.wb-badge { position: absolute; display: inline-block; top: 0; left: -0.6rem; color: white; //var(--wb-dim-color); background-color: var(--wb-bg-highlight-color); padding: 0.2em 0.3rem 0.1em 0.3rem; font-size: 60%; font-weight: 200; line-height: 1; text-align: center; white-space: nowrap; // vertical-align: baseline; border-radius: 0.5rem; pointer-events: none; } } /* Class 'wb-tristate' is used to mark checkboxes that should toggle like * indeterminate -> checked -> unchecked -> indeterminate ... */ // input[type=checkbox].wb-tristate { // } // tri-state checkbox in indeterminate state .wb-col input[type="checkbox"]:indeterminate { color: var(--wb-dim-color); background-color: red; } .wb-col input:invalid { // border-color: var(--wb-error-color); // color: var(--wb-error-color); background-color: var(--wb-error-background-color); } .wb-col.wb-invalid { border: 1px dotted var(--wb-error-color); // color: var(--wb-error-color); // background-color: var(--wb-error-background-color); } @keyframes wb-spin-animation { 0% { transform: rotate(0deg); } to { transform: rotate(1turn); } } @keyframes wb-skeleton-animation { 0% { background-color: hsl(200, 20%, 70%); } 100% { background-color: hsl(200, 20%, 95%); } } @keyframes wb-busy-animation { 0% { background-position: 0 0; } 100% { background-position: 0 $row-outer-height; } } ================================================ FILE: src/wunderbaum.ts ================================================ /*! * wunderbaum.ts * * A treegrid control. * * Copyright (c) 2021-2025, Martin Wendt (https://wwWendt.de). * https://github.com/mar10/wunderbaum * * Released under the MIT license. * @version @VERSION * @date @DATE */ // import "./wunderbaum.scss"; import * as util from "./util"; import { FilterExtension } from "./wb_ext_filter"; import { KeynavExtension } from "./wb_ext_keynav"; import { LoggerExtension } from "./wb_ext_logger"; import { DndExtension } from "./wb_ext_dnd"; import { GridExtension } from "./wb_ext_grid"; import { ExtensionsDict, WunderbaumExtension } from "./wb_extension_base"; import { AddChildrenOptions, ApplyCommandOptions, ApplyCommandType, ChangeType, ColumnDefinitionList, ExpandAllOptions, FilterModeType, FilterNodesOptions, IconMapType, GetStateOptions, MatcherCallback, NavigationType, NavModeEnum, NodeFilterCallback, NodeRegion, NodeStatusType, NodeStringCallback, NodeToDictCallback, NodeTypeDefinitionMap, NodeVisitCallback, RenderFlag, ScrollToOptions, SetActiveOptions, SetColumnOptions, SetStateOptions, SetStatusOptions, SortCallback, SourceType, TreeStateDefinition, UpdateOptions, VisitRowsOptions, WbEventInfo, WbNodeData, FilterOptionsType, EditOptionsType, DndOptionsType, SortOptions, DeprecationOptions, SortByPropertyOptions, ReloadOptions, LoadLazyNodesOptions, } from "./types"; import { DEFAULT_DEBUGLEVEL, defaultIconMaps, makeNodeTitleStartMatcher, nodeTitleSorter, RENDER_MAX_PREFETCH, DEFAULT_ROW_HEIGHT, TEST_FILE_PATH, TEST_HTML, } from "./common"; import { WunderbaumNode } from "./wb_node"; import { Deferred } from "./deferred"; import { EditExtension } from "./wb_ext_edit"; import { InitWunderbaumOptions, WunderbaumOptions } from "./wb_options"; import { DebouncedFunction } from "./debounce"; class WbSystemRoot extends WunderbaumNode { constructor(tree: Wunderbaum) { super(tree, (null), { key: "__root__", title: tree.id, }); } toString() { return `WbSystemRoot@${this.key}<'${this.tree.id}'>`; } } /** * A persistent plain object or array. * * See also {@link WunderbaumOptions}. */ export class Wunderbaum { protected static sequence = 0; protected enabled = true; /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */ public static version: string = "@VERSION"; // Set to semver by 'grunt release' /** The invisible root node, that holds all visible top level nodes. */ public readonly root: WunderbaumNode; /** Unique tree ID as passed to constructor. Defaults to `"wb_SEQUENCE"`. */ public readonly id: string; /** The `div` container element that was passed to the constructor. */ public readonly element: HTMLDivElement; /** The `div.wb-header` element if any. */ public readonly headerElement: HTMLDivElement; /** The `div.wb-list-container` element that contains the `nodeListElement`. */ public readonly listContainerElement: HTMLDivElement; /** The `div.wb-node-list` element that contains all visible div.wb-row child elements. */ public readonly nodeListElement: HTMLDivElement; /** Contains additional data that was sent as response to an Ajax source load request. */ public readonly data: { [key: string]: any } = {}; protected readonly _updateViewportThrottled: DebouncedFunction<() => void>; protected extensionList: WunderbaumExtension[] = []; protected extensions: ExtensionsDict = {}; /** Merged options from constructor args and tree- and extension defaults. */ public options: WunderbaumOptions; protected keyMap = new Map(); protected refKeyMap = new Map>(); protected treeRowCount = 0; protected _disableUpdateCount = 0; protected _disableUpdateIgnoreCount = 0; protected _activeNode: WunderbaumNode | null = null; protected _focusNode: WunderbaumNode | null = null; protected _initialSource: SourceType | null = null; /** Currently active node if any. * Use {@link WunderbaumNode.setActive|setActive} to modify. */ public get activeNode() { // Check for deleted node, i.e. node.tree === null return this._activeNode?.tree ? this._activeNode : null; } /** Current node hat has keyboard focus if any. * Use {@link WunderbaumNode.setFocus|setFocus()} to modify. */ public get focusNode() { // Check for deleted node, i.e. node.tree === null return this._focusNode?.tree ? this._focusNode : null; } /** Shared properties, referenced by `node.type`. */ public types: NodeTypeDefinitionMap = {}; /** List of column definitions. */ public columns: ColumnDefinitionList = []; protected _columnsById: { [key: string]: any } = {}; protected resizeObserver: ResizeObserver; // Modification Status protected pendingChangeTypes: Set = new Set(); /** A Promise that is resolved when the tree was initialized (similar to `init(e)` event). */ public readonly ready: Promise; /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */ public static util = util; /** A map of default iconMaps. * May be used as default, when passing partial icon definition maps: * ```js * const tree = new mar10.Wunderbaum({ * ... * iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { * folder: "bi bi-archive", * }), * }); * ``` */ public static iconMaps = defaultIconMaps; /** Expose some useful methods of the util.ts module as `tree._util`. */ public _util = util; // --- SELECT --- // public selectRangeAnchor: WunderbaumNode | null = null; // --- BREADCRUMB --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ public breadcrumb: HTMLElement | null = null; // --- FILTER --- /** Filter options (used as defaults for calls to {@link Wunderbaum.filterNodes} ) */ public filterMode: FilterModeType = null; // --- KEYNAV --- /** @internal Use `setColumn()`/`getActiveColElem()` to access. */ public activeColIdx = 0; /** @internal */ public _cellNavMode = false; /** @internal */ public lastQuicksearchTime = 0; /** @internal */ public lastQuicksearchTerm = ""; // --- EDIT --- protected lastClickTime = 0; constructor(options: InitWunderbaumOptions) { // Set default options and merge with user options const initOptions = Object.assign< InitWunderbaumOptions, InitWunderbaumOptions >( { id: undefined, source: [], // URL for GET/PUT, Ajax options, or callback element: util.unsafeCast(null), debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose header: null, // Show/hide header (pass bool or string) rowHeightPx: DEFAULT_ROW_HEIGHT, iconMap: "bootstrap", columns: [], //util.unsafeCast(null), types: {}, enabled: true, fixedCol: false, showSpinner: false, checkbox: false, minExpandLevel: 0, emptyChildListExpandable: false, skeleton: false, autoCollapse: false, adjustHeight: true, connectTopBreadcrumb: null, columnsFilterable: false, columnsMenu: false, columnsResizable: false, columnsSortable: false, selectMode: "multi", // SelectModeType scrollIntoViewOnExpandClick: true, // --- Extensions (actually set by exensions on init) dnd: util.unsafeCast(null), edit: util.unsafeCast(null), filter: util.unsafeCast(null), // --- KeyNav --- navigationModeOption: util.unsafeCast(null), quicksearch: true, // --- Events --- // iconBadge: null, // change: null, // ... // --- Strings --- strings: { loadError: "Error", loading: "Loading...", noData: "No data", breadcrumbDelimiter: " » ", queryResult: "Found ${matches} of ${count}", noMatch: "No results", matchIndex: "${match} of ${matches}", }, }, options ); const opts = initOptions as WunderbaumOptions; this.options = opts; const readyDeferred = new Deferred(); this.ready = readyDeferred.promise(); let readyOk = false; this.ready .then(() => { readyOk = true; try { this._callEvent("init"); } catch (error) { // We re-raise in the reject handler, but Chrome resets the stack // frame then, so we log it here: this.logError("Exception inside `init(e)` event:", error); } }) .catch((err) => { if (readyOk) { // Error occurred in `init` handler. We can re-raise, but Chrome // resets the stack frame. throw err; } else { // Error in load process this._callEvent("init", { error: err }); } }); this.id = initOptions.id || "wb_" + ++Wunderbaum.sequence; delete initOptions.id; this.root = new WbSystemRoot(this); this._registerExtension(new KeynavExtension(this)); this._registerExtension(new EditExtension(this)); this._registerExtension(new FilterExtension(this)); this._registerExtension(new DndExtension(this)); this._registerExtension(new GridExtension(this)); this._registerExtension(new LoggerExtension(this)); this._updateViewportThrottled = util.adaptiveThrottle( this._updateViewportImmediately.bind(this), {} ); // --- Evaluate options this.columns = initOptions.columns || []; delete initOptions.columns; if (!this.columns || !this.columns.length) { const title = typeof opts.header === "string" ? opts.header : this.id; this.columns = [{ id: "*", title: title, width: "*" }]; } if (initOptions.types) { this.setTypes(initOptions.types, true); } delete initOptions.types; // --- Create Markup this.element = util.elemFromSelector(initOptions.element)!; util.assert( !!this.element, `Invalid 'element' option: ${initOptions.element}` ); delete (initOptions).element; this.element.classList.add("wunderbaum"); if (!this.element.getAttribute("tabindex")) { this.element.tabIndex = 0; } if (opts.rowHeightPx !== DEFAULT_ROW_HEIGHT) { this.element.style.setProperty( "--wb-row-outer-height", opts.rowHeightPx + "px" ); this.element.style.setProperty( "--wb-row-inner-height", opts.rowHeightPx - 2 + "px" ); } // Attach tree instance to
    (this.element)._wb_tree = this; // Create header markup, or take it from the existing html this.headerElement = this.element.querySelector("div.wb-header")!; const wantHeader = opts.header == null ? this.columns.length > 1 : !!opts.header; if (this.headerElement) { // User existing header markup to define `this.columns` util.assert( !this.columns, "`opts.columns` must not be set if table markup already contains a header" ); this.columns = []; const rowElement = this.headerElement.querySelector("div.wb-row")!; for (const colDiv of rowElement.querySelectorAll("div")) { this.columns.push({ id: colDiv.dataset.id || `col_${this.columns.length}`, // id: colDiv.dataset.id || null, title: "" + colDiv.textContent, // text: "" + colDiv.textContent, width: "*", // TODO: read from header span }); } } else { // We need a row div, the rest will be computed from `this.columns` const coldivs = "".repeat( this.columns.length ); this.element.innerHTML = `
    ${coldivs}
    `; if (!wantHeader) { const he = this.element.querySelector("div.wb-header")!; he.style.display = "none"; } } // this.element.innerHTML += `
    `; this.listContainerElement = this.element.querySelector( "div.wb-list-container" )!; this.nodeListElement = this.listContainerElement.querySelector( "div.wb-node-list" )!; this.headerElement = this.element.querySelector("div.wb-header")!; this.element.classList.toggle("wb-grid", this.columns.length > 1); if (this.options.connectTopBreadcrumb) { this.breadcrumb = util.elemFromSelector( this.options.connectTopBreadcrumb )!; util.assert( !this.breadcrumb || this.breadcrumb.innerHTML != null, `Invalid 'connectTopBreadcrumb' option: ${this.breadcrumb}.` ); this.breadcrumb.addEventListener("click", (e) => { // const node = Wunderbaum.getNode(e)!; const elem = e.target as HTMLElement; if (elem && elem.matches("a.wb-breadcrumb")) { const node = this.keyMap.get(elem.dataset.key!); node?.setActive(); e.preventDefault(); } }); } this._initExtensions(); // --- apply initial options ["enabled", "fixedCol"].forEach((optName) => { if ((opts as any)[optName] != null) { this.setOption(optName, (opts as any)[optName]); } }); // --- Load initial data if (initOptions.source) { if (opts.showSpinner) { this.nodeListElement.innerHTML = `${opts.strings.loading}`; } this.load(initOptions.source) .then(() => { // The source may have defined columns, so we may adjust the nav mode if (opts.navigationModeOption == null) { if (this.isGrid()) { this.setNavigationOption(NavModeEnum.cell); } else { this.setNavigationOption(NavModeEnum.row); } } else { this.setNavigationOption(opts.navigationModeOption); } this.update(ChangeType.structure, { immediate: true }); readyDeferred.resolve(); }) .catch((error) => { readyDeferred.reject(error); }) .finally(() => { this.element.querySelector("progress.spinner")?.remove(); this.element.classList.remove("wb-initializing"); }); } else { readyDeferred.resolve(); } // Async mode is sometimes required, because this.element.clientWidth // has a wrong value at start??? this.update(ChangeType.any); // --- Bind listeners this._registerEventHandlers(); this.resizeObserver = new ResizeObserver((entries) => { // this.log("ResizeObserver: Size changed", entries); this.update(ChangeType.resize); }); this.resizeObserver.observe(this.element); } private _registerEventHandlers() { this.element.addEventListener("scroll", (e: Event) => { // this.log(`scroll, scrollTop:${e.target.scrollTop}`, e); this.update(ChangeType.scroll); }); util.onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => { const info = Wunderbaum.getEventInfo(e); const command = (e.target)?.dataset?.command; this._callEvent("buttonClick", { event: e, info: info, command: command, }); }); util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; const mouseEvent = e as MouseEvent; // this.log("click", info); if ( this._callEvent("click", { event: e, node: node, info: info }) === false ) { this.lastClickTime = Date.now(); return false; } if (node) { if (mouseEvent.ctrlKey) { node.toggleSelected(); return; } // Edit title if 'clickActive' is triggered: const trigger = this.getOption("edit.trigger"); const slowClickDelay = this.getOption("edit.slowClickDelay"); if ( trigger.indexOf("clickActive") >= 0 && info.region === "title" && node.isActive() && (!slowClickDelay || Date.now() - this.lastClickTime < slowClickDelay) ) { node.startEditTitle(); } if (info.region === NodeRegion.expander) { node.setExpanded(!node.isExpanded(), { scrollIntoView: this.options.scrollIntoViewOnExpandClick !== false, }); } else if (info.region === NodeRegion.checkbox) { node.toggleSelected(); } else { if (info.colIdx >= 0) { node.setActive(true, { colIdx: info.colIdx, event: e }); } else { node.setActive(true, { event: e }); } } } this.lastClickTime = Date.now(); }); util.onEvent(this.nodeListElement, "dblclick", "div.wb-row", (e) => { const info = Wunderbaum.getEventInfo(e); const node = info.node; // this.log("dblclick", info, e); if ( this._callEvent("dblclick", { event: e, node: node, info: info }) === false ) { return false; } if ( node && info.colIdx === 0 && node.isExpandable() && info.region !== NodeRegion.expander ) { this._callMethod("edit._stopEditTitle"); node.setExpanded(!node.isExpanded()); } }); util.onEvent(this.element, "keydown", (e) => { const info = Wunderbaum.getEventInfo(e); const eventName = util.eventToString(e); const node = info.node || this.getFocusNode(); this._callHook("onKeyEvent", { event: e, node: node, info: info, eventName: eventName, }); }); util.onEvent(this.element, "focusin focusout", (e) => { const flag = e.type === "focusin"; const targetNode = Wunderbaum.getNode(e)!; this._callEvent("focus", { flag: flag, event: e }); if (flag && this.isRowNav() && !this.isEditingTitle()) { if (this.options.navigationModeOption === NavModeEnum.row) { targetNode?.setActive(); } else { this.setCellNav(); } } if (!flag) { this._callMethod("edit._stopEditTitle", true, { event: e, forceClose: true, }); } }); } /** * Return a Wunderbaum instance, from element, id, index, or event. * * ```js * getTree(); // Get first Wunderbaum instance on page * getTree(1); // Get second Wunderbaum instance on page * getTree(event); // Get tree for this mouse- or keyboard event * getTree("foo"); // Get tree for this `tree.options.id` * getTree("#tree"); // Get tree for first matching element selector * ``` */ public static getTree( el?: Element | Event | number | string | WunderbaumNode ): Wunderbaum | null { if (el instanceof Wunderbaum) { return el; } else if (el instanceof WunderbaumNode) { return el.tree; } if (el === undefined) { el = 0; // get first tree } if (typeof el === "number") { el = document.querySelectorAll(".wunderbaum")[el]; // el was an integer: return nth element } else if (typeof el === "string") { // Search all trees for matching ID for (const treeElem of document.querySelectorAll(".wunderbaum")) { const tree = (treeElem)._wb_tree; if (tree && tree.id === el) { return tree; } } // Search by selector el = document.querySelector(el)!; if (!el) { return null; } } else if ((el).target) { el = (el).target as Element; } util.assert(el instanceof Element, `Invalid el type: ${el}`); if (!(el).matches(".wunderbaum")) { el = (el).closest(".wunderbaum")!; } if (el && (el)._wb_tree) { return (el)._wb_tree; } return null; } /** * Return the icon-function -> icon-definition mapping. * @deprecated Use {@link Wunderbaum.iconMaps} */ get iconMap(): IconMapType { const map = this.options.iconMap; if (typeof map === "string") { return defaultIconMaps[map as keyof typeof defaultIconMaps]; } return map; } /** * Return a WunderbaumNode instance from element or event. */ public static getNode(el: Element | Event): WunderbaumNode | null { if (!el) { return null; } else if (el instanceof WunderbaumNode) { return el; } else if ((el).target !== undefined) { el = (el).target! as Element; // el was an Event } // `el` is a DOM element // let nodeElem = obj.closest("div.wb-row"); while (el) { if ((el)._wb_node) { return (el)._wb_node as WunderbaumNode; } el = (el).parentElement!; //.parentNode; } return null; } /** * Iterate all descendant nodes depth-first, pre-order using `for ... of ...` syntax. * More concise, but slightly slower than {@link Wunderbaum.visit}. * * Example: * ```js * for(const node of tree) { * ... * } * ``` */ *[Symbol.iterator](): IterableIterator { yield* this.root; } /** @internal */ protected _registerExtension(extension: WunderbaumExtension): void { this.extensionList.push(extension); this.extensions[extension.id] = extension; // this.extensionMap.set(extension.id, extension); } /** Called on tree (re)init after markup is created, before loading. */ protected _initExtensions(): void { for (const ext of this.extensionList) { ext.init(); } } /** * Calculate a *stable*, unique key for a node from its refKey (or title). * We also add information from the parent, because a refKey may occur multiple * times in a tree (but not as child of the same parent). * @internal */ _calculateKey(data: WbNodeData, parent?: WunderbaumNode): string { if (data.key) { // Always use an explicitly passed key return data.key; } // Auto-keys are optional, use a monotonic counter by default: if (!this.options.autoKeys) { return "" + ++WunderbaumNode.sequence; } // Add the parent's key to the hash. Assuming this was generated by the // same algorithm, this should incorporate the whole path: const s = (parent ? parent.key : "") + (data.refKey || data.title); // 32-bit has a high probability of collisions, so we pump up to 64-bit // https://security.stackexchange.com/q/209882/207588 const h1 = util.murmurHash3(s, true); let key = "id_" + h1 + util.murmurHash3(h1 + s, true); // Check for collisions // (Most likely if the same title occurs multiple in the same parent). const existingNode = this.keyMap.get(key); if (existingNode) { key += "." + ++Wunderbaum.sequence; this.logWarn( `Node with existing key: '${existingNode}', using ${key}.`, data ); } return key; } /** Add node to tree's bookkeeping data structures. @internal */ _registerNode(node: WunderbaumNode): void { const key = node.key; util.assert(key != null, `Missing key: '${node}'.`); util.assert(!this.keyMap.has(key), `Duplicate key: '${key}': ${node}.`); this.keyMap.set(key, node); const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); // Set of nodes with this refKey if (rks) { rks.add(node); } else { this.refKeyMap.set(rk, new Set([node])); } } } /** Remove node from tree's bookkeeping data structures. @internal */ _unregisterNode(node: WunderbaumNode): void { // Remove refKey reference from map (if any) const rk = node.refKey; if (rk != null) { const rks = this.refKeyMap.get(rk); if (rks && rks.delete(node) && !rks.size) { // We just removed the last element this.refKeyMap.delete(rk); } } // Remove key reference from map this.keyMap.delete(node.key); // Mark as disposed (node.tree as any) = null; (node.parent as any) = null; // Remove HTML markup node.removeMarkup(); } /** Call all hook methods of all registered extensions.*/ protected _callHook( hook: keyof WunderbaumExtension, data: any = {} ): any { let res; const d = util.extend( {}, { tree: this, options: this.options, result: undefined }, data ); for (const ext of this.extensionList) { res = (ext[hook]).call(ext, d); if (res === false) { break; } if (d.result !== undefined) { res = d.result; } } return res; } /** * Call tree method or extension method if defined. * * Example: * ```js * tree._callMethod("edit.startEdit", "arg1", "arg2") * ``` */ _callMethod(name: string, ...args: any[]): any { const [p, n] = name.split("."); const obj = n ? this.extensions[p] : this; const func = (obj)[n]; if (func) { return func.apply(obj, args); } else { this.logError(`Calling undefined method '${name}()'.`); } } /** * Call event handler if defined in tree or tree.EXTENSION options. * * Example: * ```js * tree._callEvent("edit.beforeEdit", {foo: 42}) * ``` */ _callEvent(type: string, extra?: any): any { const [p, n] = type.split("."); const opts = this.options as any; const func = n ? opts[p][n] : opts[p]; if (func) { return func.call( this, util.extend({ type: type, tree: this, util: this._util }, extra) ); // } else { // this.logError(`Triggering undefined event '${type}'.`) } } /** Return the node for given row index. */ protected _getNodeByRowIdx(idx: number): WunderbaumNode | null { // TODO: start searching from active node (reverse) let node: WunderbaumNode | null = null; this.visitRows((n) => { if (n._rowIdx === idx) { node = n; return false; } }); return node!; } /** Return the topmost visible node in the viewport. * @param complete If `false`, the node is considered visible if at least one * pixel is visible. */ getTopmostVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const gracePx = 1; // ignore subpixel scrolling const scrollParent = this.element; // const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; // + headerHeight; let topIdx: number; if (complete) { topIdx = Math.ceil((scrollTop - gracePx) / rowHeight); } else { topIdx = Math.floor(scrollTop / rowHeight); } return this._getNodeByRowIdx(topIdx)!; } /** Return the lowest visible node in the viewport. */ getLowestVpNode(complete = true) { const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const clientHeight = scrollParent.clientHeight - headerHeight; let bottomIdx: number; if (complete) { bottomIdx = Math.floor((scrollTop + clientHeight) / rowHeight) - 1; } else { bottomIdx = Math.ceil((scrollTop + clientHeight) / rowHeight) - 1; } bottomIdx = Math.min(bottomIdx, this.count(true) - 1); return this._getNodeByRowIdx(bottomIdx)!; } /** Return preceding visible node in the viewport. */ protected _getPrevNodeInView(node?: WunderbaumNode, ofs = 1) { this.visitRows( (n) => { node = n; if (ofs-- <= 0) { return false; } }, { reverse: true, start: node || this.getActiveNode() } ); return node; } /** Return following visible node in the viewport. */ protected _getNextNodeInView( node?: WunderbaumNode, options?: { ofs?: number; reverse?: boolean; cb?: (n: WunderbaumNode) => boolean; } ) { let ofs = options?.ofs || 1; const reverse = !!options?.reverse; this.visitRows( (n) => { node = n; if (options?.cb && options.cb(n)) { return false; } if (ofs-- <= 0) { return false; } }, { reverse: reverse, start: node || this.getActiveNode() } ); return node; } /** * Append (or insert) a list of toplevel nodes. * * @see {@link WunderbaumNode.addChildren} */ addChildren(nodeData: any, options?: AddChildrenOptions): WunderbaumNode { return this.root.addChildren(nodeData, options); } /** * Apply a modification (or navigation) operation on the **tree or active node**. */ applyCommand(cmd: ApplyCommandType, options?: ApplyCommandOptions): any; /** * Apply a modification (or navigation) operation on a **node**. * @see {@link WunderbaumNode.applyCommand} */ applyCommand( cmd: ApplyCommandType, node: WunderbaumNode, options?: ApplyCommandOptions ): any; /** * Apply a modification or navigation operation. * * Most of these commands simply map to a node or tree method. * This method is especially useful when implementing keyboard mapping, * context menus, or external buttons. * * Valid commands: * - 'moveUp', 'moveDown' * - 'indent', 'outdent' * - 'remove' * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate * */ applyCommand( cmd: ApplyCommandType, nodeOrOpts?: WunderbaumNode | any, options?: ApplyCommandOptions ): any { let // clipboard, node, refNode; // options = $.extend( // { setActive: true, clipboard: CLIPBOARD }, // options_ // ); if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { node = this.getActiveNode()!; util.assert(options === undefined, `Unexpected options: ${options}`); options = nodeOrOpts; } // clipboard = options.clipboard; switch (cmd) { // Sorting and indentation: case "moveUp": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "before"); node.setActive(); } break; case "moveDown": refNode = node.getNextSibling(); if (refNode) { node.moveTo(refNode, "after"); node.setActive(); } break; case "indent": refNode = node.getPrevSibling(); if (refNode) { node.moveTo(refNode, "appendChild"); refNode.setExpanded(); node.setActive(); } break; case "outdent": if (!node.isTopLevel()) { node.moveTo(node.getParent()!, "after"); node.setActive(); } break; // Remove: case "remove": refNode = node.getPrevSibling() || node.getParent(); node.remove(); if (refNode) { refNode.setActive(); } break; // Add, edit (requires ext-edit): case "addChild": this._callMethod("edit.createNode", "prependChild"); break; case "addSibling": this._callMethod("edit.createNode", "after"); break; case "rename": node.startEditTitle(); break; // Simple clipboard simulation: // case "cut": // clipboard = { mode: cmd, data: node }; // break; // case "copy": // clipboard = { // mode: cmd, // data: node.toDict(function(d, n) { // delete d.key; // }), // }; // break; // case "clear": // clipboard = null; // break; // case "paste": // if (clipboard.mode === "cut") { // // refNode = node.getPrevSibling(); // clipboard.data.moveTo(node, "child"); // clipboard.data.setActive(); // } else if (clipboard.mode === "copy") { // node.addChildren(clipboard.data).setActive(); // } // break; // Navigation commands: case "down": case "first": case "last": case "left": case "nextMatch": case "pageDown": case "pageUp": case "parent": case "prevMatch": case "right": case "up": return node.navigate(cmd); default: util.error(`Unhandled command: '${cmd}'`); } } /** Delete all nodes. */ clear() { this.root.removeChildren(); this.root.children = null; this.keyMap.clear(); this.refKeyMap.clear(); this.treeRowCount = 0; this._activeNode = null; this._focusNode = null; // this.types = {}; // this. columns =[]; // this._columnsById = {}; // Modification Status // this.changedSince = 0; // this.changes.clear(); // this.changedNodes.clear(); // // --- FILTER --- // public filterMode: FilterModeType = null; // // --- KEYNAV --- // public activeColIdx = 0; // public cellNavMode = false; // public lastQuicksearchTime = 0; // public lastQuicksearchTerm = ""; this.update(ChangeType.structure); } /** * Clear nodes and markup and detach events and observers. * * This method may be useful to free up resources before re-creating a tree * on an existing div, for example in unittest suites. * Note that this Wunderbaum instance becomes unusable afterwards. */ public destroy() { this.logInfo("destroy()..."); this.clear(); this.resizeObserver.disconnect(); this.element.innerHTML = ""; // Remove all event handlers this.element.outerHTML = this.element.outerHTML; // eslint-disable-line } /** * Return `tree.option.NAME` (also resolving if this is a callback). * * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()} * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`. * * @param name option name (use dot notation to access extension option, e.g. * `filter.mode`) */ getOption(name: string, defaultValue?: any): any { let ext; let opts = this.options as any; // Lookup `name` in options dict if (name.indexOf(".") >= 0) { [ext, name] = name.split("."); opts = opts[ext]; } let value = opts[name]; // A callback resolver always takes precedence if (typeof value === "function") { value = value({ type: "resolve", tree: this }); } // Use value from value options dict, fallback do default // console.info(name, value, opts) return value ?? defaultValue; } /** * Set tree option. * Use dot notation to set plugin option, e.g. "filter.mode". */ setOption(name: string, value: any): void { // this.log(`setOption(${name}, ${value})`); if (name.indexOf(".") >= 0) { const parts = name.split("."); const ext = this.extensions[parts[0]]; ext!.setPluginOption(parts[1], value); return; } (this.options as any)[name] = value; switch (name) { case "checkbox": this.update(ChangeType.any); break; case "enabled": this.setEnabled(!!value); break; case "fixedCol": this.element.classList.toggle("wb-fixed-col", !!value); break; } } /** Return true if the tree (or one of its nodes) has the input focus. */ hasFocus() { return this.element.contains(document.activeElement); } /** * Return true if the tree displays a header. Grids have a header unless the * `header` option is set to `false`. Plain trees have a header if the `header` * option is a string or `true`. */ hasHeader() { const header = this.options.header; return this.isGrid() ? header !== false : !!header; } /** Run code, but defer rendering of viewport until done. * * ```js * const res = tree.runWithDeferredUpdate(() => { * return someFunctionThatWouldUpdateManyNodes(); * }); * ``` */ runWithDeferredUpdate(func: () => util.NotPromise): T { try { this.enableUpdate(false); const res = func(); util.assert( !(res instanceof Promise), `Promise return not allowed (see 'runWithDeferredUpdateAsync()'): ${res}` ); return res; } finally { this.enableUpdate(true); } } /** Run code, but defer rendering of viewport until done. * * ```js * const res = await tree.runWithDeferredUpdate(async () => { * return someAsyncFunctionThatWouldUpdateManyNodes(); * }); * ``` */ async runWithDeferredUpdateAsync(func: () => Promise): Promise { try { this.enableUpdate(false); return await func(); } finally { this.enableUpdate(true); } } /** Recursively expand all expandable nodes (triggers lazy load if needed). */ async expandAll(flag: boolean = true, options?: ExpandAllOptions) { await this.root.expandAll(flag, options); } /** Recursively select all nodes. */ selectAll(flag: boolean = true) { return this.root.setSelected(flag, { propagateDown: true }); } /** Toggle select all nodes. */ toggleSelect() { this.selectAll(this.root._anySelectable()); } /** * Return an array of selected nodes. * @param stopOnParents only return the topmost selected node (useful with selectMode 'hier') */ getSelectedNodes(stopOnParents: boolean = false): WunderbaumNode[] { return this.root.getSelectedNodes(stopOnParents); } /** * Return an array of refKey values. * * RefKeys are unique identifiers for a node data, and are used to identify * clones. * If more than one node has the same refKey, it is only returned once. * @param selected if true, only return refKeys of selected nodes. */ getRefKeys(selected = false): string[] { return this.root.getRefKeys(selected); } /* * Return an array of selected nodes. */ protected _selectRange(eventInfo: WbEventInfo): false | void { this.logDebug("_selectRange", eventInfo); util.error("Not yet implemented."); // const mode = this.options.selectMode!; // if (mode !== "multi") { // this.logDebug(`Range selection only available for selectMode 'multi'`); // return; // } // if (eventInfo.canonicalName === "Meta+click") { // eventInfo.node?.toggleSelected(); // return false; // don't // } else if (eventInfo.canonicalName === "Shift+click") { // let from = this.activeNode; // let to = eventInfo.node; // if (!from || !to || from === to) { // return; // } // this.runWithDeferredUpdate(() => { // this.visitRows( // (node) => { // node.setSelected(); // }, // { // includeHidden: true, // includeSelf: false, // start: from, // reverse: from!._rowIdx! > to!._rowIdx!, // } // ); // }); // return false; // } } /** Return the number of nodes in the data model. * @param visible if true, nodes that are hidden due to collapsed parents are ignored. */ count(visible = false): number { return visible ? this.treeRowCount : this.keyMap.size; } /** Return the number of *unique* nodes in the data model, i.e. unique `node.refKey`. */ countUnique(): number { return this.refKeyMap.size; } /** @internal sanity check. */ _check() { let i = 0; this.visit((n) => { i++; }); if (this.keyMap.size !== i) { this.logWarn(`_check failed: ${this.keyMap.size} !== ${i}`); } // util.assert(this.keyMap.size === i); } /** * Find all nodes that match condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findAll} */ findAll(match: string | RegExp | MatcherCallback) { return this.root.findAll(match); } /** * Find all nodes with a given _refKey_ (aka a list of clones). * * @param refKey a `node.refKey` value to search for. * @returns an array of matching nodes with at least two element or `[]` * if nothing found. * * @see {@link WunderbaumNode.getCloneList} */ findByRefKey(refKey: string): WunderbaumNode[] { const clones = this.refKeyMap.get(refKey); return clones ? Array.from(clones) : []; } /** * Find first node that matches condition. * * @param match title string to search for, or a * callback function that returns `true` if a node is matched. * @see {@link WunderbaumNode.findFirst} */ findFirst(match: string | RegExp | MatcherCallback) { return this.root.findFirst(match); } /** * Find first node that matches condition. * * @see {@link WunderbaumNode.findFirst} * */ findKey(key: string): WunderbaumNode | null { return this.keyMap.get(key) || null; } /** * Find the next visible node that starts with `match`, starting at `startNode` * and wrap-around at the end. * Used by quicksearch and keyboard navigation. */ findNextNode( match: string | MatcherCallback, startNode?: WunderbaumNode | null, reverse = false ): WunderbaumNode | null { //, visibleOnly) { let res: WunderbaumNode | null = null; const firstNode = this.getFirstChild()!; // Last visible node (calculation is expensive, so do only if we need it): const lastNode = reverse ? this.findRelatedNode(firstNode, "last")! : null; const matcher = typeof match === "string" ? makeNodeTitleStartMatcher(match) : match; startNode = startNode || (reverse ? lastNode : firstNode); function _checkNode(n: WunderbaumNode) { // console.log("_check " + n) if (matcher(n)) { res = n; } if (res || n === startNode) { return false; } } this.visitRows(_checkNode, { start: startNode, includeSelf: false, reverse: reverse, }); // Wrap around search if (!res && startNode !== firstNode) { this.visitRows(_checkNode, { start: reverse ? lastNode : firstNode, includeSelf: true, reverse: reverse, }); } return res; } /** * Find a node relative to another node. * * @param node * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. * (Alternatively the keyCode that would normally trigger this move, * e.g. `$.ui.keyCode.LEFT` = 'left'. * @param includeHidden Not yet implemented */ findRelatedNode( node: WunderbaumNode, where: NavigationType, includeHidden = false ) { const rowHeight = this.options.rowHeightPx; let res = null; const pageSize = Math.floor( this.listContainerElement.clientHeight / rowHeight ); switch (where) { case "parent": if (node.parent && node.parent.parent) { res = node.parent; } break; case "first": // First visible node this.visit((n) => { if (n.isVisible()) { res = n; return false; } }); break; case "last": this.visit((n) => { // last visible node if (n.isVisible()) { res = n; } }); break; case "left": if (node.parent && node.parent.parent) { res = node.parent; } // if (node.expanded) { // node.setExpanded(false); // } else if (node.parent && node.parent.parent) { // res = node.parent; // } break; case "right": if (node.children && node.children.length) { res = node.children[0]; } // if (this.cellNavMode) { // throw new Error("Not implemented"); // } else { // if (!node.expanded && (node.children || node.lazy)) { // node.setExpanded(); // res = node; // } else if (node.children && node.children.length) { // res = node.children[0]; // } // } break; case "up": res = this._getNextNodeInView(node, { reverse: true }); break; case "down": res = this._getNextNodeInView(node); break; case "pageDown": { const bottomNode = this.getLowestVpNode(); // this.logDebug(`${where}(${node}) -> ${bottomNode}`); if (node._rowIdx! < bottomNode._rowIdx!) { res = bottomNode; } else { res = this._getNextNodeInView(node, { reverse: false, ofs: pageSize, }); } } break; case "pageUp": if (node._rowIdx === 0) { res = node; } else { const topNode = this.getTopmostVpNode(); // this.logDebug(`${where}(${node}) -> ${topNode}`); if (node._rowIdx! > topNode._rowIdx!) { res = topNode; } else { res = this._getNextNodeInView(node, { reverse: true, ofs: pageSize, }); } } break; case "prevMatch": // fallthrough case "nextMatch": if (!this.isFilterActive) { this.logWarn(`${where}: Filter is not active.`); break; } res = this.findNextNode( (n) => n.isMatched(), node, where === "prevMatch" ); res?.setActive(); break; default: this.logWarn("Unknown relation '" + where + "'."); } return res; } /** * Iterator version of {@link Wunderbaum.format}. */ *format_iter( name_cb?: NodeStringCallback, connectors?: string[] ): IterableIterator { yield* this.root.format_iter(name_cb, connectors); } /** * Return multiline string representation of the node hierarchy. * Mostly useful for debugging. * * Example: * ```js * console.info(tree.format((n)=>n.title)); * ``` * logs * ``` * Playground * ├─ Books * | ├─ Art of War * | ╰─ Don Quixote * ├─ Music * ... * ``` * * @see {@link Wunderbaum.format_iter} and {@link WunderbaumNode.format}. */ format(name_cb?: NodeStringCallback, connectors?: string[]): string { return this.root.format(name_cb, connectors); } /** * Always returns null (so a tree instance behaves as `tree.root`). */ get parent(): null { return null; } /** * Return a list of top-level nodes. */ get children(): WunderbaumNode[] { return this.root.children || []; } /** * Return the active cell (`span.wb-col`) of the currently active node or null. */ getActiveColElem() { if (this.activeNode && this.activeColIdx >= 0) { return this.activeNode.getColElem(this.activeColIdx); } return null; } /** * Return the currently active node or null (alias for `tree.activeNode`). * Alias for {@link Wunderbaum.activeNode}. * * @see {@link WunderbaumNode.setActive} * @see {@link WunderbaumNode.isActive} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getActiveNode() { return this.activeNode; } /** * Return the first top level node if any (not the invisible root node). */ getFirstChild() { return this.root.getFirstChild(); } /** * Return the last top level node if any (not the invisible root node). */ getLastChild() { return this.root.getLastChild(); } /** * Return the node that currently has keyboard focus or null. * Alias for {@link Wunderbaum.focusNode}. * @see {@link WunderbaumNode.setFocus} * @see {@link WunderbaumNode.hasFocus} * @see {@link Wunderbaum.activeNode} * @see {@link Wunderbaum.focusNode} */ getFocusNode() { return this.focusNode; } /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event. * * @param {Event} event Mouse event, e.g. click, ... * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined */ static getEventInfo(event: Event): WbEventInfo { const target = event.target; const cl = target.classList; const parentCol = target.closest("span.wb-col") as HTMLSpanElement; const node = Wunderbaum.getNode(target); const tree = node ? node.tree : Wunderbaum.getTree(event); const res: WbEventInfo = { event: event, canonicalName: util.eventToString(event), tree: tree!, node: node, region: NodeRegion.unknown, colDef: undefined, colIdx: -1, colId: undefined, colElem: parentCol, }; if (cl.contains("wb-title")) { res.region = NodeRegion.title; } else if (cl.contains("wb-expander")) { res.region = node!.isExpandable() ? NodeRegion.expander : NodeRegion.prefix; } else if (cl.contains("wb-checkbox")) { res.region = NodeRegion.checkbox; } else if (cl.contains("wb-icon")) { //|| cl.contains("wb-custom-icon")) { res.region = NodeRegion.icon; } else if (cl.contains("wb-node")) { res.region = NodeRegion.title; } else if (parentCol) { res.region = NodeRegion.column; const idx = Array.prototype.indexOf.call( parentCol.parentNode!.children, parentCol ); res.colIdx = idx; } else if (cl.contains("wb-row")) { // Plain tree res.region = NodeRegion.title; } else { // Somewhere near the title if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) { tree?.logWarn("getEventInfo(): not found", event, res); } return res; } if (res.colIdx === -1) { res.colIdx = 0; } res.colDef = tree?.columns[res.colIdx]; res.colDef != null ? (res.colId = (res.colDef).id) : 0; // this.log("Event", event, res); return res; } /** * Return readable string representation for this instance. * @internal */ toString() { return `Wunderbaum<'${this.id}'>`; } /** Return true if any node title or grid cell is currently beeing edited. * * See also {@link isEditingTitle}. */ isEditing(): boolean { const focusElem = this.nodeListElement.querySelector( "input:focus,select:focus" ); return !!focusElem; } /** Return true if any node is currently in edit-title mode. * * See also {@link WunderbaumNode.isEditingTitle} and {@link isEditing}. */ isEditingTitle(): boolean { return this._callMethod("edit.isEditingTitle"); } /** * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. */ isLoading(): boolean { let res = false; this.root.visit((n) => { // also visit rootNode if (n._isLoading || n._requestId) { res = true; return false; } }, true); return res; } /** Write to `console.log` with tree name as prefix if opts.debugLevel >= 4. * @see {@link logDebug} */ log(...args: any[]) { if (this.options.debugLevel! >= 4) { console.log(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.debug` with tree name as prefix if opts.debugLevel >= 4. * and browser console level includes debug/verbose messages. * @see {@link log} */ logDebug(...args: any[]) { if (this.options.debugLevel! >= 4) { console.debug(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.error` with tree name as prefix. */ logError(...args: any[]) { if (this.options.debugLevel! >= 1) { console.error(this.toString(), ...args); // eslint-disable-line no-console } } /** Write to `console.info` with tree name as prefix if opts.debugLevel >= 3. */ logInfo(...args: any[]) { if (this.options.debugLevel! >= 3) { console.info(this.toString(), ...args); // eslint-disable-line no-console } } /** @internal */ logTime(label: string): string { if (this.options.debugLevel! >= 4) { console.time(this + ": " + label); // eslint-disable-line no-console } return label; } /** @internal */ logTimeEnd(label: string): void { if (this.options.debugLevel! >= 4) { console.timeEnd(this + ": " + label); // eslint-disable-line no-console } } /** Write to `console.warn` with tree name as prefix with if opts.debugLevel >= 2. */ logWarn(...args: any[]) { if (this.options.debugLevel! >= 2) { console.warn(this.toString(), ...args); // eslint-disable-line no-console } } /** Emit a warning for deprecated methods. @internal */ logDeprecate(method: string, options?: DeprecationOptions) { if (this.options.debugLevel! >= 2) { let msg = `${this}: ${method} is deprecated`; if (options?.since) { msg += ` since ${options.since}`; } if (options?.hint) { msg += ` (${options.since})`; } console.warn(msg + "."); // eslint-disable-line no-console } } /** Reset column widths to default. @since 0.10.0 */ resetColumns() { this.columns.forEach((col) => { delete col.customWidthPx; }); this.update(ChangeType.colStructure); } // /** Renumber nodes `_nativeIndex`. @see {@link WunderbaumNode.resetNativeChildOrder} */ // resetNativeChildOrder(options?: ResetOrderOptions) { // this.root.resetNativeChildOrder(options); // } /** * Make sure that this node is vertically scrolled into the viewport. * * Nodes that are above the visible area become the top row, nodes that are * below the viewport become the bottom row. */ scrollTo(nodeOrOpts: ScrollToOptions | WunderbaumNode) { const PADDING = 2; // leave some pixels between viewport bounds let node; // WunderbaumNode; let options: ScrollToOptions | undefined; if (nodeOrOpts instanceof WunderbaumNode) { node = nodeOrOpts; } else { options = nodeOrOpts; node = options.node; } util.assert(node && node._rowIdx != null, `Invalid node: ${node}`); const rowHeight = this.options.rowHeightPx; const scrollParent = this.element; const headerHeight = this.headerElement.clientHeight; // May be 0 const scrollTop = scrollParent.scrollTop; const vpHeight = scrollParent.clientHeight; const rowTop = node._rowIdx! * rowHeight + headerHeight; const vpTop = headerHeight; const vpRowTop = rowTop - scrollTop; const vpRowBottom = vpRowTop + rowHeight; const topNode = options?.topNode; // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts , options); let newScrollTop: number | null = null; if (vpRowTop >= vpTop) { if (vpRowBottom <= vpHeight) { // Already in view // this.log("Already in view"); } else { // Node is below viewport // this.log("Below viewport"); newScrollTop = rowTop + rowHeight - vpHeight + PADDING; // leave some pixels between viewport bounds } } else { // Node is above viewport // this.log("Above viewport"); newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between viewport bounds } if (newScrollTop != null) { this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`); scrollParent.scrollTop = newScrollTop; if (topNode) { // Make sure the topNode is always visible this.scrollTo(topNode); } // this.update(ChangeType.scroll); } } /** * Make sure that this node is horizontally scrolled into the viewport. * Called by {@link setColumn}. */ protected scrollToHorz() { // const PADDING = 1; const fixedWidth = this.columns[0]._widthPx!; const vpWidth = this.element.clientWidth; const scrollLeft = this.element.scrollLeft; const colElem = this.getActiveColElem()!; const colLeft = Number.parseInt(colElem?.style.left, 10); const colRight = colLeft + Number.parseInt(colElem?.style.width, 10); let newLeft = scrollLeft; if (colLeft - scrollLeft < fixedWidth) { // The current column is scrolled behind the left fixed column newLeft = colLeft - fixedWidth; } else if (colRight - scrollLeft > vpWidth) { // The current column is scrolled outside the right side newLeft = colRight - vpWidth; } newLeft = Math.max(0, newLeft); // util.assert(node._rowIdx != null); this.log( `scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}` ); this.element.scrollLeft = newLeft; // this.update(ChangeType.scroll); } /** * Set column #colIdx to 'active'. * * This higlights the column header and -cells by adding the `wb-active` * class to all grid cells of the active column.
    * Available in cell-nav mode only. * * If _options.edit_ is true, the embedded input element is focused, or if * colIdx is 0, the node title is put into edit mode. */ setColumn(colIdx: number | string, options?: SetColumnOptions) { const edit = options?.edit; const scroll = options?.scrollIntoView !== false; util.assert(this.isCellNav(), "Expected cellNav mode"); if (typeof colIdx === "string") { const cid = colIdx; colIdx = this.columns.findIndex((c) => c.id === colIdx); util.assert(colIdx >= 0, `Invalid colId: ${cid}`); } util.assert( 0 <= colIdx && colIdx < this.columns.length, `Invalid colIdx: ${colIdx}` ); this.activeColIdx = colIdx; // Update `wb-active` class for all headers if (this.hasHeader()) { for (const rowDiv of this.headerElement.children) { let i = 0; for (const colDiv of rowDiv.children) { (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx); } } } this.activeNode?.update(ChangeType.status); // Update `wb-active` class for all cell spans for (const rowDiv of this.nodeListElement.children) { let i = 0; for (const colDiv of rowDiv.children) { (colDiv as HTMLElement).classList.toggle("wb-active", i++ === colIdx); } } // Horizontically scroll into view if (scroll || edit) { this.scrollToHorz(); } if (edit && this.activeNode) { // this.activeNode.setFocus(); // Blur prev. input if any if (colIdx === 0) { this.activeNode.startEditTitle(); } else { this.getActiveColElem() ?.querySelector("input,select") ?.focus(); } } } /* Set or remove keyboard focus to the tree container. @internal */ _setActiveNode(node: WunderbaumNode | null) { this._activeNode = node; } /** Set or remove keyboard focus to the tree container. */ setActiveNode(key: string, flag: boolean = true, options?: SetActiveOptions) { this.findKey(key)?.setActive(flag, options); } /** Set or remove keyboard focus to the tree container. */ setFocus(flag = true) { if (flag) { this.element.focus(); } else { this.element.blur(); } } /* Set or remove keyboard focus to the tree container. @internal */ _setFocusNode(node: WunderbaumNode | null) { this._focusNode = node; } /** Return the current selection/expansion/activation status. @experimental */ getState(options: GetStateOptions = {}): TreeStateDefinition { const { activeKey = true, expandedKeys = false, selectedKeys = false, } = options; const expandSet = new Set(); if (expandedKeys) { for (const node of this) { if (node.isExpanded() && node.hasChildren()) { expandSet.add(node.key); } } } // Parents of active node are always expanded if (activeKey && this.activeNode) { this.activeNode.visitParents((n) => { if (n.parent) { expandSet.add(n.key); } }, false); } const state: TreeStateDefinition = { expandedKeys: expandSet.size ? Array.from(expandSet) : undefined, activeKey: this.activeNode?.key ?? null, activeColIdx: this.activeColIdx, selectedKeys: selectedKeys ? this.getSelectedNodes().flatMap((n) => n.key) : undefined, }; return state; } /** Apply selection/expansion/activation status. @experimental */ async setState(state: TreeStateDefinition, options: SetStateOptions = {}) { const { expandLazy = true } = options; return this.runWithDeferredUpdateAsync(async () => { if (state.expandedKeys && state.expandedKeys.length) { if (expandLazy) { // Expand all keys recursively, even if they are not in the tree yet await this._loadLazyNodes(state.expandedKeys, { expand: true, noEvents: true, }); } else { for (const key of state.expandedKeys) { this.findKey(key)?.setExpanded(true); } } } if (state.activeKey) { this.setActiveNode(state.activeKey); } if (state.selectedKeys) { this.selectAll(false); for (const key of state.selectedKeys) { this.findKey(key)?.setSelected(true); } } if (this.isCellNav() && state.activeColIdx != null) { this.setColumn(state.activeColIdx); } }); } /** * Schedule an update request to reflect a tree change. * The render operation is async and debounced unless the `immediate` option * is set. * * Use {@link WunderbaumNode.update} if only a single node has changed, * or {@link WunderbaumNode._render}) to pass special options. */ update(change: ChangeType, options?: UpdateOptions): void; /** * Update a row to reflect a single node's modification. * * @see {@link WunderbaumNode.update}, {@link WunderbaumNode._render} */ update( change: ChangeType, node: WunderbaumNode, options?: UpdateOptions ): void; update( change: ChangeType, node?: WunderbaumNode | UpdateOptions, options?: UpdateOptions ): void { // this.log(`update(${change}) node=${node}`); if (!(node instanceof WunderbaumNode)) { options = node; node = undefined; } const immediate = !!util.getOption(options, "immediate"); const RF = RenderFlag; const pending = this.pendingChangeTypes; if (this._disableUpdateCount) { // Assuming that we redraw all when enableUpdate() is re-enabled. // this.log( // `IGNORED update(${change}) node=${node} (disable level ${this._disableUpdateCount})` // ); this._disableUpdateIgnoreCount++; return; } switch (change) { case ChangeType.any: case ChangeType.colStructure: pending.add(RF.header); pending.add(RF.clearMarkup); pending.add(RF.redraw); pending.add(RF.scroll); break; case ChangeType.resize: // case ChangeType.colWidth: pending.add(RF.header); pending.add(RF.redraw); break; case ChangeType.structure: pending.add(RF.redraw); break; case ChangeType.scroll: pending.add(RF.scroll); break; case ChangeType.row: case ChangeType.data: case ChangeType.status: util.assert(node, `Option '${change}' requires a node.`); // Single nodes are immediately updated if already inside the viewport // (otherwise we can ignore) if ((node)!._rowElem) { (node)!._render({ change: change }); } break; default: util.error(`Invalid change type '${change}'.`); } if (change === ChangeType.colStructure) { const isGrid = this.isGrid(); this.element.classList.toggle("wb-grid", isGrid); if (!isGrid && this.isCellNav()) { this.setCellNav(false); } } if (pending.size > 0) { if (immediate) { this._updateViewportImmediately(); } else { this._updateViewportThrottled(); } } } /** Disable mouse and keyboard interaction (return prev. state). */ setEnabled(flag: boolean = true): boolean { const prev = this.enabled; this.enabled = !!flag; this.element.classList.toggle("wb-disabled", !flag); return prev; } /** Return false if tree is disabled. */ isEnabled(): boolean { return this.enabled; } /** Return true if tree has more than one column, i.e. has additional data columns. */ isGrid(): boolean { return this.columns && this.columns.length > 1; } /** Return true if cell-navigation mode is active. */ isCellNav(): boolean { return !!this._cellNavMode; } /** Return true if row-navigation mode is active. */ isRowNav(): boolean { return !this._cellNavMode; } /** Set the tree's navigation mode. */ setCellNav(flag: boolean = true) { const prev = this._cellNavMode; // if (flag === prev) { // return; // } this._cellNavMode = !!flag; if (flag && !prev) { // switch from row to cell mode this.setColumn(0); } this.element.classList.toggle("wb-cell-mode", flag); this.activeNode?.update(ChangeType.status); } /** Set the tree's navigation mode option. */ setNavigationOption(mode: NavModeEnum, reset = false) { if (!this.isGrid() && mode !== NavModeEnum.row) { this.logWarn("Plain trees only support row navigation mode."); return; } this.options.navigationModeOption = mode; switch (mode) { case NavModeEnum.cell: this.setCellNav(true); break; case NavModeEnum.row: this.setCellNav(false); break; case NavModeEnum.startCell: if (reset) { this.setCellNav(true); } break; case NavModeEnum.startRow: if (reset) { this.setCellNav(false); } break; default: util.error(`Invalid mode '${mode}'.`); } } /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */ setStatus( status: NodeStatusType, options?: SetStatusOptions ): WunderbaumNode | null { return this.root.setStatus(status, options); } /** Add or redefine node type definitions. */ setTypes(types: any, replace = true) { util.assert(util.isPlainObject(types), `Expected plain objext: ${types}`); if (replace) { this.types = types; } else { util.extend(this.types, types); } // Convert `TYPE.classes` to a Set for (const t of Object.values(this.types) as any) { if (t.classes) { t.classes = util.toSet(t.classes); } } } /** * Sort nodes list by title or custom criteria. * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1 * (defaults to sorting by title). * @param {boolean} deep pass true to sort all descendant nodes recursively * @deprecated use {@link sort} */ sortChildren( cmp: SortCallback | null = nodeTitleSorter, deep: boolean = false ): void { this.logDeprecate("sortChildren()", { since: "0.14.0" }); return this.sort({ cmp: cmp ? cmp : undefined, deep: deep, propName: "title", }); } /** * Convenience method to implement column sorting. * @see {@link WunderbaumNode.sortByProperty}. * @since 0.11.0 * @deprecated use {@link sort} */ sortByProperty(options: SortByPropertyOptions) { this.logDeprecate("sortByProperty()", { since: "0.14.0" }); this.root.sortByProperty(options); } /** * Sort nodes list by title or custom criteria. * @since 0.14.0 */ sort(options: SortOptions): void { this.root.sort(options); } /** Convert tree to an array of plain objects. * * @param callback is called for every node, in order to allow * modifications. * Return `false` to ignore this node or `"skip"` to include this node * without its children. * @see {@link WunderbaumNode.toDict}. */ toDictArray(callback?: NodeToDictCallback): Array { const res = this.root.toDict(true, callback); return res.children ?? []; } /** * Update column headers and column width. * Return true if at least one column width changed. */ // _updateColumnWidths(options?: UpdateColumnsOptions): boolean { _updateColumnWidths(): boolean { // options = Object.assign({ updateRows: true, renderMarkup: false }, options); const defaultMinWidth = 4; const vpWidth = this.element.clientWidth; // Shorten last column width to avoid h-scrollbar // (otherwise resizbing the demo would display a void scrollbar?) const FIX_ADJUST_LAST_COL = 1; const columns = this.columns; const col0 = columns[0]; let totalWidth = 0; let totalWeight = 0; let fixedWidth = 0; let modified = false; // this.element.classList.toggle("wb-grid", isGrid); // if (!isGrid && this.isCellNav()) { // this.setCellNav(false); // } // if (options.calculateCols) { if (col0.id !== "*") { throw new Error(`First column must have id '*': got '${col0.id}'.`); } // Gather width definitions this._columnsById = {}; for (const col of columns) { this._columnsById[col.id] = col; const cw = col.customWidthPx ? `${col.customWidthPx}px` : col.width; if (col.id === "*" && col !== col0) { throw new Error( `Column id '*' must be defined only once: '${col.title}'.` ); } if (!cw || cw === "*") { col._weight = 1.0; totalWeight += 1.0; } else if (typeof cw === "number") { col._weight = cw; totalWeight += cw; } else if (typeof cw === "string" && cw.endsWith("px")) { col._weight = 0; const px = parseFloat(cw.slice(0, -2)); if (col._widthPx != px) { modified = true; col._widthPx = px; } fixedWidth += px; } else { util.error( `Invalid column width: ${cw} (expected string ending with 'px' or number, e.g. "px" or ).` ); } } // Share remaining space between non-fixed columns const restPx = Math.max(0, vpWidth - fixedWidth); let ofsPx = 0; for (const col of columns) { let minWidth: number; if (col._weight) { const cmw = col.minWidth; if (typeof cmw === "number") { minWidth = cmw; } else if (typeof cmw === "string" && cmw.endsWith("px")) { minWidth = parseFloat(cmw.slice(0, -2)); } else { minWidth = defaultMinWidth; } const px = Math.max(minWidth, (restPx * col._weight) / totalWeight); if (col._widthPx != px) { modified = true; col._widthPx = px; } } col._ofsPx = ofsPx; ofsPx += col._widthPx!; } columns[columns.length - 1]._widthPx! -= FIX_ADJUST_LAST_COL; totalWidth = ofsPx - FIX_ADJUST_LAST_COL; const tw = `${totalWidth}px`; this.headerElement.style.width = tw; this.listContainerElement!.style.width = tw; // } // Every column has now a calculated `_ofsPx` and `_widthPx` // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element); // console.trace(); // util.error("BREAK"); // if (modified) { // this._renderHeaderMarkup(); // if (options.renderMarkup) { // this.update(ChangeType.header, { removeMarkup: true }); // } else if (options.updateRows) { // this._updateRows(); // } // } return modified; } // protected _insertIcon(icon: string, elem: HTMLElement) { // const iconElem = document.createElement("i"); // iconElem.className = icon; // elem.appendChild(iconElem); // } /** Create/update header markup from `this.columns` definition. * @internal */ protected _renderHeaderMarkup() { util.assert(this.headerElement, "Expected a headerElement"); const wantHeader = this.hasHeader(); util.setElemDisplay(this.headerElement, wantHeader); if (!wantHeader) { return; } const iconMap = this.iconMap; const colCount = this.columns.length; const headerRow = this.headerElement.querySelector(".wb-row")!; util.assert(headerRow, "Expected a row in header element"); headerRow.innerHTML = "".repeat(colCount); for (let i = 0; i < colCount; i++) { const col = this.columns[i]; const colElem = headerRow.children[i]; colElem.style.left = col._ofsPx + "px"; colElem.style.width = col._widthPx + "px"; // Add classes from `columns` definition to `` cells if (typeof col.headerClasses === "string") { col.headerClasses ? colElem.classList.add(...col.headerClasses.split(" ")) : 0; } else { col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0; } // Add tooltip to column title let tooltip = ""; if (col.tooltip) { tooltip = util.escapeTooltip(col.tooltip); tooltip = ` title="${tooltip}"`; } // Add column header icons let addMarkup = ""; // NOTE: we use CSS float: right to align icons, so they must be added in // reverse order if (util.toBool(col.menu, this.options.columnsMenu, false)) { const iconClass = "wb-col-icon-menu " + iconMap.colMenu; const icon = ``; addMarkup += icon; } if (util.toBool(col.sortable, this.options.columnsSortable, false)) { let iconClass = "wb-col-icon-sort " + iconMap.colSortable; if (col.sortOrder) { iconClass += `wb-col-sort-${col.sortOrder}`; iconClass += col.sortOrder === "asc" ? iconMap.colSortAsc : iconMap.colSortDesc; } const icon = ``; addMarkup += icon; } if (util.toBool(col.filterable, this.options.columnsFilterable, false)) { colElem.classList.toggle("wb-col-filter", !!col.filterActive); let iconClass = "wb-col-icon-filter " + iconMap.colFilter; if (col.filterActive) { iconClass += iconMap.colFilterActive; } const icon = ``; addMarkup += icon; } // Add resizer to all but the last column if (i < colCount - 1) { if (util.toBool(col.resizable, this.options.columnsResizable, false)) { addMarkup += ''; } else { addMarkup += ''; } } // Create column header const title = util.escapeHtml(col.title || col.id); colElem.innerHTML = `${title}${addMarkup}`; // Highlight active column if (this.isCellNav()) { colElem.classList.toggle("wb-active", i === this.activeColIdx); } } } /** * Render pending changes that were scheduled using {@link WunderbaumNode.update} if any. * * This is hardly ever neccessary, since we normally either * - call `update(ChangeType.TYPE)` (async, throttled), or * - call `update(ChangeType.TYPE, {immediate: true})` (synchronous) * * `updatePendingModifications()` will only force immediate execution of * pending async changes if any. */ updatePendingModifications() { if (this.pendingChangeTypes.size > 0) { this._updateViewportImmediately(); } } /** @internal */ public _createNodeIcon( node: WunderbaumNode, showLoading: boolean, showBadge: boolean ): HTMLElement | null { const iconMap = this.iconMap; let iconElem; let icon = node.getOption("icon"); if (node._errorInfo) { icon = iconMap.error; } else if (node._isLoading && showLoading) { // Status nodes, or nodes without expander (< minExpandLevel) should // display the 'loading' status with the i.wb-icon span icon = iconMap.loading; } if (icon === false) { return null; // explicitly disabled: don't try default icons } if (typeof icon === "string") { // Callback returned an icon definition // icon = icon.trim() } else if (node.statusNodeType) { icon = (iconMap)[node.statusNodeType]; } else if (node.expanded) { icon = iconMap.folderOpen; } else if (node.children) { icon = iconMap.folder; } else if (node.lazy) { icon = iconMap.folderLazy; } else { icon = iconMap.doc; } if (!icon) { iconElem = document.createElement("i"); iconElem.className = "wb-icon"; } else if (TEST_HTML.test(icon)) { iconElem = util.elemFromHtml(icon); } else if (TEST_FILE_PATH.test(icon)) { iconElem = util.elemFromHtml( `` ); } else { // Class name iconElem = document.createElement("i"); iconElem.className = "wb-icon " + icon; } // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement const cbRes = showBadge && node._callEvent("iconBadge", { iconSpan: iconElem }); let badge = null; if (cbRes != null && cbRes !== false) { let classes = ""; let tooltip = ""; if (util.isPlainObject(cbRes)) { badge = "" + cbRes.badge; classes = cbRes.badgeClass ? " " + cbRes.badgeClass : ""; tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : ""; } else if (typeof cbRes === "number") { badge = "" + cbRes; } else { badge = cbRes; // string or HTMLSpanElement } if (typeof badge === "string") { badge = util.elemFromHtml( `${util.escapeHtml( badge )}` ); } if (badge) { iconElem.append(badge); } } return iconElem; } private _updateTopBreadcrumb() { const breadcrumb = this.breadcrumb!; const topmost = this.getTopmostVpNode(true); const parentList = topmost?.getParentList(false, false); if (parentList?.length) { breadcrumb.innerHTML = ""; for (const n of topmost.getParentList(false, false)) { const icon = this._createNodeIcon(n, false, false); if (icon) { breadcrumb.append(icon, " "); } const part = document.createElement("a"); part.textContent = n.title; part.href = "#"; part.classList.add("wb-breadcrumb"); part.dataset.key = n.key; breadcrumb.append(part, this.options.strings.breadcrumbDelimiter); } } else { breadcrumb.innerHTML = " "; } } /** * This is the actual update method, which is wrapped inside a throttle method. * It calls `updateColumns()` and `_updateRows()`. * * This protected method should not be called directly but via * {@link WunderbaumNode.update}`, {@link Wunderbaum.update}, * or {@link Wunderbaum.updatePendingModifications}. * @internal */ protected _updateViewportImmediately() { if (this._disableUpdateCount) { this.log( `_updateViewportImmediately() IGNORED (disable level: ${this._disableUpdateCount}).` ); this._disableUpdateIgnoreCount++; return; } if (this._updateViewportThrottled.pending()) { // this.logWarn(`_updateViewportImmediately() cancel pending timer.`); this._updateViewportThrottled.cancel(); } // Shorten container height to avoid v-scrollbar const FIX_ADJUST_HEIGHT = 1; const RF = RenderFlag; const pending = new Set(this.pendingChangeTypes); this.pendingChangeTypes.clear(); const scrollOnly = pending.has(RF.scroll) && pending.size === 1; if (scrollOnly) { this._updateRows({ newNodesOnly: true }); // this.log("_updateViewportImmediately(): scroll only."); } else { this.log("_updateViewportImmediately():", pending); if (this.options.adjustHeight !== false) { let height = this.listContainerElement.clientHeight; const headerHeight = this.headerElement.clientHeight; // May be 0 const wantHeight = this.element.clientHeight - headerHeight - FIX_ADJUST_HEIGHT; if (Math.abs(height - wantHeight) > 1.0) { // this.log("resize", height, wantHeight); this.listContainerElement.style.height = wantHeight + "px"; height = wantHeight; } } // console.profile(`_updateViewportImmediately()`) if (pending.has(RF.clearMarkup)) { this.visit((n) => { n.removeMarkup(); }); } // let widthModified = false; if (pending.has(RF.header)) { // widthModified = this._updateColumnWidths(); this._updateColumnWidths(); this._renderHeaderMarkup(); } this._updateRows(); // console.profileEnd(`_updateViewportImmediately()`) } if (this.breadcrumb) { this._updateTopBreadcrumb(); } this._callEvent("update"); } // /** // * Assert that TR order matches the natural node order // * @internal // */ // protected _validateRows(): boolean { // let trs = this.nodeListElement.childNodes; // let i = 0; // let prev = -1; // let ok = true; // trs.forEach((element) => { // const tr = element as HTMLTableRowElement; // const top = Number.parseInt(tr.style.top); // const n = (tr)._wb_node; // // if (i < 4) { // // console.info( // // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'` // // ); // // } // if (prev >= 0 && top !== prev + ROW_HEIGHT) { // n.logWarn( // `TR order mismatch at index ${i}: top=${top}px != ${ // prev + ROW_HEIGHT // }` // ); // // throw new Error("fault"); // ok = false; // } // prev = top; // i++; // }); // return ok; // } /* * - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes. * - Store count of rows to `tree.treeRowCount`. * - Renumber `node._rowIdx` for all visible nodes. * - Calculate the index range that must be rendered to fill the viewport * (including upper and lower prefetch) * - */ protected _updateRows(options?: any): boolean { // const label = this.logTime("_updateRows"); // this.log("_updateRows", opts) options = Object.assign({ newNodesOnly: false }, options); const newNodesOnly = !!options.newNodesOnly; const rowHeight = this.options.rowHeightPx; const vpHeight = this.element.clientHeight; const prefetch = RENDER_MAX_PREFETCH; // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH; const ofs = this.element.scrollTop; let startIdx = Math.max(0, ofs / rowHeight - prefetch); startIdx = Math.floor(startIdx); // Make sure start is always even, so the alternating row colors don't // change when scrolling: if (startIdx % 2) { startIdx--; } let endIdx = Math.max(0, (ofs + vpHeight) / rowHeight + prefetch); endIdx = Math.ceil(endIdx); // this.debug("render", opts); const obsoleteNodes = new Set(); this.nodeListElement.childNodes.forEach((elem) => { if ((elem)._wb_node) { obsoleteNodes.add((elem)._wb_node); } }); let idx = 0; let top = 0; let modified = false; let prevElem: HTMLDivElement | "first" | "last" = "first"; this.visitRows(function (node) { // node.log("visit") const rowDiv = node._rowElem; // Renumber all expanded nodes if (node._rowIdx !== idx) { node._rowIdx = idx; modified = true; } if (idx < startIdx || idx > endIdx) { // row is outside viewport bounds if (rowDiv) { prevElem = rowDiv; } } else if (rowDiv && newNodesOnly) { obsoleteNodes.delete(node); // no need to update existing node markup rowDiv.style.top = idx * rowHeight + "px"; prevElem = rowDiv; } else { obsoleteNodes.delete(node); // Create new markup if (rowDiv) { rowDiv.style.top = idx * rowHeight + "px"; } node._render({ top: top, after: prevElem }); // node.log("render", top, prevElem, "=>", node._rowElem); prevElem = node._rowElem!; } idx++; top += rowHeight; }); this.treeRowCount = idx; for (const n of obsoleteNodes) { n._callEvent("discard"); n.removeMarkup(); } // Resize tree container this.nodeListElement.style.height = `${top}px`; // this.log( // `_updateRows(scrollOfs:${ofs}, ${startIdx}..${endIdx})`, // this.nodeListElement.style.height // ); // this.logTimeEnd(label); // this._validateRows(); return modified; } /** * Call `callback(node)` for all nodes in hierarchical order (depth-first, pre-order). * @see `wb_node.WunderbaumNode.IterableIterator` * @see {@link WunderbaumNode.visit}. * * @param {function} callback the callback function. * Return false to stop iteration, return "skip" to skip this node and * children only. * @returns {boolean} false, if the iterator was stopped. */ visit(callback: (node: WunderbaumNode) => any) { return this.root.visit(callback, false); } /** * Call callback(node) for all nodes in vertical order, top down (or bottom up). * * Note that this considers expansion state, i.e. filtered nodes and children * of collapsed nodes are skipped, unless `includeHidden` is set. * * Stop iteration if callback() returns false.
    * Return false if iteration was stopped. * * @returns {boolean} false if iteration was canceled */ visitRows(callback: NodeVisitCallback, options?: VisitRowsOptions): boolean { if (!this.root.hasChildren()) { return false; } if (options && options.reverse) { delete options.reverse; return this._visitRowsUp(callback, options); } options = options || {}; let i, nextIdx, parent, res, siblings, stopNode: WunderbaumNode, siblingOfs = 0, skipFirstNode = options.includeSelf === false, node: WunderbaumNode = options.start || this.root.children![0]; const includeHidden = !!options.includeHidden; const checkFilter = !includeHidden && this.filterMode === "hide"; parent = node.parent; while (parent) { // visit siblings siblings = parent.children!; nextIdx = siblings.indexOf(node) + siblingOfs; util.assert( nextIdx >= 0, `Could not find ${node} in parent's children: ${parent}` ); for (i = nextIdx; i < siblings.length; i++) { node = siblings[i]; if (node === stopNode!) { return false; } if ( checkFilter && !node.statusNodeType && !node.match && !node.subMatchCount ) { continue; } if (!skipFirstNode && callback(node) === false) { return false; } skipFirstNode = false; // Dive into node's child nodes if ( node.children && node.children.length && (includeHidden || node.expanded) ) { res = node.visit((n: WunderbaumNode) => { if (n === stopNode) { return false; } if (checkFilter && !n.match && !n.subMatchCount) { return "skip"; } if (callback(n) === false) { return false; } if (!includeHidden && n.children && !n.expanded) { return "skip"; } }, false); if (res === false) { return false; } } } // Visit parent nodes (bottom up) node = parent; parent = parent.parent; siblingOfs = 1; // if (!parent && options.wrap) { this.logDebug("visitRows(): wrap around"); util.assert(options.start, "`wrap` option requires `start`"); stopNode = options.start!; options.wrap = false; parent = this.root; siblingOfs = 0; } } return true; } /** * Call fn(node) for all nodes in vertical order, bottom up. * @internal */ protected _visitRowsUp( callback: NodeVisitCallback, options: VisitRowsOptions ): boolean { let children, idx, parent, node = options.start || this.root.children![0]; const includeHidden = !!options.includeHidden; if (options.includeSelf !== false) { if (callback(node) === false) { return false; } } while (true) { parent = node.parent; children = parent.children!; if (children[0] === node) { // If this is already the first sibling, goto parent node = parent; if (!node.parent) { break; // first node of the tree } children = parent.children; } else { // Otherwise, goto prev. sibling idx = children.indexOf(node); node = children[idx - 1]; // If the prev. sibling has children, follow down to last descendant while ( (includeHidden || node.expanded) && node.children && node.children.length ) { children = node.children; parent = node; node = children[children.length - 1]; } } // Skip invisible if (!includeHidden && !node.isVisible()) { continue; } if (callback(node) === false) { return false; } } return true; } /** * Reload the tree with a new source. * * Previous data is cleared. Note that also column- and type defintions may * be passed with the `source` object. * @see {@link Wunderbaum.reload} for a shortcut to reload the last ajax request * and restore the previous state. */ async load(source: SourceType) { this.clear(); this._initialSource = source; return this.root.load(source); } /** Reload the tree and optionally restore state. * Source defaults to last ajax url if any. * Restoring the active node requires stable keys * @see {@link WunderbaumOptions.autoKeys} * @see {@link Wunderbaum.load} * @experimental */ async reload(options: ReloadOptions = {}) { const { source = this._initialSource, reactivate = true } = options; if (!source) { this.logWarn("No previous ajax source to reload."); return; } if (!reactivate) { return this.load(source); } const state = this.getState(); await this.load(source); return this.setState(state); } /** * Make sure that all nodes in the given keyList are accessible. * This may include loading lazy parent nodes. * Recursively load (and optionally expand) all requested node paths. */ protected async _loadLazyNodes( keyList: string[], options: LoadLazyNodesOptions = {} ) { const { expand = true } = options; const keySet = new Set(keyList); // Make sure that all parent nodes are loaded (and expand if requested) while (keySet.size > 0) { const pendingNodes: Promise[] = []; const curSet = new Set(keySet); for (const key of curSet) { const node = this.findKey(key); if (!node) { continue; // key not yet found (need to load lazy parent?) } keySet.delete(key); if (expand) { pendingNodes.push(node.setExpanded(true)); } else if (node.isUnloaded()) { pendingNodes.push(node.loadLazy()); } if (node._rowElem) { node._render(); // show spinner even is update is suppressed } } if (pendingNodes.length === 0) { // will not load any more nodes, so if if there are still keys // left in the set, we will never find them this.logWarn(`Could not expand ${keySet.size} nodes:`, keySet); break; } await Promise.allSettled(pendingNodes); } } /** * Disable render requests during operations that would trigger many updates. * * ```js * try { * tree.enableUpdate(false); * // ... (long running operation that would trigger many updates) * foo(); * // ... NOTE: make sure that async operations have finished, e.g. * await foo(); * } finally { * tree.enableUpdate(true); * } * ``` */ public enableUpdate(flag: boolean): void { /* 5 7 9 20 25 30 1 >-------------------------------------< 2 >--------------------< 3 >--------------------------< */ if (flag) { util.assert( this._disableUpdateCount > 0, "enableUpdate(true) was called too often" ); this._disableUpdateCount--; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); if (this._disableUpdateCount === 0) { this.logDebug( `enableUpdate(): active again. Re-painting to catch up with ${this._disableUpdateIgnoreCount} ignored update requests...` ); this._disableUpdateIgnoreCount = 0; this.update(ChangeType.any, { immediate: true }); } } else { this._disableUpdateCount++; // this.logDebug( // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...` // ); // this._disableUpdate = Date.now(); } // return !flag; // return previous value } /* --------------------------------------------------------------------------- * FILTER * -------------------------------------------------------------------------*/ /** * Dim or hide unmatched nodes. * @param filter a string to match against node titles, or a callback function. * @param options filter options. Defaults to the `tree.options.filter` settings. * @returns the number of nodes that match the filter. * @example * ```ts * tree.filterNodes("foo", {mode: 'dim', fuzzy: true}); * // or pass a callback * tree.filterNodes((node) => { return node.data.foo === true }, {mode: 'hide'}); * ``` */ filterNodes( filter: string | RegExp | NodeFilterCallback, options: FilterNodesOptions ): number { return this.extensions.filter.filterNodes(filter, options); } /** * Return the number of nodes that match the current filter. * @see {@link Wunderbaum.filterNodes} * @since 0.9.0 */ countMatches(): number { return this.extensions.filter.countMatches(); } /** * Dim or hide whole branches. * @deprecated Use {@link filterNodes} instead and set `options.matchBranch: true`. */ filterBranches( filter: string | NodeFilterCallback, options: FilterNodesOptions ) { return this.extensions.filter.filterBranches(filter, options); } /** * Reset the filter. */ clearFilter() { return this.extensions.filter.clearFilter(); } /** * Return true if a filter is currently applied. */ isFilterActive() { return !!this.filterMode; } /** * Re-apply current filter. */ updateFilter() { return this.extensions.filter.updateFilter(); } } ================================================ FILE: test/generator/README.txt ================================================ Generate test fixtures (Wunderbaum JSON data files) NOTE: Requires Python and pipenv to be installed. ```bash cd /wunderbaum pipenv install pipenv shell cd test/generator python -m make_fixture TYPE ``` e.g. ```bash python -m make_fixture department_M python -m make_fixture fmea_XL python -m make_fixture store_XL ``` ================================================ FILE: test/generator/__init__.py ================================================ ================================================ FILE: test/generator/generator.py ================================================ """ Generate a test data fixture for the tree viewer. Implements a generator that creates a random tree structure from a specification. Example: ```py structure_definition = { ... } random_tree: nutree.TypedTree = generate_tree(structure_definition) ``` See `make_fixture.py` for more examples. See `test_tree_generator.py` for details. """ from collections import Counter from enum import Enum import json from nutree.tree_generator import GenericNodeData from nutree.typed_tree import TypedTree class FileFormat(Enum): nested = "nested" flat = "flat" class Automatic: """Argument value that triggers automatic calculation.""" #: Preferred mappings for auto-compression (_keyMap) RESERVED_SHORT_NAMES = { "title": "t", "type": "y", "children": "c", "key": "k", "refKey": "r", "selected": "s", "expanded": "e", } #: Node properties that are of type bool (or boolean & string). #: When parsing, we accept 0 for false and 1 for true for better JSON compression. COMPRESSABLE_BOOLS = { "checkbox", "colspan", "expanded", "icon", "iconTooltip", "radiogroup", "selected", "tooltip", "unselectable", } def _rounded_number(n: int) -> str: if n < 800: return str(n) if n < 900_000: return f"{round(n / 1_000)}k" return f"{round(n / 1_000_000)}M" # for n in (1, 32, 90, 100, 110, 532, 999, 1000, 1001, 2045, 98000, 101000, 300000): # print(n, rounded_number(n)) def generate_random_wb_source(structure_definition: dict): """ Return a randomized tree structure in uncompressed, nested format. """ # Generate a random nutree.TypedTree structure tree = TypedTree.build_random_tree(structure_definition) # tree.print() # if tree.count < 110: # tree.print() # print(f"Generated tree with {len(tree):,} nodes, depth: {tree.calc_height()}") # nutree generator uses GenericNodeData as default node type and we rely on it defaults = structure_definition.get("types", {}).get("*", {}) assert defaults.get(":factory") in (None, GenericNodeData) child_list = tree.to_dict_list(mapper=GenericNodeData.serialize_mapper) random_struct = { "child_list": child_list, "node_count": len(tree), "node_count_disp": _rounded_number(len(tree)), "depth": tree.calc_height(), } return random_struct def _iter_dict_pre_order(child_list: list): """Depth-first, pre-order iterator.""" idx = 0 def _iter(child_list: list, parent_idx): nonlocal idx for c in child_list: # Get 'children' before caller renames to short name cl = c.get("children") yield parent_idx, c idx += 1 if cl: yield from _iter(cl, idx - 1) return yield from _iter(child_list, None) def compress_child_list( child_list: list, *, format: FileFormat, types: dict = None, columns: list = None, key_map: dict | Automatic = Automatic, positional: list | Automatic = Automatic, auto_compress=True, auto_compress_bool: set | None = None, ) -> dict: """ Convert a child_list that was created by `generate_tree()`. 1. Optionally convert nested child list to flat parent-referencong list 2. Shorten node dict keys using a `keyMap` 3. In flat mode """ if type(child_list) is not list: raise RuntimeError(f"Expected JSON list (not {child_list!r})") #: Available short type names avail_short_names = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") for short in RESERVED_SHORT_NAMES.values(): avail_short_names.remove(short) if auto_compress and key_map is Automatic: # Reserve some short names for well-known attributes key_map = {} else: key_map = key_map.copy() #: Map full_name -> short_name inverse_key_map = {v: k for k, v in key_map.items()} # Remove used short names from list of available abbreviations for short in key_map.keys(): if len(short) == 1: # Raises ValueError if key_map contains a reserved abbrev. avail_short_names.remove(short) if auto_compress and positional is Automatic: positional = ["title", "type"] if positional: positional = list(positional) # don't want type if "children" in positional: positional.remove("children") #: Occurrence counter of (long) attribute names attr_counts = Counter() #: Flat node list (used for) node_list = [] #: Map type_name -> type_idx type_map = {} #: List of type names. The index into this list will be used. type_list = [] # ---------- # Pass 1: collect used attribute and type names seq = 0 for parent_idx, node in _iter_dict_pre_order(child_list): # Build/update key_map / inverse_key_map for attr in node.keys(): attr_counts[attr] += 1 if attr not in inverse_key_map: if attr in RESERVED_SHORT_NAMES: short = RESERVED_SHORT_NAMES[attr] else: # Try to dreive the short name from first char first_char_uc = attr[0].upper() first_char_lc = attr[0].lower() if first_char_uc in avail_short_names: short = first_char_uc avail_short_names.remove(first_char_uc) elif first_char_lc.lower() in avail_short_names: short = first_char_lc avail_short_names.remove(first_char_lc) elif avail_short_names: short = avail_short_names.pop(0) else: # we are out of single-character short names seq += 1 short = f"_{seq}" inverse_key_map[attr] = short key_map[short] = attr # Build/update type_map & type_list node_type = node.get("type") if node_type and node_type not in type_map: type_idx = len(type_list) type_list.append(node_type) type_map[node_type] = type_idx #: Short names of attrs that are passed as posiotional arg positional_short_names = [inverse_key_map.get(p, p) for p in positional] positional_short_names_set = set(positional_short_names) # ---------- # Pass 2: collect used attribute and type names for parent_idx, node in _iter_dict_pre_order(child_list): # Replace `"type": "TYPE_NAME"` with `"type": INDEX` node_type = node.get("type") if node_type: type_idx = type_map.get(node_type) node["type"] = type_idx # Replace `"FULL_NAME": VALUE` with `"SHORT_NAME": VALUE` for attr, val in list(node.items()): short = inverse_key_map.get(attr) if short: node[short] = val del node[attr] if format == FileFormat.flat: pos_args = [node.get(p) for p in positional_short_names] key_args = { k: v for k, v in node.items() if k not in positional_short_names_set } key_args.pop(inverse_key_map.get("children", "children"), None) if key_args: elem = (parent_idx, *pos_args, key_args) else: elem = (parent_idx, *pos_args) node_list.append(elem) # else: # node = if format == FileFormat.flat: children = node_list else: children = child_list print("Attribute usage:", attr_counts) # print("inverse_key_map:", inverse_key_map) # print("positional:", positional) # print("positional_short_names:", positional_short_names) # print("type_map:", type_map) # print("type_list:", type_list) # print("key_map:", key_map) # print("node_list:", node_list) # Declare complete dict here, so we can control the order res = { "_format": format.value, # "_version": 1, "types": types, "columns": columns, "_valueMap": {"type": type_list}, # "_typeList": type_list, "_keyMap": inverse_key_map, # since v0.7.0 "_positional": positional, "children": children, } if format != FileFormat.flat: res.pop("_positional") # pprint(res) return res def compress_source_file(file_path, *, key_map: dict) -> dict: with open(file_path, "rt") as fp: source = json.load(fp) return compress_child_list(source, key_map=key_map) if __name__ == "__main__": raise RuntimeError("Run `python make_fixture.py` instead.") ================================================ FILE: test/generator/make_fixture.py ================================================ """ This script generates fixture data for different tree structures. The script contains several functions that generate fixture data for different tree structures. Each function corresponds to a specific fixture and returns a dictionary representing the tree data. The available fixtures are: - 'store_XL': Generates fixture data for a store tree structure. - 'department_M': Generates fixture data for a department tree structure. - 'fmea_XL': Generates fixture data for a failure mode and effects analysis (FMEA) tree structure. The naming conventions for the fixture functions are as follows: - The name of the fixture function should be prefixed with '_generate_fixture_'. - follwed by a name that describes the tree structure - and a suffix that indicates the size of the tree structure (e.g., 'XL', 'M', 'S'). To generate fixture data for a specific tree structure, pass the name of the fixture as a command-line argument when running the script. Example usage: python make_fixture.py store_XL The generated fixture data is written to JSON files in different formats: - tree_NAME_p.json: Plain list format: Each node is represented as a dictionary in a nested list. No compression is applied. - tree_NAME_o.json: Standard format: One top-level dictionary with a 'children' key containing a nested list child nodes. No compression is applied. - tree_NAME_c.json: Standard format with columns: a 'columns' key containing the column definitions. No compression is applied. - tree_NAME_t.json: Standard format with types: a 'types' key containing the node type definitions. No compression is applied except for type references. - tree_NAME_t_c.json: Standard format with types and columns. No compression is applied except for type references. - tree_NAME_t_c_comp.json: Standard format with types, columns, and compression: The child nodes are compressed using `_valueMap` and `_keyMap` mappings. - tree_NAME_t_c_flat_comp.json: Flat parent-referencing list with types, columns, and compression: The child nodes are compressed using `_valueMap`, `_keyMap`, and `_positional` mappings. The generated JSON files are saved in the 'fixtures' directory. """ from copy import deepcopy from datetime import date import json import os from pathlib import Path import sys from textwrap import dedent sys.path.append(os.path.dirname(__file__)) from generator import ( Automatic, FileFormat, compress_child_list, generate_random_wb_source, ) from nutree.tree_generator import ( DateRangeRandomizer, RangeRandomizer, SampleRandomizer, SparseBoolRandomizer, TextRandomizer as Fab, BlindTextRandomizer as Blind, ) # ------------------------------------------------------------------------------ # Fixture: 'store' # ------------------------------------------------------------------------------ def _generate_fixture_store_XL() -> dict: # --- Node Types --- type_dict = { "folder": {"colspan": True}, "book": {"icon": "bi bi-book"}, "computer": {"icon": "bi bi-laptop"}, "music": {"icon": "bi bi-disc"}, "phone": {"icon": "bi bi-phone"}, } # --- Define Columns --- column_list = [ {"id": "*", "title": "Product", "width": "250px"}, {"id": "author", "title": "Author", "width": "200px"}, {"id": "year", "title": "Year", "width": "60px", "classes": "wb-helper-end"}, {"id": "qty", "title": "Qty", "width": "80px", "classes": "wb-helper-end"}, {"id": "sale", "title": "Sale", "width": "60px", "classes": "wb-helper-center"}, { "id": "price", "title": "Price ($)", "width": "90px", "classes": "wb-helper-end", }, # In order to test horizontal scrolling, we need a fixed or at least minimal width: {"id": "details", "title": "Details", "width": "*", "minWidth": "600px"}, ] # --- Compression Hints --- key_map = Automatic positional = [ "title", "type", "author", "year", "qty", "price", "details", ] # --- Build nested node dictionary --- structure_def = { #: Relations define the possible parent / child relationships between #: node types and optionally override the default properties. "relations": { "__root__": { "product_group": { ":count": 10, "type": "folder", "title": Fab("$(Noun:plural)"), }, }, "product_group": { "product_subgroup": { ":count": RangeRandomizer(70, 130), "type": "folder", "title": Fab("$(Adj) $(Noun:plural)"), }, }, "product_subgroup": { "product": { # ":count": 10, ":count": RangeRandomizer(0, 200), # ":callback": _person_callback, "type": SampleRandomizer(("book", "computer", "music", "phone")), "title": Fab("$(Noun)"), "author": Fab("$(name:middle)"), "year": DateRangeRandomizer(date(2, 1, 1), date(2023, 12, 31)), "qty": RangeRandomizer(1, 1_000_000, probability=0.9, none_value=0), "price": RangeRandomizer(0.01, 10_000.0), "sale": SparseBoolRandomizer(probability=0.1), "details": Fab("$(Verb:s) $(noun:plural) $(adv:#positive)."), }, }, }, } random_data = generate_random_wb_source(structure_definition=structure_def) random_data.update( { "types": type_dict, "columns": column_list, "key_map": key_map, "positional": positional, "children": random_data["child_list"], } ) return random_data # ------------------------------------------------------------------------------ # Fixture: 'department' # ------------------------------------------------------------------------------ def _generate_fixture_department_M() -> dict: CB_COUNT = 50 # --- Node Types --- type_dict = { "department": {"icon": "bi bi-diagram-3", "colspan": True}, "role": {"icon": "bi bi-microsoft-teams", "colspan": True}, "person": {"icon": "bi bi-person"}, } # --- Define Columns --- column_list = [ {"title": "Title", "id": "*", "width": "250px", "sortable": True}, { "title": "Age", "id": "age", "width": "50px", "html": "", "classes": "wb-helper-end", "sortable": True, }, { "title": "Date", "id": "date", "width": "100px", "html": '', "sortable": True, }, { "title": "Mood", "id": "mood", "width": "70px", "html": dedent( """ """ ), "sortable": True, }, { "title": "Remarks", "id": "remarks", "width": "300px", "html": "", "sortable": True, }, ] for i in range(1, CB_COUNT): column_list.append( { "title": f"#{i}", "id": f"state_{i}", "width": "30px", "classes": "wb-helper-center", "html": "", "sortable": False, } ) # --- Compression Hints --- key_map = Automatic positional = [ "title", "type", "state", "avail", "age", "date", "remarks", ] # --- Build nested node dictionary --- def _person_callback(data): # Initialize checkbox values vr = SparseBoolRandomizer(probability=0.2) for i in range(1, CB_COUNT + 1): key = f"state_{i}" val = vr.generate() if val is None: data.pop(key, None) else: data[key] = val return # --- Build nested node dictionary --- structure_def = { #: Relations define the possible parent / child relationships between #: node types and optionally override the default properties. "relations": { "__root__": { "department": { ":count": 10, "type": "department", "title": Fab("Dept. for $(Noun:plural) and $(Noun:plural)"), # "expanded": SparseBoolRandomizer(probability=0.2), }, }, "department": { "role": { ":count": RangeRandomizer(8, 13), "type": "role", "title": Fab("$(Verb) $(noun:plural)"), # "expanded": SparseBoolRandomizer(probability=0.3),7 }, }, "role": { "person": { ":count": RangeRandomizer(0, 22), ":callback": _person_callback, "type": "person", "title": Fab("$(name:middle)"), "state": SampleRandomizer(("h", "s"), probability=0.3), "avail": SparseBoolRandomizer(probability=0.9), "age": RangeRandomizer(21, 99), "date": DateRangeRandomizer( date(1970, 1, 1), date.today(), probability=0.6, ), # "remarks": Fab( # "$(Verb:s) $(noun:plural) $(adv:#positive).", probability=0.3 # ), "remarks": Blind( dialect="ipsum", sentence_count=1, probability=0.3 ), }, }, }, } random_data = generate_random_wb_source(structure_definition=structure_def) random_data.update( { "types": type_dict, "columns": column_list, "key_map": key_map, "positional": positional, "children": random_data["child_list"], } ) return random_data # ------------------------------------------------------------------------------ # Fixture: 'fmea' # ------------------------------------------------------------------------------ def _generate_fixture_fmea_XL() -> dict: # --- Node Types --- type_dict = { "function": {"icon": "bi bi-gear"}, "failure": {"icon": "bi bi-exclamation-triangle"}, "causes": {"icon": "bi bi-tools", "expanded": True}, "cause": {"icon": "bi bi-tools"}, "effects": {"icon": "bi bi-lightning", "expanded": True}, "effect": {"icon": "bi bi-lightning"}, } # --- Define Columns --- column_list = None # column_list = [ # { # "title": "Title", # "id": "*", # "width": "250px", # }, # ] # --- Compression Hints --- key_map = Automatic positional = Automatic # Uses default (title, type) # --- Build nested node dictionary --- structure_def = { "relations": { "__root__": { "function": { ":count": 200, "type": "function", "title": Fab(["Deliver $(verb:ing)", "Produce $(noun:plural)"]), # "expanded": SparseBoolRandomizer(probability=0.1), "expanded": True, }, }, "function": { "failure": { ":count": RangeRandomizer(1, 32), "type": "failure", "title": Fab( ["$(Noun) is $(adj:#negative)", "$(Noun) not $(verb:ing)"] ), # "expanded": SparseBoolRandomizer(probability=0.3),7 }, }, "failure": { "causes": { ":count": 1, "type": "causes", "title": "Causes", }, "effects": { ":count": 1, "type": "effects", "title": "Effects", }, }, "causes": { "cause": { ":count": RangeRandomizer(1, 35, probability=0.8), "type": "cause", "title": Fab("$(Noun:plural) not provided"), }, }, "effects": { "effect": { ":count": RangeRandomizer(1, 35, probability=0.8), "type": "effect", "title": Fab("$(Noun:plural) not provided"), }, }, }, } random_data = generate_random_wb_source(structure_definition=structure_def) random_data.update( { "types": type_dict, "columns": column_list, "key_map": key_map, "positional": positional, "children": random_data["child_list"], } ) return random_data # ------------------------------------------------------------------------------ # Main CLI # ------------------------------------------------------------------------------ def _size_disp(path: Path) -> str: size = path.stat().st_size if size > 500_000: return f"{round(0.000001*size, 2):,} MiB" elif size > 3000: return f"{round(0.001*size, 2):,} kiB" return f"{size:,} bytes" def _write_json(path: Path, data: dict, *, debug: bool): with open(path, "wt") as fp: if debug: json.dump(data, fp, indent=4, separators=(", ", ": ")) else: json.dump(data, fp, indent=None, separators=(",", ":")) print(f"Created {path}, {_size_disp(path)}") def main(locals): # --- Find all implementation functions (starting with 'generate_fixture_') METHOD_PREFIX = "_generate_fixture_" METHOD_PREFIX_LEN = len(METHOD_PREFIX) DEBUG = False BASE_DIR = Path(__file__).parent.parent / "fixtures" FILE_PREFIX = "tree_" avail = [ name[METHOD_PREFIX_LEN:] for name in locals if name.startswith(METHOD_PREFIX) ] avail_disp = "'{}'".format("', '".join(avail)) if len(sys.argv) != 2: print("Usage: `python make_fixture.py NAME`") print(f"Supported names: {avail_disp}") sys.exit(1) fixture_name = sys.argv[1] method = locals.get(f"{METHOD_PREFIX}{fixture_name}") if not callable(method): print(f"Invalid fixture name: {fixture_name!r}. Expected {avail_disp}") sys.exit(1) # --- Call the genreator method random_data = method() col_count = len(random_data["columns"]) if random_data.get("columns") else 0 # base_name = f'{FILE_PREFIX}{fixture_name}_{tree_data["node_count_disp"]}_{tree_data["depth"]}_{col_count}' base_name = f"{FILE_PREFIX}{fixture_name}" print(f"Writing results to {BASE_DIR}") # Remove previous fixtures for fn in BASE_DIR.glob(f"{FILE_PREFIX}{fixture_name}_*"): fn.unlink() print(f"REMOVED {fn}") # Write as plain list file_name = f"{base_name}_p.json" path = BASE_DIR / file_name out = random_data["child_list"] _write_json(path, out, debug=DEBUG) # Extended Standard (object format) file_name = f"{base_name}_o.json" path = BASE_DIR / file_name out = {"children": random_data["children"]} _write_json(path, out, debug=DEBUG) if col_count: # Extended standard with columns file_name = f"{base_name}_c.json" path = BASE_DIR / file_name out = {"columns": random_data["columns"], "children": random_data["children"]} _write_json(path, out, debug=DEBUG) if random_data["types"]: # Extended standard with types file_name = f"{base_name}_t.json" path = BASE_DIR / file_name out = {"types": random_data["types"], "children": random_data["children"]} _write_json(path, out, debug=DEBUG) if col_count: # Extended standard with types and columns file_name = f"{base_name}_t_c.json" path = BASE_DIR / file_name out = { "types": random_data["types"], "columns": random_data["columns"], "children": random_data["children"], } _write_json(path, out, debug=DEBUG) suffix = "" if random_data["types"]: suffix += "_t" if col_count: suffix += "_c" file_name = f"{base_name}{suffix}_flat_comp.json" path = BASE_DIR / file_name out = compress_child_list( deepcopy(random_data["child_list"]), # DEEP-COPY, because nodes are modified format=FileFormat.flat, types=random_data["types"], columns=random_data["columns"], key_map=random_data["key_map"], positional=random_data["positional"], auto_compress=True, ) _write_json(path, out, debug=DEBUG) file_name = f"{base_name}{suffix}_comp.json" path = BASE_DIR / file_name out = compress_child_list( random_data["child_list"], format=FileFormat.nested, types=random_data["types"], columns=random_data["columns"], key_map=random_data["key_map"], positional=random_data["positional"], auto_compress=True, ) _write_json(path, out, debug=DEBUG) print( "Generated tree with {node_count:,} nodes, {col_count} columns, depth: {depth}".format( **random_data, col_count=col_count ) ) if __name__ == "__main__": main(locals=locals()) ================================================ FILE: test/playground.html ================================================
    ================================================ FILE: test/playground.js ================================================ /* * Note: This file must be import as module: * `` */ /* eslint-disable no-console */ import { Wunderbaum } from "../build/wunderbaum.esm.js"; // import { Wunderbaum } from "../build/wunderbaum.esm.min.js"; const util = Wunderbaum.util; // const ModeElemTemplate = ``; // let sequence = 1; const tree = new Wunderbaum({ element: "#tree", // checkbox: (node) => "radio", // checkbox: "radio", // checkbox: true, id: "Playground", // rowHeightPx: 40, // enabled: false, fixedCol: true, debugLevel: 4, // minExpandLevel: 1, emptyChildListExpandable: true, // tooltip: (e) => { // return `${e.node.title} (${e.node.children?.length || 0})`; // }, // autoCollapse: true, header: true, //"Playground", iconMap: Object.assign(Wunderbaum.iconMaps.bootstrap, { doc: "😍", expanderCollapsed: "🤔", expanderExpanded: "🤗", }), // navigationModeOption: "cell", // scrollIntoViewOnExpandClick: false, showSpinner: true, columns: [ { title: "test", id: "*", width: "200px" }, // { // title: "Fav", // id: "favorite", // width: "30px", // classes: "wb-helper-center", // html: "", // }, { title: "Details", id: "details", width: "100px", html: "", headerClasses: "wb-helper-center", sortable: true, filterable: true, menu: true, // headerClasses: "", classes: "wb-helper-end", }, // { title: "Mode", id: "mode", width: "100px" }, { title: "Date", id: "date", width: "100px", html: "", }, ], types: { book: { icon: "bi bi-book", classes: "extra-book-class" }, folder: { // icon: "bi bi-folder", // checkbox: "radio", }, }, columnsResizable: true, source: "../docs/assets/json/ajax-tree-products.json", // source: // "https://cdn.jsdelivr.net/gh/mar10/assets@master/wunderbaum/tree_fmea_XL_t_flat_comp.json", // source: { // children: [ // { title: "a", type: "book", details: "A book", children: [] }, // { // title: "b", // children: [ // { // title: "ba", // }, // ], // }, // { title: "c", children: null }, // { title: "d", children: false }, // { title: "e" }, // ], // }, dnd: { effectAllowed: "all", dropEffectDefault: "move", guessDropEffect: true, dragStart: (e) => { // if (e.node.type === "folder") { // return false; // } return true; }, dragEnter: (e) => { // console.log(`DragEnter ${e.event.dataTransfer.dropEffect} ${e.node}`, e); // We can only drop 'over' a folder, so the source node becomes a child. // We can drop 'before' or 'after' a non-folder, so the source node becomes a sibling. if (e.node.type === "folder") { // e.event.dataTransfer.dropEffect = "link"; return "over"; } return ["before", "after"]; }, drag: (e) => { // e.tree.log(e.type, e); }, drop: (e) => { console.log( `Drop ${e.sourceNode} => ${e.suggestedDropEffect} ${e.suggestedDropMode} ${e.node}`, e ); switch (e.suggestedDropEffect) { case "copy": e.node.addNode( { title: `Copy of ${e.sourceNodeData.title}` }, e.suggestedDropMode ); break; case "link": e.node.addNode( { title: `Link to ${e.sourceNodeData.title}` }, e.suggestedDropMode ); break; default: e.sourceNode.moveTo(e.node, e.suggestedDropMode); } }, }, edit: { trigger: ["F2", "macEnter"], minlength: 2, maxlength: 10, edit: (e) => { e.node.log(`${e.type}`); }, apply: (e) => { e.node.log(`${e.type}`); if (e.newValue.indexOf("x") >= 0) { throw new util.ValidationError("No 'x' please"); } }, }, filter: { // mode: "hide", }, lazyLoad: (e) => { // return {url: "../docs/assets/json/ajax-lazy-products.json"}; return util.setTimeoutPromise(() => { return { url: "../docs/assets/json/ajax-lazy-products.json" }; }, 1000); }, activate: (e) => { tree.log(e.type, e); }, click: (e) => { tree.log(e.type, e); }, buttonClick: (e) => { tree.log(e.type, e); if (e.command === "filter") { // ... ... // Update the button state e.info.colDef.filterActive = !e.info.colDef.filterActive; tree.update("colStructure"); } if (e.command === "sort") { e.tree.sortByProperty({ colId: e.info.colId, updateColInfo: true }); } else if (e.command === "menu") { // eslint-disable-next-line no-alert alert("Open menu..."); } }, deactivate: (e) => {}, discard: (e) => {}, change: (e) => { const node = e.node; const value = e.inputValue; node.log(e.type, e, value, node.data); // Simulate a validation error from the client: switch (e.info.colId) { case "age": if (e.inputValue > 20) { throw new util.ValidationError("Age must be <= 20"); } break; case "date": if (e.inputElem.valueAsDate > new Date()) { // throw new util.ValidationError("Date must be in the past"); e.inputElem.setCustomValidity("Date must be in the past"); } break; } // Simulate a async/delayed behavior: return util.setTimeoutPromise(() => { node.log( `Change '${e.info.colId}': '${node.data[e.info.colId]}' -> '${value}'` ); // Simulate a validation error from the server: if (("" + value).indexOf("x") >= 0) { throw new util.ValidationError("No 'x' please"); } // Read the value from the input control that triggered the change event: node.data[e.info.colId] = value; }, 500); }, render: (e) => { const node = e.node; // node.log(e.type, e, node, e.nodeElem.querySelector("span.wb-title")); // e.nodeElem.querySelector( "span.wb-title" ).innerHTML = `${node.title}`; for (const col of Object.values(e.renderColInfosById)) { switch (col.id) { default: // Assumption: we named column.id === node.data.NAME util.setValueToElem(col.elem, node.data[col.id]); break; } } }, // iconBadge: (e) => { // const count = e.node.children?.length || 0; // const subMatch = e.node.subMatch || 0; // if (subMatch > 0) { // return { badge: subMatch, badgeTooltip: `${subMatch} matches` }; // } else if (count > 0 && !e.node.expanded) { // if (count > 99) { // return { badge: "99+", badgeTooltip: `${count} children` }; // } // // return { badge: count }; // // return { badge: count, classes: "badge-primary" }; // // return "" + count; // return count; // // return `${count}`; // } // }, init: (e) => { // e.tree.findFirst("Anthony Ross")?.setActive(true, { // colIdx: "*", // edit: true, // focusTree: true, // }); // e.tree.findFirst("Observe")?.setTooltip("This is a tooltip"); // const res = e.tree.filterNodes(/^jo[eh]/i, { // mode: "hide", // hideExpanders: true, // // matchBranch: true, // // leavesOnly: true, // // fuzzy: true, // autoExpand: true, // }); // e.tree.log("matches", e.tree.countMatches(), res); }, }); console.log(`Created ${tree}`); tree.ready .then(() => { console.log(`${tree} is ready.`); }) .catch((err) => { console.error(`${tree} init failed.`, err); }); // document.body.style.setProperty("--wb-node-text-color", "#ff8080"); // document.querySelector("div.wunderbaum").style.setProperty("--wb-font-stack", "monospace"); document.querySelectorAll(".demo-btn").forEach((elem) => { elem.addEventListener("click", (e) => { const action = e.target.dataset.action; switch (action) { case "collapseAll": { tree.logTime("iter"); let count = 0; // for (const node of tree) { // count++; // } tree.visit((node) => { count++; }); tree.logTimeEnd("iter"); tree.log(`count: ${count}`); tree.expandAll(false, { // force: true, depth: 2, }); } break; case "expandAll": tree.expandAll(true, { loadLazy: true, depth: 5, }); break; case "test1": // console.info(tree.getActiveNode()._format_line(tree.root)); // console.info((tree.getActiveNode() || tree.root).format((n)=>n.title)); // tree.sortChildren(null, true) tree.getActiveNode().setIcon("bi bi-diagram-3"); // tree.columns.push( // { title: "Mode", id: "mode_" + sequence++, width: "100px" } // ) // tree.update("colStructure") break; } }); }); ================================================ FILE: test/unit/ajax-simple-sub.json ================================================ [{ "title": "SubNode 1" }, { "title": "SubNode 2" }] ================================================ FILE: test/unit/ajax-simple.json ================================================ [ { "title": "Node 1", "expanded": true, "children": [{ "title": "Node 1.1" }, { "title": "Node 1.2" }] }, { "title": "Node 2", "lazy": true } ] ================================================ FILE: test/unit/test-core-fixture1.js ================================================ /*! * Wunderbaum - Unit Test * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ const { test } = QUnit; const Wunderbaum = mar10.Wunderbaum; const FIXTURE_1 = [ { title: "Node 1", expanded: true, children: [{ title: "Node 1.1" }, { title: "Node 1.2" }], }, { title: "Node 2", lazy: true }, ]; QUnit.module("Fixture-1 tests", (hooks) => { let tree; hooks.beforeEach(() => { tree = new Wunderbaum({ element: "#tree", source: FIXTURE_1, }); }); hooks.afterEach(() => { tree.destroy(); tree = null; }); test("fixture ok", (assert) => { assert.expect(1); assert.equal(tree.count(), 4); }); }); ================================================ FILE: test/unit/test-core.js ================================================ /*! * Wunderbaum - Unit Test * Copyright (c) 2021-2025, Martin Wendt. Released under the MIT license. * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ /* global mar10, QUnit */ /* eslint-disable no-console */ const { test } = QUnit; const Wunderbaum = mar10.Wunderbaum; const util = Wunderbaum.util; const FIXTURE_1 = [ { title: "Node 1", expanded: true, children: [{ title: "Node 1.1" }, { title: "Node 1.2" }], }, { title: "Node 2", lazy: true }, ]; /* Setup */ QUnit.testStart(function () { window.sessionStorage.clear(); window.localStorage.clear(); }); /* Tear Down */ QUnit.testDone(function () {}); QUnit.module("Utility tests", (hooks) => { test("Static utility functions", (assert) => { assert.expect(2); assert.equal(util.type([]), "array", "type([])"); assert.equal(util.type({}), "object", "type({})"); }); }); QUnit.module("Static tests", (hooks) => { test("Access static properties", (assert) => { assert.expect(4); assert.true(Wunderbaum.version != null, "Statics defined"); assert.throws( function () { const _dummy = Wunderbaum(); }, /TypeError/, "Fail if 'new' keyword is missing" ); assert.throws( function () { const _dummy = new Wunderbaum(); }, /Error: Invalid 'element' option: null/, "Fail if option is missing" ); assert.throws( function () { const _dummy = new Wunderbaum({}); }, /Error: Invalid 'element' option: null/, "Fail if 'element' option is missing" ); }); }); QUnit.module("Instance tests", (hooks) => { let tree = null; hooks.beforeEach(() => {}); hooks.afterEach(() => { tree.destroy(); tree = null; }); test("Initial event sequence (fetch)", (assert) => { assert.expect(5); assert.timeout(1000); // Timeout after 1 second const done = assert.async(); tree = new Wunderbaum({ element: "#tree", source: "ajax-simple.json", // source: FIXTURE_1, receive: (e) => { assert.step("receive"); assert.equal( e.response[0].title, "Node 1", "receive(e) passes e.response" ); }, load: (e) => { assert.step("load"); }, // render: (e) => { // assert.step("render"); // }, init: (e) => { assert.step("init"); assert.verifySteps(["receive", "load", "init"], "Event sequence"); done(); }, }); }); test("Lazy load (fetch)", (assert) => { assert.expect(8); assert.timeout(1000); // Timeout after 1 second const done = assert.async(); let initComplete = false; tree = new Wunderbaum({ element: "#tree", source: "ajax-simple.json", lazyLoad: (e) => { if (initComplete) { assert.step("lazyLoad"); assert.equal( e.node.title, "Node 2", "lazyLoad(e) passes parent node" ); return { url: "ajax-simple-sub.json" }; } }, receive: (e) => { if (initComplete) { assert.step("receive"); assert.equal( e.response[0].title, "SubNode 1", "receive(e) passes e.response" ); } }, load: (e) => { if (initComplete) { assert.step("load"); assert.verifySteps( ["init", "lazyLoad", "receive", "load"], "Event sequence" ); done(); } }, // render: (e) => { // assert.step("render"); // }, init: (e) => { initComplete = true; assert.step("init"); const lazyNode = tree.findFirst("Node 2"); assert.equal(lazyNode.title, "Node 2", "Find node by name"); // We need the markup, to issue a click event // tree.updateViewport(true); // assert.true(lazyNode.isRendered(), "Node is rendered"); // lazyNode.colspan.click(); lazyNode.setExpanded(); }, }); }); test("applyCommand", (assert) => { assert.expect(2); assert.timeout(1000); // Timeout after 1 second const done = assert.async(); tree = new Wunderbaum({ element: "#tree", source: FIXTURE_1, init: (e) => { const node1 = tree.findFirst("Node 1"); const node2 = tree.findFirst("Node 2"); assert.equal(node1.getPrevSibling(), null); node1.applyCommand("moveDown"); assert.equal(node1.getPrevSibling(), node2); // Avoid errors reported by ResizeObserver done(); }, }); }); test("clones", (assert) => { assert.expect(11); assert.timeout(1000); // Timeout after 1 second const done = assert.async(); tree = new Wunderbaum({ element: "#tree", source: [ { title: "Node 1", key: "1", refKey: "n1" }, { title: "Node 2", key: "2", refKey: "nX" }, { title: "Node 3", key: "3", refKey: "nX" }, ], init: (e) => { const n1 = tree.findKey("1"); const n2 = tree.findKey("2"); const n3 = tree.findKey("3"); // console.warn(`tree.findByRefKey('nX'): >${tree.findByRefKey("nX")}<`); assert.deepEqual(tree.findByRefKey("x"), []); assert.deepEqual(tree.findByRefKey("n1"), [n1]); assert.equal(tree.findByRefKey("nX").length, 2); assert.false(n1.isClone()); assert.true(n2.isClone()); assert.true(n3.isClone()); assert.deepEqual(n1.getCloneList(), []); assert.deepEqual(n1.getCloneList(true), [n1]); assert.equal(n2.getCloneList().length, 1); assert.equal(n2.getCloneList(false).length, 1); assert.equal(n2.getCloneList(true).length, 2); done(); }, }); }); }); ================================================ FILE: test/unit/test-dev.html ================================================ Test Suite (DEV) | Wunderbaum
    ================================================ FILE: test/unit/test-dist.html ================================================ Test Suite (DIST) | Wunderbaum
    ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./build" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, // "noUnusedParameters": true /* Report errors on unused parameters. */, // "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, // "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, "typeRoots": ["./node_modules/@types"] }, "exclude": [ "node_modules", "build", "dist", "docs", "**/*.test.ts", "**/*.spec.ts" ], "typedocOptions": { // "name": "API Docs | Wunderbaum", "entryPoints": [ "src/wunderbaum.ts", "src/wb_node.ts", "src/wb_options.ts", "src/common.ts", "src/types.ts", "src/util.ts" ], "out": "docs/api", "excludePrivate": true, "excludeProtected": true, "includeVersion": true } } ================================================ FILE: typedoc.json ================================================ { "intentionallyNotExported": [ "src/debounce.ts:DebouncedFunction", "src/debounce.ts:DebounceOptions", "src/debounce.ts:ThrottleOptions", "src/debounce.ts:Procedure" // "src/debounce.ts" ] }