├── .editorconfig
├── .eslintrc
├── .github
├── FUNDING.yml
└── workflows
│ ├── gh-pages.yml
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cypress.config.js
├── cypress
├── e2e
│ └── single-model
│ │ └── single-model.spec.js
├── fixtures
│ └── single-model
│ │ ├── .discoveryrc.cjs
│ │ └── ui
│ │ └── page
│ │ └── default.js
├── plugins
│ └── index.сjs
├── support
│ ├── commands.js
│ └── e2e.js
└── test.sh
├── dist
├── .gitignore
└── .npmignore
├── docs
├── embed.md
├── encodings.md
├── load-data.md
└── upload.md
├── gh-pages.config.cjs
├── gh-pages.extensions.js
├── models
├── embed.html
├── index.cjs
└── load-benchmark
│ ├── index.cjs
│ ├── ui.css
│ └── ui.js
├── package-lock.json
├── package.json
├── scripts
└── transpile.js
├── src
├── core
│ ├── action.ts
│ ├── color-scheme.ts
│ ├── dict.ts
│ ├── emitter.ts
│ ├── encodings
│ │ ├── index.ts
│ │ ├── json.ts
│ │ ├── jsonxl-snapshot9.js
│ │ ├── jsonxl-snapshot9.ts
│ │ ├── jsonxl.ts
│ │ └── utils.ts
│ ├── object-marker.ts
│ ├── observer.ts
│ ├── page.ts
│ ├── preset.ts
│ ├── text-view.ts
│ ├── utils
│ │ ├── base64.ts
│ │ ├── compare.ts
│ │ ├── container-styles.ts
│ │ ├── copy-text.ts
│ │ ├── debounce.ts
│ │ ├── dom.ts
│ │ ├── html.ts
│ │ ├── id.ts
│ │ ├── index-script.ts
│ │ ├── index.ts
│ │ ├── inject-styles.ts
│ │ ├── is-type.ts
│ │ ├── json.ts
│ │ ├── layout.ts
│ │ ├── load-data-streams.ts
│ │ ├── load-data.ts
│ │ ├── load-data.types.ts
│ │ ├── location-sync.ts
│ │ ├── logger.ts
│ │ ├── object-utils.ts
│ │ ├── pattern.ts
│ │ ├── persistent.ts
│ │ ├── pointer.ts
│ │ ├── progressbar.ts
│ │ ├── query-to-config.ts
│ │ ├── safe-filter-rx.ts
│ │ └── size.ts
│ └── view.ts
├── extensions
│ ├── embed-client.ts
│ ├── embed-host.ts
│ ├── embed-message.types.ts
│ ├── inspector.css
│ ├── inspector.js
│ ├── inspector
│ │ ├── data.js
│ │ ├── overlay.css
│ │ ├── popup-content.css
│ │ ├── popup-sidebar.css
│ │ ├── popup-toolbar.css
│ │ ├── popup.css
│ │ ├── props-config.js
│ │ ├── utils.js
│ │ └── view-tree.js
│ ├── modelfree.ts
│ ├── router.ts
│ └── upload.ts
├── favicon.png
├── lib-script.ts
├── lib.css
├── lib.ts
├── logo.svg
├── main
│ ├── app.css
│ ├── app.ts
│ ├── index.css
│ ├── index.ts
│ ├── model-extension-api.ts
│ ├── model-legacy-extension-api.ts
│ ├── model.ts
│ ├── query-suggestions.ts
│ ├── style
│ │ ├── base.css
│ │ └── sidebar.css
│ ├── view-model.css
│ └── view-model.ts
├── nav
│ ├── buttons.ts
│ ├── img
│ │ ├── burger-menu.svg
│ │ ├── clipboard.png
│ │ ├── github.svg
│ │ └── inspect.svg
│ ├── index.css
│ └── index.ts
├── pages
│ ├── default.css
│ ├── default.js
│ ├── discovery.css
│ ├── discovery.js
│ ├── discovery
│ │ ├── editor-common.css
│ │ ├── editor-query.css
│ │ ├── editor-query.ts
│ │ ├── editor-view.css
│ │ ├── editor-view.ts
│ │ ├── header.css
│ │ ├── header.ts
│ │ ├── img
│ │ │ ├── clone-root.svg
│ │ │ ├── clone.svg
│ │ │ ├── delete.svg
│ │ │ ├── edit.svg
│ │ │ ├── expand.svg
│ │ │ ├── formatting.svg
│ │ │ ├── fullscreen-off.svg
│ │ │ ├── fullscreen-on.svg
│ │ │ ├── minus.svg
│ │ │ ├── perform.svg
│ │ │ ├── plus.svg
│ │ │ ├── share.svg
│ │ │ ├── stash-root.svg
│ │ │ ├── stash.svg
│ │ │ ├── subquery.svg
│ │ │ └── suggestions.svg
│ │ ├── params.ts
│ │ └── types.ts
│ ├── index.css
│ ├── index.js
│ ├── not-found.js
│ ├── views-showcase.css
│ ├── views-showcase.js
│ └── views-showcase
│ │ ├── intro-text-render-md.js
│ │ ├── intro-web-render-md.js
│ │ ├── view-usage-render.css
│ │ └── view-usage-render.ts
├── preloader.css
├── preloader.ts
├── text-views
│ ├── alert.js
│ ├── alert.usage.js
│ ├── badge.js
│ ├── badges.usage.js
│ ├── base-blocks.js
│ ├── base-blocks.usage.js
│ ├── blockquote.js
│ ├── blockquote.usage.js
│ ├── context.js
│ ├── context.usage.js
│ ├── headers.js
│ ├── headers.usage.js
│ ├── index.js
│ ├── link.js
│ ├── link.usage.js
│ ├── lists.js
│ ├── lists.usage.js
│ ├── source.js
│ ├── source.usage.js
│ ├── switch.js
│ ├── switch.usage.js
│ ├── table.js
│ ├── table.usage.js
│ ├── text.js
│ └── text.usage.js
├── version.ts
└── views
│ ├── button.css
│ ├── charts
│ ├── histogram.css
│ ├── histogram.js
│ └── histogram.usage.js
│ ├── context.js
│ ├── context.usage.js
│ ├── controls
│ ├── button.css
│ ├── button.js
│ ├── button.usage.js
│ ├── checkbox-list.css
│ ├── checkbox-list.js
│ ├── checkbox-list.usage.js
│ ├── checkbox.css
│ ├── checkbox.js
│ ├── checkbox.svg
│ ├── checkbox.usage.js
│ ├── content-filter.css
│ ├── content-filter.js
│ ├── content-filter.svg
│ ├── content-filter.usage.js
│ ├── dropdown.css
│ ├── dropdown.js
│ ├── dropdown.usage.js
│ ├── input.css
│ ├── input.js
│ ├── input.usage.js
│ ├── menu-item.css
│ ├── menu-item.js
│ ├── menu-item.usage.js
│ ├── menu.css
│ ├── menu.js
│ ├── menu.usage.js
│ ├── nav-button.css
│ ├── nav-button.js
│ ├── nav-button.usage.js
│ ├── progress.css
│ ├── progress.js
│ ├── progress.usage.js
│ ├── select-arrow.svg
│ ├── select.css
│ ├── select.js
│ ├── select.usage.js
│ ├── tab.css
│ ├── tab.js
│ ├── tab.usage.js
│ ├── tabs.css
│ ├── tabs.js
│ ├── tabs.usage.js
│ ├── toggle-group.css
│ ├── toggle-group.js
│ ├── toggle-group.usage.js
│ ├── toggle.css
│ └── toggle.js
│ ├── editor
│ ├── editor-mode-query.js
│ ├── editor-mode-view.js
│ ├── editors-hint.css
│ ├── editors-hint.js
│ ├── editors.css
│ └── editors.ts
│ ├── index.css
│ ├── index.js
│ ├── layout
│ ├── block.js
│ ├── block.usage.js
│ ├── column.css
│ ├── column.js
│ ├── columns.css
│ ├── columns.js
│ ├── columns.usage.js
│ ├── expand.css
│ ├── expand.js
│ ├── expand.svg
│ ├── expand.usage.js
│ ├── hstack.css
│ ├── hstack.js
│ ├── hstack.usage.js
│ ├── list-item.css
│ ├── list-item.js
│ ├── lists.css
│ ├── lists.js
│ ├── lists.usage.js
│ ├── page-header.css
│ ├── page-header.js
│ ├── page-header.usage.js
│ ├── popup.css
│ ├── popup.ts
│ ├── section.css
│ ├── section.js
│ ├── section.usage.js
│ ├── toc-section.css
│ └── toc-section.js
│ ├── signature
│ ├── collect-stat.js
│ ├── const.js
│ ├── index.js
│ ├── render-details.js
│ ├── render-stat.js
│ ├── signature.usage.js
│ └── style
│ │ ├── action-button.css
│ │ ├── details-popup.css
│ │ ├── img
│ │ └── group.svg
│ │ └── index.css
│ ├── struct
│ ├── click-handler.js
│ ├── el-proto.ts
│ ├── index.js
│ ├── popup-signature.js
│ ├── popup-value-actions.js
│ ├── render-annotations.ts
│ ├── struct.usage.js
│ ├── style
│ │ ├── action-button.css
│ │ ├── annotation.css
│ │ ├── index.css
│ │ └── structure.css
│ └── value-to-html.ts
│ ├── switch.js
│ ├── switch.usage.js
│ ├── table
│ ├── table-cell-details-expand.svg
│ ├── table-cell-utils.js
│ ├── table-cell.css
│ ├── table-cell.js
│ ├── table-footer-cell.css
│ ├── table-footer-cell.js
│ ├── table-footer.css
│ ├── table-footer.js
│ ├── table-header-cell.css
│ ├── table-header-cell.js
│ ├── table-header.css
│ ├── table-header.js
│ ├── table-row.css
│ ├── table-row.js
│ ├── table-sort-asc.svg
│ ├── table-sort-desc.svg
│ ├── table-sortable.svg
│ ├── table.css
│ ├── table.js
│ └── table.usage.js
│ ├── text-render.css
│ ├── text-render.js
│ ├── text-render.usage.js
│ ├── text
│ ├── alerts.css
│ ├── alerts.js
│ ├── alerts.usage.js
│ ├── app-header.css
│ ├── app-header.js
│ ├── app-header.usage.js
│ ├── auto-link.js
│ ├── badges.css
│ ├── badges.js
│ ├── badges.usage.js
│ ├── blockquote-caution.svg
│ ├── blockquote-important.svg
│ ├── blockquote-note.svg
│ ├── blockquote-tip.svg
│ ├── blockquote-warning.svg
│ ├── blockquote.css
│ ├── blockquote.js
│ ├── blockquote.usage.js
│ ├── headers-anchor.svg
│ ├── headers.css
│ ├── headers.js
│ ├── headers.usage.js
│ ├── html.js
│ ├── html.usage.js
│ ├── image-preview.css
│ ├── image-preview.js
│ ├── image-preview.usage.js
│ ├── image.css
│ ├── image.js
│ ├── image.svg
│ ├── image.usage.js
│ ├── indicator.css
│ ├── indicator.js
│ ├── indicator.usage.js
│ ├── link.css
│ ├── link.js
│ ├── link.usage.js
│ ├── markdown-marked-renderer.ts
│ ├── markdown.css
│ ├── markdown.js
│ ├── markdown.usage.js
│ ├── source-copied.svg
│ ├── source-copy.svg
│ ├── source.css
│ ├── source.js
│ ├── source.usage.js
│ ├── text-match.css
│ ├── text-match.js
│ ├── text-match.usage.js
│ ├── text-numeric.css
│ ├── text-numeric.js
│ ├── text-numeric.usage.js
│ ├── text.js
│ └── text.usage.js
│ └── tree
│ ├── tree-leaf.css
│ ├── tree-leaf.js
│ ├── tree.css
│ ├── tree.js
│ └── tree.usage.js
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 4
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: discoveryjs
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GH-pages
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [published]
7 |
8 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
9 | jobs:
10 | # This workflow contains a single job called "build"
11 | build:
12 | # The type of runner that the job will run on
13 | runs-on: ubuntu-latest
14 |
15 | # Steps represent a sequence of tasks that will be executed as part of the job
16 | steps:
17 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: '20.x'
23 | - run: npm ci
24 | - run: npm run build-gh-pages
25 | - name: Deploy
26 | uses: peaceiris/actions-gh-pages@v3
27 | with:
28 | github_token: ${{ secrets.GITHUB_TOKEN }}
29 | publish_dir: ./.gh-pages
30 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Test changes
2 | on:
3 | push:
4 | pull_request:
5 |
6 | env:
7 | PRIMARY_NODEJS_VERSION: 22
8 |
9 | jobs:
10 | cypress-run:
11 | name: Cypress run
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }}
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }}
19 | cache: "npm"
20 |
21 | - run: npm ci
22 | - run: npm run transpile
23 |
24 | - name: Cypress run
25 | uses: cypress-io/github-action@v6
26 | with:
27 | start: npm run cypress:server
28 | wait-on: 'http://localhost:8124'
29 | record: true
30 | env:
31 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33 |
34 | lint:
35 | name: Lint
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v4
39 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }}
40 | uses: actions/setup-node@v4
41 | with:
42 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }}
43 | cache: "npm"
44 | - run: npm ci
45 | - run: npm run lint
46 |
47 | typecheck:
48 | name: TypeScript typings
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: actions/checkout@v4
52 | - name: Setup node ${{ env.PRIMARY_NODEJS_VERSION }}
53 | uses: actions/setup-node@v4
54 | with:
55 | node-version: ${{ env.PRIMARY_NODEJS_VERSION }}
56 | cache: "npm"
57 | - run: npm ci
58 | - run: npm run typecheck
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gh-pages
2 | /node_modules/
3 | cypress/screenshots
4 | cypress/videos
5 | tmp/
6 | lib/
7 | build/
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-2025 Roman Dvornov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cypress.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | projectId: 't2rgkz',
5 | includeShadowDom: true,
6 | e2e: {
7 | // We've imported your old cypress plugins here.
8 | // You may want to clean this up later by importing these.
9 | // setupNodeEvents(on, config) {
10 | // return require('./cypress/plugins/index.сjs')(on, config)
11 | // },
12 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}'
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/cypress/e2e/single-model/single-model.spec.js:
--------------------------------------------------------------------------------
1 | describe('Single model', () => {
2 | it('Has model name', () => {
3 | cy.visit('localhost:8124');
4 | cy.contains('Single model');
5 | });
6 |
7 | it('Opens report page', () => {
8 | cy.visit('localhost:8124');
9 | cy.contains('Discover').click();
10 | cy.get('input').should('have.attr', 'placeholder', 'Untitled discovery');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/cypress/fixtures/single-model/.discoveryrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Single model',
3 | data: () => ({ hello: 'world' }),
4 | view: {
5 | basedir: __dirname + '/ui',
6 | assets: [
7 | 'page/default.js'
8 | ]
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/cypress/fixtures/single-model/ui/page/default.js:
--------------------------------------------------------------------------------
1 | discovery.page.define('default', [
2 | 'h1:#.model.name',
3 | 'struct'
4 | ]);
5 |
--------------------------------------------------------------------------------
/cypress/plugins/index.сjs:
--------------------------------------------------------------------------------
1 | //
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (/* on, config */) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | };
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/cypress/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | npm run transpile
3 | npm run cypress:server &
4 | PID=$!
5 | cypress run --headless
6 | kill $PID
7 |
--------------------------------------------------------------------------------
/dist/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !.npmignore
4 |
--------------------------------------------------------------------------------
/dist/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !discovery-embed-host.js
3 | !discovery-embed-host.d.ts
4 | # !discovery-embed-host.js.map
5 | !discovery-preloader.css
6 | !discovery-preloader.js
7 | # !discovery-preloader.js.map
8 | !discovery.css
9 | !discovery.js
10 | # !discovery.js.map
11 |
--------------------------------------------------------------------------------
/docs/encodings.md:
--------------------------------------------------------------------------------
1 | # Encodings
2 |
3 | Discovery.js allows the configuration of custom encodings in addition to the default ones. Encodings are utilized during payload loading to decode the payload into a JavaScript object. An encoding configuration is defined by an object with these properties:
4 |
5 | - `name`: A unique string identifier for the encoding.
6 | - `test`: A function that receives the first chunk of the payload and a `done` flag (indicating whether more payload is expected or if it's complete) and returns a boolean to indicate if the encoding is applicable.
7 | - `streaming` (optional, defaults to `false`): Determines if the encoding supports streaming processing.
8 | - `decode`: A function that decodes the payload into a JavaScript value. It accepts an async iterator if `streaming` is `true`, or `Uint8Array` otherwise.
9 |
10 | The TypeScript type definition for an encoding is as follows:
11 |
12 | ```ts
13 | type Encoding = {
14 | name: string;
15 | test(chunk: Uint8Array): boolean;
16 | } & ({
17 | streaming: true;
18 | decode(iterator: AsyncIterableIterator): Promise;
19 | } | {
20 | streaming: false;
21 | decode(payload: Uint8Array): any;
22 | });
23 | ```
24 |
25 | Encodings can be set using the `encodings` option in `Model`, `Widget` and `App` configurations. The [Loading Data API](./load-data.md) applies the specified encodings to the data payload, and `Model` (base class for `App` and `Widget`) integrates these encodings into its `loadData*` method calls if they are specified upon initialization.
26 |
27 | In addition to `App` and `Widget`, preloaders can pass the `encodings` configuration to a data loader if specified in `loadDataOptions`.
28 |
29 | Custom encodings are applied before the default encodings, which are, in order of application:
30 |
31 | - `jsonxl` (`snapshot9`), non-streaming
32 | - `json` (utilizing `@discoveryjs/json-ext`), streaming
33 |
34 | Example of a custom encoding:
35 |
36 | ```js
37 | new App({
38 | encodings: [
39 | {
40 | name: 'lines/counter',
41 | test: () => true, // Always applicable
42 | streaming: false,
43 | decode: (payload) => new TextDecoder().decode(payload).split('\n').length
44 | }
45 | ]
46 | });
47 | ```
48 |
--------------------------------------------------------------------------------
/gh-pages.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Discovery.js',
3 | version: require('./package.json').version,
4 | description: 'Explore, analyze, share',
5 | icon: './src/logo.svg',
6 | view: {
7 | assets: [
8 | './gh-pages.extensions.js'
9 | ]
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/gh-pages.extensions.js:
--------------------------------------------------------------------------------
1 | import modelfree from './src/extensions/modelfree.js';
2 | import upload from './src/extensions/upload.js';
3 | import * as navButtons from './src/nav/buttons.js';
4 |
5 | discovery.apply([
6 | modelfree,
7 | upload.setup({ clipboard: true }),
8 | navButtons.uploadFile,
9 | navButtons.uploadFromClipboard,
10 | navButtons.unloadData
11 | ]);
12 |
13 | discovery.nav.primary.append({
14 | name: 'github',
15 | text: '',
16 | href: 'https://github.com/discoveryjs/discovery',
17 | external: true
18 | });
19 |
--------------------------------------------------------------------------------
/models/index.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Discovery examples',
3 | models: {
4 | discovery: '../gh-pages.config.cjs',
5 | 'load-benchmark': './load-benchmark/index.cjs'
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/models/load-benchmark/index.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'Data load benchmark',
3 | view: {
4 | basedir: __dirname,
5 | assets: [
6 | 'ui.css',
7 | 'ui.js'
8 | ]
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/core/action.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import { Dictionary } from './dict.js';
4 |
5 | type Action = {
6 | name: string;
7 | callback: (...args: unknown[]) => unknown;
8 | }
9 |
10 | export class ActionManager extends Dictionary {
11 | #actionMap: Readonly>;
12 |
13 | constructor() {
14 | super(true);
15 |
16 | this.#actionMap = Object.create(null);
17 | }
18 |
19 | define(name: string, callback: Action['callback']) {
20 | if (typeof callback !== 'function') {
21 | throw new Error('callback is not a function');
22 | }
23 |
24 | this.#actionMap = Object.freeze({
25 | ...this.#actionMap,
26 | [name]: callback
27 | });
28 |
29 | return ActionManager.define(this, name, Object.freeze({
30 | name,
31 | callback
32 | }));
33 | }
34 |
35 | revoke(name: string) {
36 | if (this.has(name)) {
37 | const map = { ...this.#actionMap };
38 | delete map[name];
39 | this.#actionMap = Object.freeze(map);
40 | }
41 |
42 | super.revoke(name);
43 | }
44 |
45 | get actionMap() {
46 | return this.#actionMap;
47 | }
48 |
49 | call(name: string, ...args: unknown[]) {
50 | const action = this.get(name);
51 |
52 | if (action === undefined) {
53 | throw new Error(`action "${name}" doesn't exist`);
54 | }
55 |
56 | const { callback } = action;
57 |
58 | return callback(...args);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/core/dict.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import { Emitter } from './emitter.js';
4 |
5 | export class Dictionary extends Emitter<{
6 | define: [key: K, value: V];
7 | revoke: [key: K];
8 | }> {
9 | #entries: Map;
10 | #allowRevoke: boolean;
11 |
12 | protected static define(dict: Dictionary, key: K, value: Readonly) {
13 | dict.#entries.set(key, value);
14 | dict.emit('define', key, value);
15 |
16 | return value;
17 | }
18 |
19 | constructor(allowRevoke?: boolean) {
20 | super();
21 |
22 | this.#entries = new Map();
23 | this.#allowRevoke = Boolean(allowRevoke);
24 | }
25 |
26 | revoke(key: K) {
27 | if (!this.#allowRevoke) {
28 | throw new Error('Entry revoking is not allowed');
29 | }
30 |
31 | this.#entries.delete(key);
32 | this.emit('revoke', key);
33 | }
34 |
35 | isDefined(key: K) {
36 | return this.#entries.has(key);
37 | }
38 | has(key: K) {
39 | return this.#entries.has(key);
40 | }
41 |
42 | get(key: K) {
43 | return this.#entries.get(key);
44 | }
45 | get names() {
46 | return [...this.#entries.keys()];
47 | }
48 |
49 | get keys() {
50 | return this.#entries.keys();
51 | }
52 | get values() {
53 | return this.#entries.values();
54 | }
55 | get entries() {
56 | return this.#entries.entries();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/core/encodings/index.ts:
--------------------------------------------------------------------------------
1 | export { encoding as json } from './json.js';
2 | export { encoding as jsonxl } from './jsonxl.js';
3 |
--------------------------------------------------------------------------------
/src/core/encodings/json.ts:
--------------------------------------------------------------------------------
1 | import type { Encoding } from '../utils/load-data.js';
2 | import { parseChunked } from '@discoveryjs/json-ext';
3 |
4 | export const encoding = /* @__PURE__ */ Object.freeze({
5 | name: 'json',
6 | test: () => true,
7 | streaming: true,
8 | decode: parseChunked
9 | }) satisfies Encoding as Encoding;
10 |
--------------------------------------------------------------------------------
/src/core/encodings/jsonxl-snapshot9.ts:
--------------------------------------------------------------------------------
1 | export { decode, isHeaderAcceptable };
2 | declare function decode(data: Uint8Array): any;
3 | declare function isHeaderAcceptable(data: Uint8Array): boolean;
4 |
--------------------------------------------------------------------------------
/src/core/encodings/jsonxl.ts:
--------------------------------------------------------------------------------
1 | import type { Encoding } from '../utils/load-data.js';
2 | import { decode, isHeaderAcceptable as test } from './jsonxl-snapshot9.js';
3 |
4 | export const encoding = /* @__PURE__ */ Object.freeze({
5 | name: 'jsonxl/snapshot9',
6 | test,
7 | streaming: false,
8 | decode
9 | }) satisfies Encoding as Encoding;
10 |
--------------------------------------------------------------------------------
/src/core/encodings/utils.ts:
--------------------------------------------------------------------------------
1 | import type { Encoding } from '../utils/load-data.js';
2 |
3 | export function validateEncodingConfig(config: any) {
4 | if (!config || typeof config !== 'object') {
5 | return 'value is not an object';
6 | }
7 |
8 | const { name, test, decode } = config;
9 |
10 | if (typeof name !== 'string') {
11 | return 'missed name';
12 | }
13 |
14 | if (typeof test !== 'function') {
15 | return 'missed test function';
16 | }
17 |
18 | if (typeof decode !== 'function') {
19 | return 'missed decode function';
20 | }
21 |
22 | return false;
23 | }
24 |
25 | export function normalizeEncodingConfig(config: any): Encoding {
26 | const error = validateEncodingConfig(config);
27 |
28 | if (error) {
29 | throw new Error(`Bad encoding config${config?.name ? ` "${config.name}"` : ''}: ${error}`);
30 | }
31 |
32 | const { name, test, streaming, decode } = config as Encoding;
33 |
34 | return Object.freeze(streaming
35 | ? {
36 | name: name || 'unknown',
37 | test,
38 | streaming: true,
39 | decode
40 | }
41 | : {
42 | name: name || 'unknown',
43 | test,
44 | streaming: false,
45 | decode
46 | }
47 | );
48 | }
49 |
50 | export function normalizeEncodings(encodings?: any[]): Encoding[] {
51 | if (!encodings) {
52 | return [];
53 | }
54 |
55 | if (!Array.isArray(encodings)) {
56 | throw new Error('Encodings must be an array');
57 | }
58 |
59 | return encodings.map(normalizeEncodingConfig);
60 | }
61 |
--------------------------------------------------------------------------------
/src/core/preset.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import type { ViewRenderer } from './view.js';
4 | import { Dictionary } from './dict.js';
5 |
6 | export type Preset = {
7 | name: string;
8 | render(el: HTMLElement | DocumentFragment, config: any, data: any, context: any): any;
9 | config: any;
10 | }
11 |
12 | export class PresetRenderer extends Dictionary {
13 | #view: ViewRenderer;
14 |
15 | constructor(view: ViewRenderer) {
16 | super();
17 |
18 | this.#view = view;
19 | }
20 |
21 | define(name: string, config: any) {
22 | // FIXME: add check that config is serializable object
23 | config = JSON.parse(JSON.stringify(config));
24 |
25 | return PresetRenderer.define(this, name, Object.freeze({
26 | name,
27 | render: (el, _, data, context) => this.#view.render(el, config, data, context),
28 | config
29 | }));
30 | }
31 |
32 | render(
33 | container: HTMLElement | DocumentFragment,
34 | name: string,
35 | data?: any,
36 | context?: any
37 | ) {
38 | const preset = this.get(name);
39 |
40 | if (!preset) {
41 | const errorMsg = 'Preset `' + name + '` is not found';
42 | console.error(errorMsg, name);
43 |
44 | const el = container.appendChild(document.createElement('div'));
45 | el.className = 'discovery-buildin-view-config-error';
46 | el.textContent = errorMsg;
47 |
48 | return Promise.resolve();
49 | }
50 |
51 | return preset.render(container, null, data, context);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/core/utils/compare.ts:
--------------------------------------------------------------------------------
1 | import { hasOwn } from './object-utils.js';
2 |
3 | export const deepEqual = (a: any, b: any) => equal(a, b, deepEqual);
4 |
5 | export function equal(a: any, b: any, compare = Object.is) {
6 | if (Object.is(a, b)) {
7 | return true;
8 | }
9 |
10 | if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
11 | return false;
12 | }
13 |
14 | for (const key in a) {
15 | if (hasOwn(a, key)) {
16 | if (!hasOwn(b, key) || !compare(a[key], b[key])) {
17 | return false;
18 | }
19 | }
20 | }
21 |
22 | for (const key in b) {
23 | if (hasOwn(b, key)) {
24 | if (!hasOwn(a, key)) {
25 | return false;
26 | }
27 | }
28 | }
29 |
30 | return true;
31 | }
32 |
--------------------------------------------------------------------------------
/src/core/utils/copy-text.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import { createElement } from './dom.js';
4 |
5 | function execCommandFallback(text: string) {
6 | const copyTextBufferEl = createElement('div', {
7 | style: [
8 | 'position: fixed',
9 | 'overflow: hidden',
10 | 'font-size: 1px',
11 | 'width: 1px',
12 | 'height: 1px',
13 | 'top: 0',
14 | 'left: 0',
15 | 'white-space: pre'
16 | ].join(';')
17 | });
18 |
19 | document.body.append(copyTextBufferEl);
20 |
21 | try {
22 | const selection = window.getSelection();
23 | const range = document.createRange();
24 |
25 | copyTextBufferEl.append(text);
26 | range.selectNodeContents(copyTextBufferEl);
27 | selection?.removeAllRanges();
28 | selection?.addRange(range);
29 | document.execCommand('copy');
30 | } finally {
31 | copyTextBufferEl.remove();
32 | }
33 | }
34 |
35 | export async function copyText(text: string) {
36 | try {
37 | if (navigator.clipboard) {
38 | try {
39 | const permissionStatus = await navigator.permissions.query({
40 | name: 'clipboard-write' as PermissionName // see https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1245
41 | });
42 |
43 | if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') {
44 | return navigator.clipboard.writeText(text);
45 | }
46 | } catch (e) {
47 | // Safari does not support the clipboard-write permission and throws a TypeError;
48 | // however, the writeText() method works fine. Attempt to call writeText() before
49 | // resorting to a fallback, believing it will work
50 | if (e instanceof TypeError) {
51 | return navigator.clipboard.writeText(text);
52 | }
53 | }
54 | }
55 | } catch {}
56 |
57 | execCommandFallback(text);
58 | }
59 |
--------------------------------------------------------------------------------
/src/core/utils/html.ts:
--------------------------------------------------------------------------------
1 | const delimRegExp = /\.\d+(?:eE[-+]?\d+)?|\B(?=(?:\d{3})+(?:\D|$))/g;
2 |
3 | export function escapeHtml(str: string) {
4 | return str
5 | .replace(/&/g, '&')
6 | .replace(/"/g, '"')
7 | .replace(//g, '>');
9 | }
10 |
11 | export function numDelimOffsets(value: unknown) {
12 | const strValue = String(value);
13 | const offsets: number[] = [];
14 | let match: RegExpExecArray | null = null;
15 |
16 | while ((match = delimRegExp.exec(strValue)) !== null) {
17 | offsets.push(match.index);
18 | }
19 |
20 | return offsets;
21 | }
22 |
23 | export function numDelim(value: unknown, escape = true) {
24 | const strValue = escape && typeof value !== 'number'
25 | ? escapeHtml(String(value))
26 | : String(value);
27 |
28 | if (strValue.length > 3) {
29 | return strValue.replace(
30 | delimRegExp,
31 | m => m || ''
32 | );
33 | }
34 |
35 | return strValue;
36 | }
37 |
--------------------------------------------------------------------------------
/src/core/utils/id.ts:
--------------------------------------------------------------------------------
1 | export function randomId() {
2 | return [
3 | performance.timeOrigin.toString(16),
4 | (10000 * performance.now()).toString(16),
5 | Math.random().toString(16).slice(2)
6 | ].join('-');
7 | }
8 |
--------------------------------------------------------------------------------
/src/core/utils/index-script.ts:
--------------------------------------------------------------------------------
1 | export * as base64 from './base64.js';
2 | export { equal, deepEqual } from './compare.js';
3 | export { debounce } from './debounce.js';
4 | export { randomId } from './id.js';
5 | export { escapeHtml, numDelim } from './html.js';
6 | export type { TypedArray } from './is-type.js';
7 | export { isTypedArray, isArray, isSet, isRegExp } from './is-type.js';
8 | export { jsonStringifyAsJavaScript, jsonStringifyInfo } from './json.js';
9 | export type * from './load-data.js';
10 | export * from './load-data.js';
11 | export type * from './logger.js';
12 | export { Logger } from './logger.js';
13 | export { objectToString, hasOwn } from './object-utils.js';
14 | export { match, matchAll } from './pattern.js';
15 | export { safeFilterRx } from './safe-filter-rx.js';
16 |
--------------------------------------------------------------------------------
/src/core/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './index-script.js';
2 |
3 | export { applyContainerStyles, rollbackContainerStyles } from './container-styles.js';
4 | export { copyText } from './copy-text.js';
5 | export type { CreateElementAttrs } from './dom.js';
6 | export { createElement, createFragment, createText, isDocumentFragment, passiveCaptureOptions, passiveSupported } from './dom.js';
7 | export type { InjectStyle, InjectInlineStyle, InjectLinkStyle } from './inject-styles.js';
8 | export { injectStyles } from './inject-styles.js';
9 | export { getBoundingRect, getOffsetParent, getOverflowParent, getPageOffset, getViewportRect } from './layout.js';
10 | export type { LocationSync } from './location-sync.js';
11 | export { createLocationSync } from './location-sync.js';
12 | export { getLocalStorageEntry, getLocalStorageValue, getSessionStorageEntry, getSessionStorageValue, PersistentStorageEntry } from './persistent.js';
13 | export { pointerXY } from './pointer.js';
14 | export type * from './progressbar.js';
15 | export { Progressbar } from './progressbar.js';
16 | export { ContentRect } from './size.js';
17 |
--------------------------------------------------------------------------------
/src/core/utils/is-type.ts:
--------------------------------------------------------------------------------
1 | import { objectToString } from './object-utils.js';
2 |
3 | export type TypedArray =
4 | | Uint8Array
5 | | Uint8ClampedArray
6 | | Uint16Array
7 | | Uint32Array
8 | | Int8Array
9 | | Int16Array
10 | | Int32Array
11 | | Float32Array
12 | | Float64Array
13 | | BigInt64Array
14 | | BigUint64Array;
15 |
16 | export function isTypedArray(value: unknown): value is TypedArray {
17 | return ArrayBuffer.isView(value) && !(value instanceof DataView);
18 | }
19 |
20 | export function isArray(value: unknown): value is Array | TypedArray {
21 | return Array.isArray(value) || isTypedArray(value);
22 | }
23 |
24 | export function isSet(value: unknown): value is Set {
25 | return objectToString(value) === '[object Set]';
26 | }
27 |
28 | export function isRegExp(value: unknown): value is RegExp {
29 | return objectToString(value) === '[object RegExp]';
30 | }
31 |
32 | export function isObject(value: unknown): value is object {
33 | return typeof value === 'object' && value !== null;
34 | }
35 |
--------------------------------------------------------------------------------
/src/core/utils/location-sync.ts:
--------------------------------------------------------------------------------
1 | import type { Logger } from './logger.js';
2 |
3 | export type LocationSync = ReturnType;
4 |
5 | export function createLocationSync(
6 | onChange: (newHash: string, oldHash: string) => void,
7 | logger?: Logger
8 | ) {
9 | const location: Location = globalThis.location;
10 | const ignoreHashChange: string[] = [];
11 | const listener = ({ newURL, oldURL }) => {
12 | const newUrlHash = new URL(newURL).hash || '#';
13 | const oldUrlHash = new URL(oldURL).hash || '#';
14 |
15 | if (newUrlHash !== ignoreHashChange.shift()) {
16 | logger?.debug('locationSync onChange:', oldUrlHash, '->', newUrlHash);
17 | ignoreHashChange.length = 0;
18 |
19 | onChange(newUrlHash, oldUrlHash);
20 | }
21 | };
22 |
23 | addEventListener('hashchange', listener);
24 |
25 | return {
26 | set(hash: string, replace: boolean) {
27 | const newHash = hash || '#';
28 | const currentHash = location.hash || '#';
29 |
30 | if (currentHash === newHash) {
31 | return;
32 | }
33 |
34 | logger?.debug('locationSync set:', newHash, replace);
35 | ignoreHashChange.push(hash);
36 |
37 | if (replace) {
38 | location.replace(hash);
39 | } else {
40 | location.hash = hash;
41 | }
42 | },
43 | dispose() {
44 | removeEventListener('hashchange', listener);
45 | }
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/core/utils/object-utils.ts:
--------------------------------------------------------------------------------
1 | const { toString, hasOwnProperty } = Object.prototype;
2 |
3 | export const objectToString = (value: unknown) => toString.call(value);
4 | export const hasOwn = Object.hasOwn || ((object: object, key: PropertyKey) => hasOwnProperty.call(object, key));
5 |
--------------------------------------------------------------------------------
/src/core/utils/pointer.ts:
--------------------------------------------------------------------------------
1 | import { passiveCaptureOptions } from './dom.js';
2 | import { Observer } from '../observer.js';
3 |
4 | export const pointerXY = /* @__PURE__*/ (() => {
5 | const lastPointerXYPublisher = new Observer({ x: 0, y: 0 }, (newCoords, oldCoords) =>
6 | newCoords.x !== oldCoords.x || newCoords.y !== oldCoords.y
7 | );
8 |
9 | document.addEventListener(
10 | 'pointermove',
11 | ({ x, y }) => lastPointerXYPublisher.set({ x, y }),
12 | passiveCaptureOptions
13 | );
14 |
15 | return lastPointerXYPublisher.readonly;
16 | })();
17 |
--------------------------------------------------------------------------------
/src/core/utils/safe-filter-rx.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | function buildRx(pattern: string, flags?: string) {
4 | try {
5 | return new RegExp('((?:' + pattern + ')+)', flags);
6 | } catch {}
7 |
8 | return new RegExp('((?:' + pattern.replace(/[\[\]\(\)\?\+\*\{\}\\]/g, '\\$&') + ')+)', flags);
9 | }
10 |
11 | export function safeFilterRx(pattern: string, flags = 'i') {
12 | return Object.assign(buildRx(pattern, flags), {
13 | rawSource: pattern
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/core/utils/size.ts:
--------------------------------------------------------------------------------
1 | import { Observer } from '../observer.js';
2 |
3 | const resizeObserverSupported = typeof ResizeObserver === 'function';
4 |
5 | export class ContentRect extends Observer {
6 | static supported = resizeObserverSupported;
7 |
8 | private el: HTMLElement | null;
9 | private observer: ResizeObserver | null;
10 |
11 | constructor() {
12 | super(null);
13 |
14 | this.el = null;
15 | this.observer = resizeObserverSupported
16 | ? new ResizeObserver(entries => {
17 | for (const entry of entries) {
18 | this.set(entry.contentRect);
19 | }
20 | })
21 | : null;
22 | }
23 |
24 | observe(el: HTMLElement | null) {
25 | if (this.observer === null) {
26 | this.el = null;
27 | return;
28 | }
29 |
30 | el = el || null;
31 |
32 | if (this.el !== el) {
33 | if (this.el !== null) {
34 | this.observer.unobserve(this.el);
35 | }
36 |
37 | if (el !== null) {
38 | this.observer.observe(el);
39 | }
40 |
41 | this.el = el;
42 | }
43 | }
44 |
45 | dispose() {
46 | this.el = null;
47 | this.observer = null;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/extensions/inspector.css:
--------------------------------------------------------------------------------
1 | @import './inspector/overlay.css';
2 | @import './inspector/popup.css';
3 | @import './inspector/popup-sidebar.css';
4 | @import './inspector/popup-toolbar.css';
5 | @import './inspector/popup-content.css';
6 |
--------------------------------------------------------------------------------
/src/extensions/inspector/popup-sidebar.css:
--------------------------------------------------------------------------------
1 | .discovery-inspect-details-popup .sidebar {
2 | grid-area: sidebar;
3 | overflow: auto;
4 | overscroll-behavior: contain;
5 | padding: 4px;
6 | background-color: var(--discovery-mate-background);
7 | }
8 | .discovery-inspect-details-popup .sidebar .view-tree-leaf-content {
9 | white-space: nowrap;
10 | padding-right: 12px;
11 | }
12 | .discovery-inspect-details-popup .sidebar .view-root {
13 | display: inline-block;
14 | margin-bottom: 1px;
15 | margin-left: -6px;
16 | border: 4px solid transparent;
17 | border-width: 1px 8px;
18 | background-color: var(--discovery-view-root-highlight-color);
19 | }
20 | .discovery-inspect-details-popup .sidebar .selected {
21 | background-color: rgba(78, 187, 255, .3);
22 | box-shadow: 0 0 0 3px rgba(78, 187, 255, .3);
23 | display: inline;
24 | }
25 | .discovery-inspect-details-popup .sidebar .skipped {
26 | text-decoration: line-through;
27 | font-style: italic;
28 | opacity: .65;
29 | }
30 |
31 | .discovery-inspect-details-popup .sidebar > .view-tree-leaf:only-child {
32 | margin: -4px 0 0 -4px;
33 | grid-template-columns: 6px auto;
34 | background: none;
35 | }
36 | .discovery-inspect-details-popup .sidebar > .view-tree-leaf:only-child > .view-tree-leaf-toggle {
37 | display: none;
38 | }
39 |
40 | .discovery-inspect-details-popup .sidebar .view-badge {
41 | vertical-align: top;
42 | margin-left: 1ex;
43 | margin-right: 0;
44 | border-radius: 0;
45 | font-size: 9px;
46 | }
47 | .discovery-inspect-details-popup .sidebar .view-badge + .view-badge {
48 | margin-left: 1px;
49 | }
50 |
--------------------------------------------------------------------------------
/src/extensions/inspector/popup-toolbar.css:
--------------------------------------------------------------------------------
1 | .discovery-inspect-details-popup .toolbar {
2 | grid-area: toolbar;
3 | display: flex;
4 | gap: 1px;
5 | }
6 |
7 | .discovery-inspect-details-popup .stack-view-chain {
8 | flex: 1;
9 | gap: 1px;
10 | background-color: var(--discovery-mate-background);
11 | }
12 | .discovery-inspect-details-popup .stack-view-chain .view-toggle {
13 | border-radius: 0;
14 | margin: 0;
15 | padding: 5px 8px;
16 | line-height: 16px;
17 | }
18 | .discovery-inspect-details-popup .stack-view-chain .skipped {
19 | text-decoration: line-through;
20 | font-style: italic;
21 | opacity: .65;
22 | }
23 | .discovery-inspect-details-popup .stack-view-chain .view-root:not(.checked):not(:hover) {
24 | background-color: var(--discovery-view-root-highlight-color);
25 | }
26 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes {
27 | position: relative;
28 | vertical-align: middle;
29 | display: inline-flex;
30 | gap: 2px;
31 | margin: -5px -2px -2px 4px;
32 | font-size: 7px;
33 | line-height: 12px;
34 | text-align: center;
35 | text-transform: uppercase;
36 | }
37 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes > * {
38 | width: 12px;
39 | height: 12px;
40 | border-radius: 8px;
41 |
42 | overflow: hidden;
43 | box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
44 | }
45 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes .data {
46 | background: #1f841f80;
47 | }
48 | .discovery-inspect-details-popup .stack-view-chain .data-flow-changes .context {
49 | background: #b1366f80;
50 | bottom: 0;
51 | }
52 |
53 | .discovery-inspect-details-popup .toolbar .view-button {
54 | padding: 5px 8px 7px;
55 | font-size: 12px;
56 | line-height: 12px;
57 | border-radius: 2px 1px 2px 2px;
58 | box-shadow: none;
59 | }
60 |
--------------------------------------------------------------------------------
/src/extensions/inspector/utils.js:
--------------------------------------------------------------------------------
1 | export function normalizeSource(text) {
2 | text = text
3 | // cut first empty lines
4 | .replace(/^(?:\s*[\n]+)+?([ \t]*)/, '$1')
5 | .trimRight();
6 |
7 | // fix empty strings
8 | text = text.replace(/\n[ \t]+(?=\n)/g, '\n');
9 |
10 | // normalize text offset
11 | const lines = text.split(/\n+/);
12 | const startLine = Number(text.match(/^\s/) === null);
13 | let minOffset = 1000;
14 |
15 | for (var i = startLine; i < lines.length; i++) {
16 | const m = lines[i].match(/^\s*/);
17 |
18 | if (m[0].length < minOffset) {
19 | minOffset = m[0].length;
20 | }
21 |
22 | if (minOffset == 0) {
23 | break;
24 | }
25 | }
26 |
27 | if (minOffset > 0) {
28 | text = text.replace(new RegExp('^ {' + minOffset + '}', 'gm'), '');
29 | }
30 |
31 | return text;
32 | }
33 |
--------------------------------------------------------------------------------
/src/extensions/modelfree.ts:
--------------------------------------------------------------------------------
1 | import { ViewModel } from '../main/view-model.js';
2 |
3 | export default (host: ViewModel) => {
4 | let defaultPageId = '';
5 |
6 | host.nav.remove('index-page');
7 | host.nav.remove('discovery-page');
8 |
9 | host.on('data', () => {
10 | if (host.defaultPageId !== host.discoveryPageId) {
11 | defaultPageId = host.defaultPageId;
12 | host.defaultPageId = host.discoveryPageId;
13 | host.setPageHash(host.pageHash, true);
14 | host.cancelScheduledRender();
15 | }
16 | });
17 | host.on('unloadData', () => {
18 | if (defaultPageId !== host.defaultPageId) {
19 | host.defaultPageId = defaultPageId;
20 | host.setPageHash(host.pageHash, true);
21 | }
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/src/extensions/router.ts:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import type { ViewModel } from '../main/view-model.js';
3 | import type { LocationSync } from '../core/utils/location-sync.js';
4 | import { createLocationSync } from '../core/utils/location-sync.js';
5 |
6 | export default function(host: ViewModel) {
7 | function createHostLocationSync() {
8 | return createLocationSync(hash => host.setPageHash(hash), host.logger);
9 | }
10 |
11 | let locationSync: LocationSync | null = createHostLocationSync();
12 |
13 | // init
14 | host.setPageHash(location.hash);
15 | host.cancelScheduledRender();
16 |
17 | // register action
18 | host.action.define('permalink', (hash: string) => new URL(hash, String(location)).href);
19 | host.action.define('setPreventLocationUpdate', (prevent: boolean = true) => {
20 | if (prevent) {
21 | locationSync?.dispose();
22 | locationSync = null;
23 | } else if (locationSync === null) {
24 | locationSync = createHostLocationSync();
25 | }
26 | });
27 |
28 | // sync
29 | host.on('pageHashChange', (replace) => locationSync?.set(host.pageHash, replace));
30 | }
31 |
--------------------------------------------------------------------------------
/src/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/favicon.png
--------------------------------------------------------------------------------
/src/lib-script.ts:
--------------------------------------------------------------------------------
1 | import { version } from './version.js';
2 | import { Model } from './main/model.js';
3 | import * as textViews from './text-views/index.js';
4 | import { encoding as jsonxl } from './core/encodings/jsonxl.js';
5 | import * as utils from './core/utils/index-script.js';
6 |
7 | export type * from './main/model.js';
8 | export {
9 | version,
10 | Model,
11 | textViews,
12 | jsonxl,
13 | utils
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib.css:
--------------------------------------------------------------------------------
1 | @import url('./pages/index.css');
2 | @import url('./views/index.css');
3 | @import url('./main/index.css');
4 |
5 | .discovery-buildin-view-render-error {
6 | display: inline-block;
7 | vertical-align: top;
8 | margin: 1px;
9 | border: 1px solid rgba(212, 0, 0, 0.4);
10 | background-image: linear-gradient(to bottom, rgba(255, 25, 25, 0.1) 19px, transparent 0);
11 | background-clip: padding-box;
12 | color: #c66;
13 | border-radius: 4px;
14 | padding: 4px;
15 | font-size: 10px;
16 | line-height: 1;
17 | }
18 | .discovery-buildin-view-render-error::before {
19 | content: 'ERROR';
20 | display: inline-block;
21 | margin: -4px 1ex -4px -4px;
22 | border-radius: 3px 0 0 3px;
23 | background: rgb(226, 36, 36, .4);
24 | color: rgba(255, 255, 255, .85);
25 | text-shadow: 1px 1px rgb(0, 0, 0, .2);
26 | padding: 4px;
27 | }
28 |
29 | .discovery-buildin-view-render-error[data-type="config"]::before {
30 | content: 'CONFIG ERROR';
31 | }
32 | .discovery-buildin-view-render-error[data-type="render"]::before {
33 | content: 'RENDER ERROR';
34 | }
35 |
36 | .discovery-buildin-view-render-error.expanded::before {
37 | border-bottom-left-radius: 0;
38 | }
39 | .discovery-buildin-view-render-error .toggle-config {
40 | margin-left: 1ex;
41 | cursor: pointer;
42 | opacity: .65;
43 | color: #888;
44 | user-select: none;
45 | }
46 | .discovery-buildin-view-render-error .toggle-config:hover {
47 | opacity: 1;
48 | }
49 | .discovery-buildin-view-render-error .view-struct {
50 | margin: 5px -4px -4px;
51 | border-radius: 0 0 3px 3px;
52 | }
53 |
54 | .discovery-buildin-view-tooltip {
55 | padding: 5px 10px;
56 | min-width: 120px;
57 | border: 0.5px solid #fff5;
58 | border-radius: 3px;
59 | font-size: 12px;
60 | background: rgba(255, 255, 255, 0.75);
61 | -webkit-backdrop-filter: blur(4px);
62 | backdrop-filter: blur(4px);
63 | }
64 | .discovery-root-darkmode .discovery-buildin-view-tooltip {
65 | background: rgba(36, 36, 36, 0.8);
66 | }
67 |
--------------------------------------------------------------------------------
/src/lib.ts:
--------------------------------------------------------------------------------
1 | import { version } from './version.js';
2 | import { Model, ViewModel, App } from './main/index.js';
3 | import * as textViews from './text-views/index.js';
4 | import * as views from './views/index.js';
5 | import * as pages from './pages/index.js';
6 | import inspector from './extensions/inspector.js';
7 | import upload from './extensions/upload.js';
8 | import router from './extensions/router.js';
9 | import embed from './extensions/embed-client.js';
10 | import { buttons as navButtons } from './nav/index.js';
11 | import { encoding as jsonxl } from './core/encodings/jsonxl.js';
12 | import * as utils from './core/utils/index.js';
13 |
14 | export type * from './main/index.js';
15 | export type * from './views/editor/editors.js';
16 | export {
17 | version,
18 | Model,
19 | ViewModel,
20 | ViewModel as Widget, // for backward compatibility
21 | App,
22 | textViews,
23 | views,
24 | pages,
25 | embed,
26 | inspector,
27 | jsonxl,
28 | router,
29 | upload,
30 | navButtons,
31 | utils
32 | };
33 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/main/index.css:
--------------------------------------------------------------------------------
1 | @import url('../views/index.css');
2 | @import url('../pages/index.css');
3 | @import url('../extensions/inspector.css');
4 | @import url('./view-model.css');
5 | @import url('./app.css');
6 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | export * from './model.js';
2 | export * from './view-model.js';
3 | export { App } from './app.js';
4 |
--------------------------------------------------------------------------------
/src/main/view-model.css:
--------------------------------------------------------------------------------
1 | @import './style/base.css';
2 | @import './style/sidebar.css';
3 | @import '../nav/index.css';
4 |
--------------------------------------------------------------------------------
/src/nav/img/burger-menu.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/nav/img/clipboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/nav/img/clipboard.png
--------------------------------------------------------------------------------
/src/nav/img/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/nav/img/inspect.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/default.css:
--------------------------------------------------------------------------------
1 | .page-default h1.no-data-loaded {
2 | margin-top: 0;
3 | }
4 | .page-default h1.no-data-loaded::before {
5 | display: inline-block;
6 | vertical-align: middle;
7 | position: relative;
8 | top: -5px;
9 | margin-right: 10px;
10 | width: 42px;
11 | height: 42px;
12 | content: '';
13 | background: var(--discovery-app-icon) no-repeat center;
14 | background-size: 42px;
15 | }
16 |
17 | .welcome-block {
18 | margin-top: -10px;
19 | }
20 | .welcome-block .upload-data {
21 | margin-top: 1.5em;
22 | margin-left: -13px;
23 | background-color: #00000008;
24 | border: 1px dashed #a8a8a8;
25 | padding: 12px;
26 | border-radius: 7px;
27 | max-width: 620px;
28 | }
29 | .discovery-root-darkmode .welcome-block .upload-data {
30 | background-color: #00000014;
31 | border-color: #555;
32 | }
33 | .welcome-block .upload-notes {
34 | font-size: 82%;
35 | color: #aaa;
36 | padding: 10px 0 0 3px;
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/default.js:
--------------------------------------------------------------------------------
1 | export default function(host) {
2 | host.page.define('default', {
3 | view: 'switch',
4 | content: [
5 | {
6 | when: 'no #.datasets',
7 | content: 'preset/welcome-block'
8 | },
9 | {
10 | content: [
11 | 'page-header{ content: "h1:#.name" }',
12 | { view: 'struct', expanded: 1 }
13 | ]
14 | }
15 | ]
16 | });
17 |
18 | // default welcome block
19 | host.preset.define('welcome-block', {
20 | view: 'block',
21 | className: 'welcome-block',
22 | data: '#.model',
23 | content: [
24 | 'app-header',
25 |
26 | {
27 | view: 'block',
28 | className: 'upload-data',
29 | when: '#.actions.uploadFile',
30 | content: [
31 | 'preset/upload',
32 | {
33 | view: 'block',
34 | className: 'upload-notes',
35 | content: 'html:name + " is a server-less application that securely opens and analyzes your data directly on your device,
ensuring all processing is done locally without transmitting your data elsewhere."'
36 | }
37 | ]
38 | }
39 | ]
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/discovery.css:
--------------------------------------------------------------------------------
1 | @import url('./discovery/header.css');
2 | @import url('./discovery/editor-common.css');
3 | @import url('./discovery/editor-query.css');
4 | @import url('./discovery/editor-view.css');
5 |
6 | .page-discovery {
7 | padding-top: 20px !important;
8 | }
9 | .discovery:not([data-dzen]) .page-discovery > .discovery-render-content {
10 | min-height: calc(100vh - 121px);
11 | }
12 |
13 | /********************************
14 | * Error
15 | ********************************/
16 |
17 | .page-discovery > .discovery-editor .discovery-error,
18 | .page-discovery > .discovery-render-content > .discovery-error {
19 | display: block;
20 | overflow: hidden;
21 | border-left: 3px solid rgba(255, 0, 0, 0.8);
22 | background: rgba(225, 75, 75, 0.2);
23 | background-clip: padding-box;
24 | padding: 8px 12px;
25 | font-size: 12px;
26 | white-space: pre-wrap;
27 | font-family: var(--discovery-monospace-font-family);
28 | font-size: 11px;
29 | }
30 | .page-discovery > .discovery-editor .discovery-error::before,
31 | .page-discovery > .discovery-render-content > .discovery-error::before {
32 | display: block;
33 | margin-bottom: .5em;
34 | font-size: 16px;
35 | }
36 | .page-discovery > .discovery-editor .query-error::before {
37 | content: 'Query error';
38 | }
39 | .page-discovery > .discovery-render-content > .render-error::before {
40 | content: 'Render error';
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/clone-root.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/clone.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/delete.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/expand.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/formatting.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/fullscreen-on.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/minus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/perform.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/plus.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/share.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/stash-root.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/stash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/subquery.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/img/suggestions.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/discovery/types.ts:
--------------------------------------------------------------------------------
1 | export type KnownParams = {
2 | dzen: boolean;
3 | noedit: boolean;
4 | title: string;
5 | query: string;
6 | graph: Graph;
7 | view: string | undefined;
8 | viewEditorHidden: boolean;
9 | };
10 | export type Params = KnownParams & {
11 | [key: string]: unknown;
12 | };
13 | export type UpdateHostParams = (patch: Partial, replace?: boolean) => void;
14 | export type Graph = {
15 | current: number[];
16 | children: GraphNode[];
17 | };
18 | export type GraphNode = Partial<{
19 | query: string;
20 | view: string;
21 | children: GraphNode[];
22 | }>;
23 | export type Computation = {
24 | state: 'successful' | 'failed' | 'awaiting' | 'computing' | 'canceled';
25 | path: number[];
26 | query: string;
27 | error: Error | null;
28 | data: unknown;
29 | context: unknown;
30 | computed: unknown;
31 | duration: number;
32 | };
33 |
--------------------------------------------------------------------------------
/src/pages/index.css:
--------------------------------------------------------------------------------
1 | @import url('./default.css');
2 | @import url('./discovery.css');
3 | @import url('./views-showcase.css');
4 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default as default } from './default.js';
2 | export { default as notFound } from './not-found.js';
3 | export { default as discovery } from './discovery.js';
4 | export { default as viewsShowcase } from './views-showcase.js';
5 |
--------------------------------------------------------------------------------
/src/pages/not-found.js:
--------------------------------------------------------------------------------
1 | export default function(host) {
2 | host.page.define('not-found', [
3 | 'alert-warning:"Page \`" + name + "\` not found"'
4 | ]);
5 | }
6 |
--------------------------------------------------------------------------------
/src/preloader.css:
--------------------------------------------------------------------------------
1 | @import './views/controls/progress.css';
2 |
3 | :host {
4 | /* all: initial; */
5 | position: absolute;
6 | z-index: 1001 !important;
7 | padding: 35px 40px;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/src/text-views/alert.js:
--------------------------------------------------------------------------------
1 | import usage from './alert.usage.js';
2 |
3 | export default function({ textView }) {
4 | textView.define('alert', {
5 | type: 'block',
6 | usage,
7 | border: {
8 | top: ['╔', '═', '╗'],
9 | left: '║ ',
10 | right: ' ║',
11 | bottom: ['╚', '═', '╝']
12 | },
13 | render(node, props, data, context) {
14 | const { content = 'text' } = props;
15 |
16 | return textView.render(node, content, data, context);
17 | }
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/src/text-views/alert.usage.js:
--------------------------------------------------------------------------------
1 | export default (view) => ({
2 | demo: `${view}:"hello world\\nsome text..."`
3 | });
4 |
--------------------------------------------------------------------------------
/src/text-views/badge.js:
--------------------------------------------------------------------------------
1 | import usage from './badges.usage.js';
2 |
3 | export default function({ textView }) {
4 | const commonConfig = {
5 | type: 'inline-block',
6 | usage,
7 | props: `is not array? | {
8 | text: #.props has no 'content' ? is (string or number or boolean) ?: text,
9 | content: undefined,
10 | prefix,
11 | postfix
12 | } | overrideProps()`,
13 | render(node, props, data, context) {
14 | const { text, content, prefix, postfix } = props;
15 | let render;
16 |
17 | if (prefix) {
18 | node.appendText(String(prefix) + ' ');
19 | }
20 |
21 | if (content) {
22 | render = this.render(node, content, data, context);
23 | } else {
24 | node.appendText(String(text));
25 | }
26 |
27 | if (postfix) {
28 | node.appendText(' ' + String(postfix));
29 | }
30 |
31 | return render;
32 | }
33 | };
34 |
35 | textView.define('badge', {
36 | ...commonConfig,
37 | border: {
38 | left: ['⎡', '⎢', '⎣', '['],
39 | right: ['⎤', '⎥', '⎦', ']']
40 | // left: ['╭ ', '│', '╰', '['],
41 | // right: [' ╮', '│', '╯', ']']
42 | }
43 | });
44 | textView.define('pill-badge', {
45 | ...commonConfig,
46 | border: {
47 | left: ['⎧', '⎪', '⎩', '('],
48 | right: ['⎫', '⎪', '⎭', ')']
49 | }
50 | });
51 | };
52 |
--------------------------------------------------------------------------------
/src/text-views/badges.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: {
3 | view,
4 | text: 'hello world'
5 | },
6 | examples: [
7 | {
8 | title: 'Variations',
9 | demo: group.map(name => [
10 | `${name}:"${name}"`,
11 | `${name}:"${name}\\nmulti-line"`,
12 | `${name}:"${name}\\nmulti-line\\nmore..."`
13 | ]).flat()
14 | },
15 | {
16 | title: 'Complex content',
17 | highlightProps: ['content'],
18 | demo: {
19 | view,
20 | content: ['text:"text"', 'link:"#example"', 'text:"text"']
21 | }
22 | }
23 | ]
24 | });
25 |
--------------------------------------------------------------------------------
/src/text-views/base-blocks.js:
--------------------------------------------------------------------------------
1 | import usage from './base-blocks.usage.js';
2 |
3 | export default function({ textView }) {
4 | function render(node, props, data, context) {
5 | const { content = 'text', border } = props;
6 |
7 | if (border) {
8 | node.setBorder(border);
9 | }
10 |
11 | return textView.render(node, content, data, context);
12 | }
13 |
14 | textView.define('inline', { type: 'inline', usage, render });
15 | textView.define('inline-block', { type: 'inline-block', usage, render });
16 | textView.define('block', { type: 'block', usage, render });
17 | textView.define('line', { type: 'line', usage, render });
18 | };
19 |
--------------------------------------------------------------------------------
/src/text-views/base-blocks.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: `${view}:'just a text'`,
3 | examples: [
4 | {
5 | title: 'Variations',
6 | demo: group.map(name => [
7 | `header:"${name}"`,
8 | 'text:"…text…"',
9 | `${name}:"⎡${name}⎦"`,
10 | 'text:"…text…"',
11 | `${name}:"⎡${name}\\nmulti-line⎦"`,
12 | 'text:"…text…"',
13 | `${name}:"⎡${name}\\nmulti-line\\nmore and more…⎦"`,
14 | 'text:"…text…"'
15 | ]).flat()
16 | },
17 | {
18 | title: 'Variations with border',
19 | demo: group.map(name => [
20 | `header:"${name}"`,
21 | 'text:"…text…"',
22 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}⎦" }`,
23 | 'text:"…text…"',
24 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}\\nmulti-line⎦" }`,
25 | 'text:"…text…"',
26 | `${name}{ border: [null, " |", null, "| "], data:"⎡${name}\\nmulti-line\\nmore and more…⎦" }`,
27 | 'text:"…text…"'
28 | ]).flat()
29 | }
30 | ]
31 | });
32 |
--------------------------------------------------------------------------------
/src/text-views/blockquote.js:
--------------------------------------------------------------------------------
1 | import usage from './blockquote.usage.js';
2 |
3 | export default function({ textView }) {
4 | textView.define('blockquote', function(node, props, data, context) {
5 | const { content = 'text', kind } = props;
6 |
7 | if (typeof kind === 'string' && /\S/.test(kind)) {
8 | node.appendLine().appendText(`[!${kind.trim().toUpperCase()}]`);
9 | }
10 |
11 | return textView.render(node, content, data, context);
12 | }, {
13 | type: 'block',
14 | usage,
15 | border: {
16 | left: '> '
17 | }
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/src/text-views/blockquote.usage.js:
--------------------------------------------------------------------------------
1 | import usage from '../views/text/blockquote.usage.js';
2 | export default usage;
3 |
--------------------------------------------------------------------------------
/src/text-views/context.js:
--------------------------------------------------------------------------------
1 | import usage from './context.usage.js';
2 |
3 | export default function({ textView }) {
4 | textView.define('context', function(node, props, data, context) {
5 | const { content = [] } = props;
6 |
7 | return textView.render(node, content, data, context);
8 | }, { usage });
9 | }
10 |
--------------------------------------------------------------------------------
/src/text-views/context.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: ['md:"A non-visual view used to group views, manage their visibility, or explicitly define data and context for nested views. In the example, the `context` view sets up specific data and context, allowing nested views (`content`) to utilize these shared definitions directly:"'],
3 | demo: {
4 | view: 'context',
5 | data: { name: 'World', age: '1000 years' },
6 | context: '{ ...#, greeting: "Hello" }',
7 | content: [
8 | 'text:`${#.greeting}, ${name}!`',
9 | 'table'
10 | ]
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/text-views/headers.js:
--------------------------------------------------------------------------------
1 | import usage from './headers.usage.js';
2 |
3 | export default function({ textView }) {
4 | function renderHeader(level) {
5 | const prefix = '#'.repeat(level) + ' ';
6 |
7 | return async function render(node, config, data, context) {
8 | const { content } = config;
9 |
10 | node.appendText(prefix);
11 | return textView.render(node, content || 'text', data, context);
12 | };
13 | }
14 |
15 | textView.define('header', renderHeader(1), { type: 'block', usage });
16 | textView.define('h1', renderHeader(1), { type: 'block', usage });
17 | textView.define('h2', renderHeader(2), { type: 'block', usage });
18 | textView.define('h3', renderHeader(3), { type: 'block', usage });
19 | textView.define('h4', renderHeader(4), { type: 'block', usage });
20 | textView.define('h5', renderHeader(5), { type: 'block', usage });
21 | }
22 |
--------------------------------------------------------------------------------
/src/text-views/headers.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: `${view}:"Header \\"${view}\\""`,
3 | examples: [
4 | {
5 | title: 'Variations',
6 | view: group.map(view => `${view}:"Header \\"${view}\\""`)
7 | },
8 | {
9 | title: 'Complex content',
10 | demo: {
11 | view,
12 | content: [
13 | 'text:"Text "',
14 | 'link:{ text: "Link" }'
15 | ]
16 | }
17 | },
18 | {
19 | title: 'Using anchor',
20 | demo: [
21 | {
22 | view,
23 | anchor: 'foo',
24 | content: 'text:"Explicit value for an anchor"'
25 | },
26 | {
27 | view,
28 | anchor: true,
29 | content: 'text:"Auto generated anchor based on text content of header"'
30 | }
31 | ]
32 | }
33 | ]
34 | });
35 |
--------------------------------------------------------------------------------
/src/text-views/index.js:
--------------------------------------------------------------------------------
1 | export * as alert from './alert.js';
2 | export * as badge from './badge.js';
3 | export * as block from './base-blocks.js';
4 | export * as blockquote from './blockquote.js';
5 | export * as context from './context.js';
6 | export * as headerViews from './headers.js';
7 | export * as link from './link.js';
8 | export * as listViews from './lists.js';
9 | export * as source from './source.js';
10 | export * as switchView from './switch.js';
11 | export * as table from './table.js';
12 | export * as textViews from './text.js';
13 |
--------------------------------------------------------------------------------
/src/text-views/link.js:
--------------------------------------------------------------------------------
1 | import usage from './link.usage.js';
2 |
3 | const props = `is not array? | {
4 | text: #.props.content is undefined ? is string ?: text,
5 | content: undefined,
6 | href
7 | } | overrideProps() | {
8 | $text; $href;
9 | ...,
10 | text: $text | is not undefined or no $href ?: $href,
11 | href: $href | is not undefined or no $text ?: $text
12 | }`;
13 |
14 | export default function({ textView }) {
15 | textView.define('link', async function(node, props, data, context) {
16 | let {
17 | text,
18 | content,
19 | href
20 | } = props;
21 |
22 | if (href) {
23 | node.appendText('[');
24 | }
25 |
26 | if (content) {
27 | await textView.render(node, content, data, context);
28 | } else {
29 | node.appendText(text);
30 | }
31 |
32 | if (href) {
33 | node.appendText(`](${href})`);
34 | }
35 | }, {
36 | type: 'inline-block',
37 | props,
38 | usage
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/src/text-views/link.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'link',
4 | text: 'I am a link',
5 | href: '#example'
6 | },
7 | examples: [
8 | {
9 | title: 'Infering text ⇿ href',
10 | highlightProps: ['text', 'href', 'data'],
11 | beforeDemo: ['md:"When `text` is omitted but `href` is specified, or vice versa, the opposite component is inferred from the specified one"'],
12 | demo: [
13 | { view: 'link', text: 'http://example1.com' },
14 | { view: 'link', href: 'http://example2.com' },
15 | { view: 'link', data: '"http://example3.com"' }
16 | ]
17 | }
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------
/src/text-views/lists.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: {
3 | view,
4 | data: ['one', 'two', 'three', 'four']
5 | },
6 | examples: [
7 | {
8 | title: 'Variations',
9 | demoData: ['foo', 'bar', 'baz'],
10 | demo: group.map(view => [
11 | `header:'view: \\"${view}\\"'`,
12 | view
13 | ]).flat()
14 | },
15 | {
16 | title: 'Variations long lists',
17 | demoData: Array.from({ length: 100 }, (_, idx) => 'item#' + idx),
18 | demo: group.map(view => [
19 | `header:'view: \\"${view}\\"'`,
20 | { view: view, limit: 3 }
21 | ]).flat()
22 | },
23 | {
24 | title: 'Configure item\'s content',
25 | demo: [
26 | {
27 | view,
28 | data: ['one', 'two', 'three', 'four'],
29 | item: [
30 | 'text:"- "',
31 | {
32 | view: 'link',
33 | data: '{ href: "#" + $ }'
34 | }
35 | ]
36 | }
37 | ]
38 | }
39 | ]
40 | });
41 |
--------------------------------------------------------------------------------
/src/text-views/switch.js:
--------------------------------------------------------------------------------
1 | import usage from './switch.usage.js';
2 |
3 | export default function(host) {
4 | host.textView.define('switch', function(node, config, data, context) {
5 | let { content } = config;
6 | let renderConfig = 'text:""';
7 |
8 | if (Array.isArray(content)) {
9 | for (const branch of content) {
10 | if (branch && host.queryBool(branch.when || true, data, context)) {
11 | renderConfig = 'data' in branch
12 | ? {
13 | view: 'context',
14 | data: branch.data,
15 | content: branch.content
16 | }
17 | : branch.content;
18 | break;
19 | }
20 | }
21 | }
22 |
23 | return host.textView.render(node, renderConfig, data, context);
24 | }, { usage });
25 | }
26 |
--------------------------------------------------------------------------------
/src/text-views/switch.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'switch',
4 | content: [
5 | {
6 | when: 'expr',
7 | content: 'text:"Renders when `expr` is truthy"'
8 | },
9 | {
10 | content: 'text:"Renders when all other `when` conditions are falsy"'
11 | }
12 | ]
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/text-views/table.usage.js:
--------------------------------------------------------------------------------
1 | const defaultDemoData = [
2 | { name: 'Alice', age: 34, occupation: 'Engineer' },
3 | { name: 'Bob', age: 42, occupation: 'Doctor' },
4 | { name: 'Charlie', age: 9, occupation: 'Student' },
5 | { name: 'David', age: 50, occupation: 'Doctor' },
6 | { name: 'Eve', age: 15, occupation: 'Engineer' }
7 | ];
8 | export default {
9 | demoData: defaultDemoData,
10 | demo: {
11 | view: 'table',
12 | limit: 2,
13 | cols: {
14 | age: { footer: 'text:sum(=>age)' },
15 | occupation: { footer: {} }
16 | }
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/text-views/text.js:
--------------------------------------------------------------------------------
1 | import usage from './text.usage.js';
2 |
3 | export default function({ textView }) {
4 | textView.define('text', {
5 | props: "{ text: #.props has no 'text'? } | overrideProps()",
6 | usage,
7 | render(node, props) {
8 | const { text } = props;
9 | node.appendText(String(text));
10 | }
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/src/text-views/text.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: 'text:"just a text"'
3 | };
4 |
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | export const version = '0.0.0-dev';
2 |
--------------------------------------------------------------------------------
/src/views/button.css:
--------------------------------------------------------------------------------
1 | /* For backward capability with discovery-cli */
2 | @import './controls/button.css';
3 |
--------------------------------------------------------------------------------
/src/views/charts/histogram.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: ['md:"A histogram"'],
3 | demoData: [0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 1, 2, 5, 25, 14, 10, 25, 22, 20, 20],
4 | demo: {
5 | view: 'histogram'
6 | },
7 | examples: [
8 | {
9 | title: 'Sizes',
10 | demoData: [0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 1, 2, 5],
11 | demo: {
12 | view: 'table',
13 | context: '{ ...#, values: $ }',
14 | rows: ['xs', 's', 'm', 'l', 'xl'],
15 | cols: [
16 | { header: 'size' },
17 | { header: 'histogram', content: ['text:123', 'histogram{ size: $, dataset: #.values }'] }
18 | ]
19 | }
20 | // ['xs', 's', 'm', 'l', 'xl'].map(size => [
21 | // `header:"${size}"`,
22 | // `histogram{ size: "${size}" }`
23 | // ]).flat()
24 | }
25 | ]
26 | };
27 |
--------------------------------------------------------------------------------
/src/views/context.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: ['md:"A non-visual view used to group views, manage their visibility, or explicitly define data and context for nested views. In the example, the `context` view sets up specific data and context, allowing nested views (`content`) to utilize these shared definitions directly:"'],
3 | demo: {
4 | view: 'context',
5 | data: { name: 'World', age: '1000 years' },
6 | context: '{ ...#, greeting: "Hello" }',
7 | content: [
8 | 'text:`${#.greeting}, ${name}!`',
9 | 'table'
10 | ]
11 | },
12 | examples: [
13 | {
14 | title: 'Using with modifiers',
15 | demo: {
16 | view: 'context',
17 | modifiers: [
18 | 'h2:"Modifiers"',
19 | {
20 | view: 'input',
21 | name: 'inputValue'
22 | },
23 | {
24 | view: 'select',
25 | name: 'selectValue',
26 | data: ['foo', 'bar', 'baz']
27 | }
28 | ],
29 | content: [
30 | 'h2:"Values"',
31 | 'struct{ expanded: 1, data: # }'
32 | ]
33 | }
34 | }
35 | ]
36 | };
37 |
--------------------------------------------------------------------------------
/src/views/controls/button.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import usage from './button.usage.js';
4 | import { createElement } from '../../core/utils/dom.js';
5 |
6 | const props = `is not array? | {
7 | text: #.props has no 'content' ? is (string or number or boolean) ?: text,
8 | content: undefined,
9 | disabled: false,
10 | href,
11 | external,
12 | onClick: undefined
13 | } | overrideProps()`;
14 |
15 | export default function(host) {
16 | function render(el, props, data, context) {
17 | const {
18 | text,
19 | content,
20 | disabled,
21 | href,
22 | external,
23 | onClick
24 | } = props;
25 |
26 | el.classList.add('view-button');
27 |
28 | if (disabled) {
29 | el.disabled = true;
30 | } else if (typeof onClick === 'function') {
31 | el.addEventListener('click', () => onClick(el, data, context));
32 | el.classList.add('onclick');
33 | } else if (typeof href === 'string') {
34 | el.addEventListener('click', () => {
35 | const tmpLinkEl = createElement('a', {
36 | href,
37 | target: external ? '_blank' : ''
38 | });
39 |
40 | // temporary add element into a document (to host's root) to allow a click interception
41 | host.dom.root.append(tmpLinkEl);
42 | tmpLinkEl.click();
43 | tmpLinkEl.remove();
44 | });
45 | }
46 |
47 | if (content) {
48 | return host.view.render(el, content, data, context);
49 | } else {
50 | el.textContent = text;
51 | }
52 | }
53 |
54 | host.view.define('button', render, { tag: 'button', props, usage });
55 | host.view.define('button-primary', render, { tag: 'button', props, usage });
56 | host.view.define('button-danger', render, { tag: 'button', props, usage });
57 | host.view.define('button-warning', render, { tag: 'button', props, usage });
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/controls/button.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default (view, group) => ({
4 | demo: {
5 | view,
6 | text: 'Button',
7 | onClick: Function('return () => alert("Hello world!")')()
8 | },
9 | examples: [
10 | {
11 | title: 'Variations',
12 | demo: group.map(name => `${name}{ text: "${name}" }`)
13 | },
14 | {
15 | title: 'Disabled state',
16 | demo: group.map(name => ({
17 | view: name,
18 | disabled: true,
19 | text: name
20 | }))
21 | },
22 | {
23 | title: 'Complex content',
24 | demo: {
25 | view: 'button',
26 | content: [
27 | 'text:"Example "',
28 | 'pill-badge:123'
29 | ]
30 | }
31 | },
32 | {
33 | title: 'Button as a link',
34 | demo: {
35 | view: 'button',
36 | text: 'Click me',
37 | href: '#url',
38 | external: true
39 | }
40 | },
41 | {
42 | title: 'Using data as source of options',
43 | beforeDemo: {
44 | view: 'md',
45 | source: [
46 | 'The following properties are taken from the data when the appropriate options are not specified for a legacy reasons (is subject to remove in the future):',
47 | '- `text`',
48 | '- `href`',
49 | '- `external`'
50 | ].join('\n')
51 | },
52 | highlightProps: ['data'],
53 | demo: {
54 | view,
55 | data: {
56 | text: 'demo',
57 | href: '#example',
58 | external: true
59 | }
60 | }
61 | }
62 | ]
63 | });
64 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox-list.css:
--------------------------------------------------------------------------------
1 | .view-checkbox-list > .view-checkbox {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox-list.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './checkbox-list.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('checkbox-list', function(el, config, data, context) {
6 | const { name = 'filter', checkbox, checkboxValue = '$', emptyText, limit, onChange, onInit } = config;
7 | const state = new Set();
8 |
9 | if (emptyText !== false && emptyText !== '') {
10 | el.setAttribute('emptyText', emptyText || 'Empty list');
11 | }
12 |
13 | if (!Array.isArray(data) && data) {
14 | data = [data];
15 | }
16 |
17 | if (Array.isArray(data)) {
18 | return host.view.renderList(el, this.composeConfig({
19 | view: 'checkbox',
20 | ...checkbox,
21 | onInit: (checked, _, itemData, itemContext) => {
22 | if (checked) {
23 | state.add(host.query(checkboxValue, itemData, itemContext));
24 | }
25 | },
26 | onChange: (checked, _, itemData, itemContext) => {
27 | const size = state.size;
28 | const value = host.query(checkboxValue, itemData, itemContext);
29 |
30 | if (checked) {
31 | state.add(value);
32 | } else {
33 | state.delete(value);
34 | }
35 |
36 | if (size !== state.size && typeof onChange === 'function') {
37 | onChange([...state], name);
38 | }
39 | }
40 | }), data, context, 0, host.view.listLimit(limit, 25)).then(() => {
41 | if (typeof onInit === 'function') {
42 | onInit([...state], name);
43 | }
44 | });
45 | }
46 | }, { usage });
47 | };
48 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox-list.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'checkbox-list',
4 | checkbox: {
5 | content: 'text'
6 | },
7 | data: [
8 | 'one',
9 | 'two',
10 | 'three'
11 | ]
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox.css:
--------------------------------------------------------------------------------
1 | .view-checkbox {
2 | --discovery-checkbox-size: max(1em, 14px);
3 | padding-left: var(--discovery-checkbox-size);
4 | }
5 | .view-checkbox > input {
6 | -webkit-appearance: none;
7 | appearance: none;
8 | content: '';
9 | font-size: inherit;
10 | display: inline-block;
11 | position: relative;
12 | top: 2px;
13 | box-sizing: border-box;
14 | height: var(--discovery-checkbox-size);
15 | width: var(--discovery-checkbox-size);
16 | margin: 0;
17 | margin-left: calc(-1 * var(--discovery-checkbox-size));
18 | background-color: rgba(255, 255, 255, .2);
19 | background-repeat: no-repeat;
20 | background-position: center;
21 | background-size: calc(.57 * var(--discovery-checkbox-size));
22 | border: 1px solid rgba(151, 162, 172, 0.65);
23 | border-radius: 3px;
24 | outline: none;
25 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
26 | }
27 | .view-checkbox > input:checked {
28 | border-color: transparent;
29 | background-color: rgba(0, 121, 232, 0.68);
30 | background-image: url('./checkbox.svg');
31 | }
32 | .view-checkbox > input:focus-visible {
33 | box-shadow: 0 0 1px 3px rgba(0, 170, 255, .2);
34 | }
35 | .view-checkbox > input:focus-visible:not(:checked) {
36 | border-color: rgba(0, 141, 255, 0.75);
37 | }
38 | .view-checkbox > input:active {
39 | border-color: transparent;
40 | background-color: rgba(25, 139, 236, .6);
41 | }
42 | .view-checkbox > input[readonly],
43 | .view-checkbox > input:disabled {
44 | box-shadow: none;
45 | border-color: rgba(165, 165, 165, 0.1);
46 | background-color: rgba(165, 165, 165, 0.3);
47 | }
48 |
49 | .view-checkbox__label {
50 | margin-left: 5px;
51 | margin-right: 15px;
52 | }
53 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { createElement } from '../../core/utils/dom.js';
3 | import usage from './checkbox.usage.js';
4 |
5 | const props = `#.props | {
6 | name: undefined,
7 | checked: undefined,
8 | readonly: undefined,
9 | content: undefined,
10 | onInit: undefined,
11 | onChange: undefined
12 | } | overrideProps() | {
13 | ...,
14 | checked is truthy,
15 | readonly is truthy
16 | }`;
17 |
18 | export default function(host) {
19 | function renderContent(contentEl, content, data, context, name, inputEl) {
20 | if (contentEl === null) {
21 | return;
22 | }
23 |
24 | const localContext = name ? { ...context, [name]: inputEl.checked } : context;
25 |
26 | contentEl.innerHTML = '';
27 | return host.view.render(contentEl, content, data, localContext);
28 | }
29 |
30 | host.view.define('checkbox', function(el, config, data, context) {
31 | const { name, checked, readonly, content, onInit, onChange } = config;
32 | const inputEl = el.appendChild(createElement('input'));
33 | const contentEl = content ? el.appendChild(createElement('span', 'view-checkbox__label')) : null;
34 |
35 | inputEl.type = 'checkbox';
36 | inputEl.checked = checked;
37 | inputEl.readOnly = readonly;
38 | inputEl.addEventListener('click', (e) => {
39 | if (readonly) {
40 | e.preventDefault();
41 | }
42 | });
43 | inputEl.addEventListener('change', () => {
44 | if (typeof onChange === 'function') {
45 | onChange(inputEl.checked, name, data, context);
46 | renderContent(contentEl, content, data, context, name, inputEl);
47 | }
48 | });
49 |
50 | if (typeof onInit === 'function') {
51 | onInit(inputEl.checked, name, data, context);
52 | }
53 |
54 | return renderContent(contentEl, content, data, context, name, inputEl);
55 | }, {
56 | tag: 'label',
57 | props,
58 | usage
59 | });
60 | }
61 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/controls/checkbox.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default {
4 | demo: {
5 | view: 'checkbox',
6 | content: 'text:"checkbox caption"'
7 | },
8 | examples: [
9 | {
10 | title: 'Checked state',
11 | beforeDemo: 'Checked state is set up with `checked` property. Its value can be a query',
12 | demo: [
13 | {
14 | view: 'checkbox',
15 | checked: true,
16 | content: 'text:"should be checked"'
17 | },
18 | {
19 | view: 'checkbox',
20 | checked: '=1 > 5',
21 | content: 'text:"shouldn\'t be checked"'
22 | },
23 | {
24 | view: 'checkbox',
25 | checked: '=1 < 5',
26 | content: 'text:"should be checked"'
27 | }
28 | ]
29 | },
30 | {
31 | title: 'Readonly checkbox',
32 | demo: {
33 | view: 'checkbox',
34 | readonly: true,
35 | content: 'text:"checkbox caption"'
36 | }
37 | },
38 | {
39 | title: 'On change',
40 | demo: {
41 | view: 'checkbox',
42 | // eslint-disable-next-line no-unused-vars
43 | onChange: Function('return (value, name, data, context) => alert(`Changed to ${value}!`)')(),
44 | content: 'text:"click me!"'
45 | }
46 | }
47 | ]
48 | };
49 |
--------------------------------------------------------------------------------
/src/views/controls/content-filter.css:
--------------------------------------------------------------------------------
1 | .view-content-filter > .view-input {
2 | line-height: 1;
3 | margin-bottom: 1em;
4 | }
5 |
6 | .view-content-filter > .view-input input {
7 | background-image: url('./content-filter.svg');
8 | background-repeat: no-repeat;
9 | background-size: 32px 16px;
10 | background-position: right center;
11 | padding-right: 30px;
12 | }
13 |
14 | .view-content-filter > .content {
15 | overflow: auto;
16 | flex: 1;
17 | }
18 | .view-content-filter > .content .view-list::before {
19 | padding: 5px 10px;
20 | display: block;
21 | }
22 |
--------------------------------------------------------------------------------
/src/views/controls/content-filter.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './content-filter.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('content-filter', function(el, config, data, context) {
6 | const { name = 'filter', type = 'regexp', placeholder, content, onInit, onChange, debounce } = config;
7 |
8 | return host.view.render(el, {
9 | view: 'context',
10 | modifiers: {
11 | view: 'input',
12 | name,
13 | type,
14 | placeholder: placeholder || 'Filter',
15 | debounce
16 | },
17 | content: {
18 | view: 'block',
19 | className: 'content',
20 | content,
21 | onInit,
22 | onChange
23 | }
24 | }, data, context);
25 | }, {
26 | usage
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/views/controls/content-filter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/controls/content-filter.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'content-filter',
4 | data: ['foo', 'bar', 'baz'],
5 | content: {
6 | view: 'list',
7 | data: '.[$ ~= #.filter]'
8 | }
9 | },
10 | examples: [
11 | {
12 | title: 'Using with text-match',
13 | demo: {
14 | view: 'content-filter',
15 | data: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }],
16 | name: 'customName',
17 | content: {
18 | view: 'list',
19 | data: '.[name ~= #.customName]',
20 | item: 'text-match:{ text: name, match: #.customName }'
21 | }
22 | }
23 | }
24 | ]
25 | };
26 |
--------------------------------------------------------------------------------
/src/views/controls/dropdown.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | export default {
3 | demo: {
4 | view: 'context',
5 | modifiers: {
6 | view: 'dropdown',
7 | name: 'demo',
8 | value: { foo: 'two', bar: 'hello' },
9 | resetValue: { foo: 'one', bar: '' },
10 | caption: 'text:`${#.demo.foo} / ${#.demo.bar}`',
11 | content: [
12 | { view: 'select', name: 'foo', data: ['one', 'two', 'three', 'four'] },
13 | { view: 'input', name: 'bar' }
14 | ]
15 | },
16 | content: [
17 | { view: 'block', content: 'text:"Modified context (see values in \\"demo\\" section):"' },
18 | { view: 'struct', expanded: 1, data: '#' }
19 | ]
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/views/controls/input.css:
--------------------------------------------------------------------------------
1 | .view-input input {
2 | position: relative;
3 | width: 100%;
4 | box-sizing: border-box;
5 | margin: 0;
6 | padding: 8px 12px;
7 | font-family: inherit;
8 | font-size: inherit;
9 | line-height: 1.2;
10 | border: 1px solid rgba(153, 153, 153, 0.5);
11 | border-radius: 3px;
12 | background-color: rgba(255, 255, 255, .05);
13 | color: var(--discovery-color);
14 | outline: 0;
15 | }
16 | .view-input input:hover {
17 | border-color: rgba(153, 153, 153, 0.75);
18 | }
19 | .view-input input:focus {
20 | border-color: rgba(0, 170, 255, .65);
21 | box-shadow: 0 0 1px 3px rgba(0, 170, 255, .2), inset 0 1px 1px rgba(142, 142, 142, .2);
22 | z-index: 1;
23 | }
24 |
--------------------------------------------------------------------------------
/src/views/controls/input.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | const onChange = Function('return (value) => alert(`Changed to ${value}!`)')();
3 |
4 | export default {
5 | examples: [
6 | {
7 | title: 'Input with value',
8 | demo: {
9 | view: 'input',
10 | value: '"value"'
11 | }
12 | },
13 | {
14 | title: 'Input with placeholder',
15 | demo: {
16 | view: 'input',
17 | placeholder: 'placeholder'
18 | }
19 | },
20 | {
21 | title: 'Input type number with min and max',
22 | demo: {
23 | view: 'input',
24 | htmlType: 'number',
25 | htmlMin: 10,
26 | htmlMax: 20
27 | }
28 | },
29 | {
30 | title: 'Input with onChange',
31 | demo: {
32 | view: 'input',
33 | onChange: onChange
34 | }
35 | },
36 | {
37 | title: 'Input with onChange debounced',
38 | demo: {
39 | view: 'input',
40 | onChange: onChange,
41 | debounce: 300
42 | }
43 | }
44 | ]
45 | };
46 |
--------------------------------------------------------------------------------
/src/views/controls/menu-item.css:
--------------------------------------------------------------------------------
1 | .view-menu-item {
2 | display: block;
3 | padding: 4px 12px;
4 | color: inherit;
5 | text-decoration: none;
6 | cursor: pointer;
7 | }
8 | .view-menu-item:empty::before {
9 | content: '\200B';
10 | float: left;
11 | }
12 | .view-menu-item:hover,
13 | .view-menu-item.discovery-view-popup-active {
14 | background: rgba(131, 131, 131, 0.25);
15 | }
16 | .view-menu-item.selected {
17 | background: rgba(178, 221, 248, 0.3);
18 | cursor: default;
19 | }
20 | .view-menu-item.disabled {
21 | opacity: .65;
22 | background: none;
23 | pointer-events: none;
24 | }
25 | .view-menu-item:not(.onclick):not([href]) {
26 | pointer-events: none;
27 | }
28 |
--------------------------------------------------------------------------------
/src/views/controls/menu-item.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './menu-item.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('menu-item', function(el, config, data, context) {
6 | const { content, onClick } = config;
7 | const { text, selected = false, disabled = false, href, external } = data || {};
8 |
9 | if (disabled) {
10 | el.classList.add('disabled');
11 | } else if (typeof onClick === 'function') {
12 | el.addEventListener('click', () => onClick(data, context));
13 | el.classList.add('onclick');
14 | } else if (href) {
15 | el.href = href;
16 | el.target = external ? '_blank' : '';
17 | }
18 |
19 | if (selected) {
20 | el.classList.add('selected');
21 | }
22 |
23 | if (content) {
24 | return host.view.render(el, content, data, context);
25 | } else {
26 | el.textContent = typeof data === 'string' ? data : text || 'Untitled item';
27 | }
28 | }, {
29 | tag: 'a',
30 | usage
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/views/controls/menu-item.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'menu',
4 | data: [
5 | { text: 'one', href: '#' },
6 | { text: 'two', href: '#' },
7 | { text: 'three', href: '#' }
8 | ]
9 | },
10 | examples: [
11 | {
12 | title: 'Preselected item',
13 | demo: {
14 | view: 'menu',
15 | data: [
16 | { text: 'one', href: '#' },
17 | { text: 'two', href: '#', selected: true},
18 | { text: 'three', href: '#' }
19 | ]
20 | }
21 | },
22 | {
23 | title: 'Disabled item',
24 | demo: {
25 | view: 'menu',
26 | data: [
27 | { text: 'one', href: '#' },
28 | { text: 'two', href: '#', disabled: true},
29 | { text: 'three', href: '#' }
30 | ]
31 | }
32 | },
33 | {
34 | title: 'External links',
35 | demo: {
36 | view: 'menu',
37 | data: [
38 | { text: 'one', external: true, href: 'https://github.com/discoveryjs/discovery' },
39 | { text: 'two', external: true, href: 'https://github.com/discoveryjs/discovery' },
40 | { text: 'three', external: true, href: 'https://github.com/discoveryjs/discovery' }
41 | ]
42 | }
43 | }
44 | ]
45 | };
46 |
--------------------------------------------------------------------------------
/src/views/controls/menu.css:
--------------------------------------------------------------------------------
1 | .view-menu:empty::before {
2 | content: attr(emptyText);
3 | display: block;
4 | padding: 4px 12px;
5 | color: #888;
6 | }
7 |
8 | .view-menu > .more-buttons {
9 | margin: 2px 12px 8px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/controls/menu.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './menu.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('menu', function(el, config, data, context) {
6 | const { name = 'filter', item, itemConfig, limit, emptyText, onClick, onInit, onChange } = config;
7 |
8 | if (emptyText !== false && emptyText !== '') {
9 | el.setAttribute('emptyText', emptyText || 'No items');
10 | }
11 |
12 | if (Array.isArray(data)) {
13 | const composedItemConfig = this.composeConfig({
14 | view: 'menu-item',
15 | content: item,
16 | onClick: typeof onClick === 'function'
17 | ? onClick
18 | : typeof onChange === 'function'
19 | ? (data) => onChange(data, name)
20 | : undefined
21 | }, itemConfig);
22 |
23 | return host.view.renderList(
24 | el,
25 | composedItemConfig,
26 | data,
27 | context,
28 | 0,
29 | host.view.listLimit(limit, 25)
30 | ).then(() => {
31 | if (typeof onInit === 'function') {
32 | onInit(data.find(item => item.selected), name);
33 | }
34 | });
35 | }
36 | }, { usage });
37 | }
38 |
--------------------------------------------------------------------------------
/src/views/controls/menu.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | const onChange = Function('return (value) => alert(`Changed to ${value.text}!`)')();
3 |
4 | export default {
5 | demo: {
6 | view: 'menu',
7 | data: [
8 | { text: 'one', href: '#' },
9 | { text: 'two', href: '#' },
10 | { text: 'three', href: '#' }
11 | ]
12 | },
13 | examples: [
14 | {
15 | title: 'With limit',
16 | demo: {
17 | view: 'menu',
18 | data: [
19 | { text: 'one', href: '#' },
20 | { text: 'two', href: '#' },
21 | { text: 'three', href: '#' }
22 | ],
23 | limit: 2
24 | }
25 | },
26 | {
27 | title: 'With custom item',
28 | demo: {
29 | view: 'menu',
30 | data: [
31 | { text: 'one', href: '#' },
32 | { text: 'two', href: '#' },
33 | { text: 'three', href: '#' }
34 | ],
35 | item: 'h1:text'
36 | }
37 | },
38 | {
39 | title: 'On chage handler',
40 | demo: {
41 | view: 'menu',
42 | onChange: onChange,
43 | data: [
44 | { text: 'one', href: '#' },
45 | { text: 'two', href: '#' },
46 | { text: 'three', href: '#' }
47 | ]
48 | }
49 | }
50 | ]
51 | };
52 |
--------------------------------------------------------------------------------
/src/views/controls/nav-button.css:
--------------------------------------------------------------------------------
1 | .view-nav-button {
2 | vertical-align: top;
3 | display: inline-block;
4 | min-height: 1.6em;
5 | padding: 5px 15px 6px;
6 | color: #444;
7 | background-color: rgba(155, 155, 155, 0.15);
8 | font-size: 12px;
9 | text-decoration: none;
10 | outline: none;
11 | cursor: pointer;
12 | }
13 | .view-nav-button:hover,
14 | .view-nav-button:focus,
15 | .view-nav-button.discovery-view-popup-active {
16 | color: black;
17 | background-color: rgba(151, 151, 151, 0.3);
18 | }
19 | .view-nav-button + .view-nav-button {
20 | margin-left: 1px
21 | }
22 | .view-nav-button.disabled,
23 | .view-nav-button:not(.onclick):not([href]) {
24 | pointer-events: none;
25 | }
26 |
27 | .discovery-root-darkmode .view-nav-button {
28 | color: inherit;
29 | }
30 |
--------------------------------------------------------------------------------
/src/views/controls/nav-button.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './nav-button.usage.js';
3 |
4 | const props = `is not array? | {
5 | name: undefined,
6 | text: #.props has no 'content' ? is (string or number or boolean) ?: text,
7 | content: undefined,
8 | disabled: false,
9 | href,
10 | external,
11 | onClick: undefined
12 | } | overrideProps()`;
13 |
14 | export default function(host) {
15 | host.view.define('nav-button', function(el, props, data, context) {
16 | const {
17 | name,
18 | text,
19 | content,
20 | disabled,
21 | href,
22 | external,
23 | onClick
24 | } = props;
25 |
26 | if (name) {
27 | el.dataset.name = name;
28 | }
29 |
30 | if (disabled) {
31 | el.classList.add('disabled');
32 | } else if (typeof onClick === 'function') {
33 | el.addEventListener('click', (event) => onClick(el, data, context, event));
34 | el.classList.add('onclick');
35 | } else if (href) {
36 | el.href = href;
37 | el.target = external ? '_blank' : '';
38 | }
39 |
40 | if (content) {
41 | return host.view.render(el, content, data, context);
42 | } else {
43 | el.textContent = text;
44 | }
45 | }, {
46 | tag: 'a',
47 | props,
48 | usage
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/views/controls/nav-button.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | const onClick = () => alert('changed!');
3 |
4 | export default {
5 | demo: {
6 | view: 'nav-button',
7 | data: { text: 'I am nav button' }
8 | },
9 | examples: [
10 | {
11 | title: 'With href',
12 | demo: {
13 | view: 'nav-button',
14 | data: { text: 'I am nav button', href: '#' }
15 | }
16 | },
17 | {
18 | title: 'External link',
19 | demo: {
20 | view: 'nav-button',
21 | data: { text: 'I am nav button', external: true, href: 'https://github.com/discoveryjs/discovery' }
22 | }
23 | },
24 | {
25 | title: 'On click handler',
26 | demo: {
27 | view: 'nav-button',
28 | data: { text: 'I am nav button'},
29 | onClick
30 | }
31 | }
32 | ]
33 | };
34 |
--------------------------------------------------------------------------------
/src/views/controls/progress.css:
--------------------------------------------------------------------------------
1 | .view-progress {
2 | max-width: 300px;
3 | z-index: 1;
4 | pointer-events: none;
5 | }
6 |
7 | .view-progress.skip-fast-track {
8 | transition: opacity .25s var(--appearance-delay, 500ms);
9 | will-change: opacity;
10 | contain: paint;
11 |
12 | @starting-style {
13 | opacity: 0;
14 | }
15 | }
16 | .view-progress.skip-fast-track.done:not(.error) {
17 | display: none;
18 | }
19 |
20 | .view-progress > .progress {
21 | content: '';
22 | display: block;
23 | position: relative;
24 | overflow: hidden;
25 | margin-top: 4px;
26 | box-sizing: border-box;
27 | height: 3px;
28 | background: rgba(198, 198, 198, 0.3);
29 | border-radius: 2px;
30 | }
31 | .view-progress > .progress::before {
32 | content: '';
33 | display: block;
34 | height: 100%;
35 | width: 100%;
36 | position: absolute;
37 | left: 0;
38 | top: 0;
39 | transform: scaleX(var(--progress, 0));
40 | transform-origin: left;
41 | /* transition: transform .2s; */ /* since Chrome (tested on 85) freezes transition during js loop */
42 | background-color: var(--color, #1f7ec5);
43 | }
44 |
45 | .view-progress > .content.main-secondary {
46 | display: flex;
47 | white-space: nowrap;
48 | gap: 1ex;
49 | }
50 | .view-progress > .content > .secondary {
51 | flex: 1;
52 | overflow: hidden;
53 | text-overflow: ellipsis;
54 | color: #888;
55 | }
56 |
--------------------------------------------------------------------------------
/src/views/controls/progress.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { createElement } from '../../core/utils/dom.js';
3 | import usage from './progress.usage.js';
4 |
5 | export default function(host) {
6 | host.view.define('progress', function(el, config, data, context) {
7 | const { content, progress, color } = config;
8 |
9 | el.append(createElement('div', {
10 | class: 'progress',
11 | style: `--progress: ${Math.max(0, Math.min(1, Number(progress)))};--color: ${color || 'unset'};`
12 | }));
13 |
14 | if (content) {
15 | const contentEl = createElement('div', 'content');
16 |
17 | el.prepend(contentEl);
18 |
19 | return host.view.render(contentEl, content, data, context);
20 | }
21 | }, { usage });
22 | }
23 |
--------------------------------------------------------------------------------
/src/views/controls/progress.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'progress',
4 | progress: .5,
5 | content: 'text:"Loading..."'
6 | },
7 | examples: [
8 | {
9 | title: 'With no label (content)',
10 | demo: {
11 | view: 'progress',
12 | progress: .25
13 | }
14 | },
15 | {
16 | title: 'Custom color',
17 | demo: {
18 | view: 'progress',
19 | progress: .85,
20 | color: '#bdab77',
21 | content: 'text:"Yellow progress"'
22 | }
23 | }
24 | ]
25 | };
26 |
--------------------------------------------------------------------------------
/src/views/controls/select-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/controls/select.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | const onChange = Function('return (value) => alert(`Changed to ${value}!`)')();
3 |
4 | export default {
5 | demo: {
6 | view: 'select',
7 | data: ['one', 'two', 'three', 'four']
8 | },
9 | examples: [
10 | {
11 | title: 'Select with value',
12 | demo: {
13 | view: 'select',
14 | value: '"three"',
15 | data: ['one', 'two', 'three', 'four']
16 | }
17 | },
18 | {
19 | title: 'Select with reset option',
20 | demo: {
21 | view: 'select',
22 | resetItem: true,
23 | value: '"three"',
24 | data: ['one', 'two', 'three', 'four']
25 | }
26 | },
27 | {
28 | title: 'Select with placeholder',
29 | demo: {
30 | view: 'select',
31 | placeholder: 'placeholder',
32 | data: ['one', 'two', 'three', 'four']
33 | }
34 | },
35 | {
36 | title: 'Select with onChange',
37 | demo: {
38 | view: 'select',
39 | onChange: onChange,
40 | data: ['one', 'two', 'three', 'four']
41 | }
42 | },
43 | {
44 | title: 'Select with custom options',
45 | demo: {
46 | view: 'select',
47 | item: 'h1:text',
48 | data: ['one', 'two', 'three', 'four']
49 | }
50 | }
51 | ]
52 | };
53 |
--------------------------------------------------------------------------------
/src/views/controls/tab.css:
--------------------------------------------------------------------------------
1 | .view-tab {
2 | display: inline-block;
3 | position: relative;
4 | padding: 3px 11px;
5 | border-bottom: var(--discovery-view-tabs-border);
6 | font-size: 13px;
7 | cursor: pointer;
8 | white-space: nowrap;
9 | text-transform: capitalize;
10 | }
11 | .view-tab.active {
12 | z-index: 10;
13 | border-bottom: none;
14 | padding-bottom: 4px;
15 | cursor: default;
16 | }
17 | .view-tab.active::before {
18 | content: '';
19 | position: absolute;
20 | z-index: -1;
21 | top: 0;
22 | left: 0;
23 | right: 0;
24 | bottom: 0;
25 | border: var(--discovery-view-tabs-border);
26 | border-bottom: none;
27 | border-radius: 5px 5px 0 0;
28 | pointer-events: none;
29 | }
30 | .view-tab.disabled {
31 | color: #aaa;
32 | }
33 | .view-tab.disabled,
34 | .view-tab:not(.onclick) {
35 | pointer-events: none;
36 | }
37 |
--------------------------------------------------------------------------------
/src/views/controls/tab.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './tab.usage.js';
3 |
4 | const props = `is not array? | {
5 | value,
6 | text: #.props has no 'content' ? is (string or number or boolean) ?,
7 | content: undefined,
8 | active: undefined,
9 | disabled: undefined,
10 | onClick: undefined
11 | } | overrideProps() | {
12 | ...,
13 | text: text is not undefined ? text : value,
14 | active is truthy,
15 | disabled is truthy
16 | }`;
17 |
18 | export default function(host) {
19 | host.view.define('tab', function(el, props, data, context) {
20 | const {
21 | value,
22 | text,
23 | content,
24 | active,
25 | disabled,
26 | onClick
27 | } = props;
28 |
29 | if (disabled) {
30 | el.classList.add('disabled');
31 | } else if (typeof onClick === 'function') {
32 | el.addEventListener('click', () => onClick(value));
33 | el.classList.add('onclick');
34 | }
35 |
36 | if (active) {
37 | el.classList.add('active');
38 | }
39 |
40 | if (content) {
41 | return host.view.render(el, content, data, context);
42 | } else {
43 | el.textContent = String(text);
44 | }
45 | }, { usage, props });
46 | }
47 |
--------------------------------------------------------------------------------
/src/views/controls/tabs.css:
--------------------------------------------------------------------------------
1 | .view-tabs-buttons {
2 | --discovery-view-tabs-border: 1px solid rgba(170, 170, 170, 0.4);
3 | margin-top: 2px;
4 | display: flex;
5 | flex-wrap: wrap;
6 | align-items: flex-end;
7 | }
8 |
9 | .view-tabs-buttons::before,
10 | .view-tabs-buttons::after {
11 | content: '';
12 | width: 6px;
13 | border-bottom: var(--discovery-view-tabs-border);
14 | }
15 | .view-tabs-buttons::after {
16 | flex: 1;
17 | }
18 |
19 | .view-tabs-buttons-before,
20 | .view-tabs-buttons-after {
21 | padding: 0 6px 3px;
22 | padding-bottom: 3px;
23 | border-bottom: var(--discovery-view-tabs-border);
24 | color: #888;
25 | font-size: 13px;
26 | }
27 |
28 | .view-tabs-content {
29 | overflow: auto;
30 | }
31 |
--------------------------------------------------------------------------------
/src/views/controls/toggle-group.css:
--------------------------------------------------------------------------------
1 | .view-toggle-group {
2 | display: inline-flex;
3 | align-items: baseline;
4 | flex-wrap: wrap;
5 | }
6 | .view-toggle-group-before {
7 | padding-right: 1ex;
8 | }
9 | .view-toggle-group-after {
10 | padding-left: 1ex;
11 | order: 1000;
12 | }
13 |
14 | .view-toggle-group .view-toggle + .view-toggle {
15 | margin-left: 1px;
16 | border-top-left-radius: 0;
17 | border-bottom-left-radius: 0;
18 | }
19 | .view-toggle-group .view-toggle:not(:last-child) {
20 | border-top-right-radius: 0;
21 | border-bottom-right-radius: 0;
22 | }
23 |
--------------------------------------------------------------------------------
/src/views/controls/toggle-group.usage.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default {
4 | demo: {
5 | view: 'context',
6 | modifiers: {
7 | view: 'toggle-group',
8 | name: 'toggleValue',
9 | data: [
10 | { value: 'one', text: 'One' },
11 | { value: 'two', text: 'Two' },
12 | { value: 'three', text: 'Three' }
13 | ]
14 | },
15 | content: {
16 | view: 'switch',
17 | content: [
18 | {
19 | when: '#.toggleValue="one"',
20 | content: 'text:"One"'
21 | },
22 | {
23 | when: '#.toggleValue="two"',
24 | content: 'text:"Two"'
25 | },
26 | {
27 | when: '#.toggleValue="three"',
28 | content: 'text:"Three"'
29 | }
30 | ]
31 | }
32 | },
33 | examples: [
34 | {
35 | title: 'With before and after content',
36 | demo: {
37 | view: 'toggle-group',
38 | data: [
39 | { value: 'one', text: 'One' },
40 | { value: 'two', text: 'Two' },
41 | { value: 'three', text: 'Three' }
42 | ],
43 | beforeToggles: 'text:""',
44 | afterToggles: 'text:""'
45 | }
46 | },
47 | {
48 | title: 'On change handler',
49 | demo: {
50 | view: 'toggle-group',
51 | name: 'example',
52 | onChange: Function('return (value) => alert("changed to " + value)')(),
53 | data: [
54 | { value: 'one', text: 'One' },
55 | { value: 'two', text: 'Two' },
56 | { value: 'three', text: 'Three' }
57 | ]
58 | }
59 | }
60 | ]
61 | };
62 |
--------------------------------------------------------------------------------
/src/views/controls/toggle.css:
--------------------------------------------------------------------------------
1 | .view-toggle {
2 | display: inline-block;
3 | position: relative;
4 | padding: 3px 12px;
5 | background: rgba(200, 200, 200, 0.2);
6 | border-radius: 4px;
7 | font-size: 13px;
8 | cursor: pointer;
9 | white-space: nowrap;
10 | }
11 | .view-toggle:hover {
12 | background-color: rgba(78, 187, 255, .2);
13 | }
14 | .view-toggle.checked {
15 | background-color: rgba(78, 187, 255, .3);
16 | cursor: default;
17 | }
18 | .view-toggle.disabled {
19 | color: #aaa;
20 | }
21 | .view-toggle.disabled,
22 | .view-toggle:not(.onclick) {
23 | pointer-events: none;
24 | }
25 |
--------------------------------------------------------------------------------
/src/views/controls/toggle.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './toggle-group.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('toggle', function(el, config, data, context) {
6 | const {
7 | content,
8 | disabled = false,
9 | onToggle,
10 | value,
11 | text = String(value).replace(/^./, m => m.toUpperCase())
12 | } = config;
13 | let {
14 | checked = false
15 | } = config;
16 |
17 | if (disabled) {
18 | el.classList.add('disabled');
19 | } else if (typeof onToggle === 'function') {
20 | el.addEventListener('click', () => {
21 | checked = !checked;
22 | onToggle(checked, value);
23 | });
24 | el.classList.add('onclick');
25 | }
26 |
27 | if (checked) {
28 | el.classList.add('checked');
29 | }
30 |
31 | if (content) {
32 | return host.view.render(el, content, data, context);
33 | } else {
34 | el.textContent = text;
35 | }
36 | }, { usage });
37 | }
38 |
--------------------------------------------------------------------------------
/src/views/editor/editor-mode-view.js:
--------------------------------------------------------------------------------
1 | import CodeMirror from 'codemirror';
2 |
3 | export default function(config, options) {
4 | const isDiscoveryViewDefined = typeof options.isDiscoveryViewDefined === 'function'
5 | ? options.isDiscoveryViewDefined
6 | : () => {};
7 | const jsMode = CodeMirror.getMode(config, {
8 | name: 'javascript',
9 | json: true
10 | });
11 |
12 | return {
13 | ...jsMode,
14 | indent(state, textAfter) {
15 | return state.indented + config.indentUnit * (
16 | (state.lastType === '{' && textAfter.trim()[0] !== '}') ||
17 | (state.lastType === '(' && textAfter.trim()[0] !== ')') ||
18 | (state.lastType === '[' && textAfter.trim()[0] !== ']')
19 | );
20 | },
21 | token: function(stream, state) {
22 | if (state.suspendTokens) {
23 | const { pos, token } = state.suspendTokens.shift();
24 |
25 | stream.pos = pos;
26 | if (state.suspendTokens.length === 0) {
27 | state.suspendTokens = null;
28 | }
29 |
30 | return token;
31 | }
32 |
33 | const start = stream.pos;
34 | const token = jsMode.token(stream, state);
35 |
36 | if (token === 'string') {
37 | const end = stream.pos;
38 | const [, viewName] = stream.string
39 | .slice(start + 1, end - 1)
40 | .match(/^(.+?)([:{]|$)/) || [];
41 |
42 | if (isDiscoveryViewDefined(viewName)) {
43 | stream.pos = start + 1;
44 | state.suspendTokens = [
45 | { pos: start + 1 + viewName.length, token: 'string discovery-view-name' },
46 | { pos: end, token }
47 | ];
48 | }
49 | }
50 |
51 | return token;
52 | }
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/src/views/layout/block.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './block.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('block', function(el, config, data, context) {
6 | const { content = [], onInit, onChange } = config;
7 | const blockContent = typeof onInit !== 'function' && typeof onChange !== 'function'
8 | ? content // left as is since nothing to mix in
9 | : this.composeConfig(content, {
10 | onInit,
11 | onChange
12 | });
13 |
14 | return host.view.render(el, blockContent, data, context);
15 | }, { usage });
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/layout/block.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: ['md:"A block has no its own look. It\'s using for wrapping some content with a `className` (btw `className` is a common property for any view when appropriate)"'],
3 | demo: {
4 | view: 'block',
5 | className: 'foo',
6 | content: [
7 | 'text:"Content inside block"'
8 | ]
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/views/layout/column.css:
--------------------------------------------------------------------------------
1 | .view-column {
2 | padding-right: 40px;
3 | min-width: 150px;
4 | }
5 | .view-column:last-child {
6 | padding-right: 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/views/layout/column.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './columns.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('column', function(el, config, data, context) {
6 | const { content = [] } = config;
7 |
8 | return host.view.render(el, content, data, context);
9 | }, { usage });
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/layout/columns.css:
--------------------------------------------------------------------------------
1 | .view-columns {
2 | display: flex;
3 | flex-wrap: wrap;
4 | }
5 |
6 | .view-columns:empty::before {
7 | content: attr(emptyText);
8 | color: #888;
9 | }
10 |
--------------------------------------------------------------------------------
/src/views/layout/columns.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './columns.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('columns', function render(el, config, data, context) {
6 | const { column, columnConfig, emptyText, limit } = config;
7 |
8 | if (emptyText !== false && emptyText !== '') {
9 | el.setAttribute('emptyText', emptyText || 'Empty');
10 | }
11 |
12 | if (!Array.isArray(data) && data) {
13 | data = [data];
14 | }
15 |
16 | if (Array.isArray(data)) {
17 | return host.view.renderList(el, this.composeConfig({
18 | view: 'column',
19 | content: column
20 | }, columnConfig), data, context, 0, host.view.listLimit(limit, 25));
21 | }
22 | }, { usage });
23 | }
24 |
--------------------------------------------------------------------------------
/src/views/layout/columns.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'columns',
4 | data: ['one', 'two', 'three', 'four'],
5 | column: 'text'
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/views/layout/expand.css:
--------------------------------------------------------------------------------
1 | .view-list > .view-list-item > .view-expand {
2 | margin-bottom: 1px;
3 | }
4 | .view-expand + .view-expand {
5 | margin-top: 1px;
6 | }
7 | .view-list > .view-list-item:not(:last-child) > .view-expand.expanded {
8 | margin-bottom: 4px;
9 | }
10 | .view-expand.expanded + .view-expand {
11 | margin-top: 4px;
12 | }
13 | .view-expand > .header {
14 | --discovery-view-expand-trigger-size: 22px;
15 |
16 | display: flex;
17 | background-color: rgba(192, 192, 192, 0.175);
18 | background-color: color-mix(in srgb, var(--discovery-background-color), rgb(192, 192, 192) 17.5%);
19 | font-size: 12px;
20 | cursor: pointer;
21 | margin-left: calc(var(--discovery-view-expand-trigger-size) + 1px);
22 | }
23 | .view-expand.trigger-outside > .header {
24 | margin-left: 0;
25 | }
26 | .view-expand > .header:hover {
27 | background-color: rgba(165, 165, 165, 0.3);
28 | background-color: color-mix(in srgb, var(--discovery-background-color), rgb(165, 165, 165) 30%);
29 | }
30 | .view-expand > .header > .header-content {
31 | flex: 1;
32 | padding: 2px 8px;
33 | overflow: hidden;
34 | text-overflow: ellipsis;
35 | pointer-events: none;
36 | }
37 | .view-expand > .header > .trigger {
38 | order: -1;
39 | width: var(--discovery-view-expand-trigger-size, 20px);
40 | margin-left: calc(-1 * var(--discovery-view-expand-trigger-size) - 1px);
41 | text-align: center;
42 | background-color: inherit;
43 | background-image: linear-gradient(0deg, rgba(150, 150, 150, .15), rgba(150, 150, 150, .15));
44 | }
45 | .view-expand > .header > .trigger::before {
46 | background: url(./expand.svg) no-repeat center;
47 | background-size: 12px;
48 | transition: transform .15s ease-in;
49 | width: 12px;
50 | height: 100%;
51 | display: inline-block;
52 | vertical-align: middle;
53 | transform: rotate(-90deg);
54 | content: '';
55 | }
56 | .view-expand.expanded > .header > .trigger::before {
57 | transform: rotate(0deg);
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/layout/expand.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { createElement } from '../../core/utils/dom.js';
3 | import usage from './expand.usage.js';
4 |
5 | const props = `is not array? | {
6 | header: undefined,
7 | content: undefined,
8 | expanded: false,
9 | onToggle: undefined
10 | } | overrideProps()`;
11 |
12 | export default function(host) {
13 | host.view.define('expand', function(el, config, data, context) {
14 | async function renderState() {
15 | el.classList.toggle('expanded', expanded);
16 |
17 | if (expanded) {
18 | contentEl = createElement('div', 'content');
19 |
20 | await host.view.render(contentEl, content, data, context)
21 | .then(() => el.appendChild(contentEl));
22 | } else if (contentEl !== null) {
23 | contentEl.remove();
24 | contentEl = null;
25 | }
26 | }
27 |
28 | let { expanded, header, content, onToggle } = config;
29 | const headerEl = el.appendChild(createElement('div', 'header'));
30 | const headerContentEl = headerEl.appendChild(createElement('div', 'header-content'));
31 | let contentEl = null;
32 |
33 | headerEl.appendChild(createElement('div', 'trigger'));
34 | headerEl.addEventListener('click', () => {
35 | expanded = !expanded;
36 |
37 | const finish = renderState();
38 |
39 | if (typeof onToggle === 'function') {
40 | onToggle(expanded, {
41 | el,
42 | finish,
43 | data,
44 | context
45 | });
46 | }
47 | });
48 |
49 | return Promise.all([
50 | host.view.render(headerContentEl, header || 'text:"\u00A0"', data, context),
51 | renderState()
52 | ]);
53 | }, { usage, props });
54 | }
55 |
--------------------------------------------------------------------------------
/src/views/layout/expand.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/layout/expand.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'expand',
4 | header: 'text:"Expand me!"',
5 | content: 'text:"Content"'
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/views/layout/hstack.css:
--------------------------------------------------------------------------------
1 | .view-hstack {
2 | display: flex;
3 | flex-wrap: wrap;
4 | gap: 20px 40px;
5 | }
6 | .view-hstack > * {
7 | min-width: 240px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/views/layout/hstack.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './hstack.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('hstack', function(el, config, data, context) {
6 | const { content = [] } = config;
7 |
8 | return host.view.render(el, content, data, context);
9 | }, { usage });
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/layout/hstack.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'hstack',
4 | content: [
5 | 'button:{text:"First button"}',
6 | 'button-primary:{text:"Second button"}'
7 | ]
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/views/layout/list-item.css:
--------------------------------------------------------------------------------
1 | .view-inline-list > .view-list-item,
2 | .view-comma-list > .view-list-item {
3 | display: inline-block;
4 | list-style: none;
5 | }
6 |
7 | .view-comma-list > .view-list-item > * {
8 | margin-right: 0;
9 | }
10 |
11 | .view-comma-list > .view-list-item::after {
12 | content: ', ';
13 | white-space: pre;
14 | }
15 |
16 | .view-comma-list > .view-list-item:last-child::after {
17 | content: '';
18 | }
19 |
--------------------------------------------------------------------------------
/src/views/layout/list-item.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default function(host) {
4 | host.view.define('list-item', function(el, config, data, context) {
5 | const { content = 'text' } = config;
6 |
7 | return host.view.render(el, content, data, context);
8 | }, {
9 | tag: 'li'
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/views/layout/lists.css:
--------------------------------------------------------------------------------
1 | .view-list {
2 | display: block;
3 | list-style: none;
4 | }
5 |
6 | .view-ul,
7 | .view-ol {
8 | margin: 0;
9 | padding-left: 0;
10 | }
11 |
12 | .view-ul:not(:empty),
13 | .view-ol:not(:empty) {
14 | padding-left: 20px;
15 | }
16 |
17 | :is(.view-list, .view-ul, .view-ol) + :is(.view-list, .view-ul, .view-ol),
18 | .view-list-item > :is(.view-list, .view-ul, .view-ol) {
19 | margin-top: 0;
20 | margin-bottom: 2px;
21 | }
22 |
23 | .view-inline-list,
24 | .view-comma-list {
25 | display: inline-block;
26 | }
27 |
28 | .view-list:empty::before,
29 | .view-inline-list:empty::before,
30 | .view-ul:empty::before,
31 | .view-ol:empty::before {
32 | content: attr(emptyText);
33 | color: #888;
34 | }
35 |
--------------------------------------------------------------------------------
/src/views/layout/lists.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './lists.usage.js';
3 |
4 | export default function(host) {
5 | function render(el, config, data, context) {
6 | const { item, itemConfig, limit, emptyText } = config;
7 |
8 | if (emptyText !== false && emptyText !== '') {
9 | el.setAttribute('emptyText', emptyText || 'Empty list');
10 | }
11 |
12 | if (!Array.isArray(data) && data) {
13 | data = [data];
14 | }
15 |
16 | if (Array.isArray(data)) {
17 | return host.view.renderList(el, this.composeConfig({
18 | view: 'list-item',
19 | content: item
20 | }, itemConfig), data, context, 0, host.view.listLimit(limit, 25));
21 | }
22 | }
23 |
24 | host.view.define('list', render, { usage });
25 | host.view.define('inline-list', render, { usage });
26 | host.view.define('comma-list', render, { usage });
27 | host.view.define('ol', render, { tag: 'ol', usage });
28 | host.view.define('ul', render, { tag: 'ul', usage });
29 | }
30 |
--------------------------------------------------------------------------------
/src/views/layout/lists.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: {
3 | view,
4 | data: ['one', 'two', 'three', 'four']
5 | },
6 | examples: [
7 | {
8 | title: 'Variations',
9 | demo: {
10 | view: 'context',
11 | data: ['foo', 'bar', 'baz'],
12 | content: group.map(view => [
13 | `header{ content: 'md:${JSON.stringify('`view: \\"' + view + '\\"`')}' }`,
14 | view
15 | ])
16 | }
17 | },
18 | {
19 | title: 'Configure item\'s content',
20 | demo: [
21 | {
22 | view,
23 | data: ['one', 'two', 'three', 'four'],
24 | item: [
25 | 'text:"
- "',
26 | {
27 | view: 'link',
28 | data: '{ href: "#" + $ }'
29 | }
30 | ]
31 | }
32 | ]
33 | },
34 | {
35 | title: 'Configure item\'s config',
36 | demo: {
37 | view,
38 | data: ['one', 'two', 'three', 'four'],
39 | itemConfig: {
40 | className: 'special'
41 | },
42 | item: {
43 | view: 'text',
44 | data: '"prefix-" + $'
45 | }
46 | }
47 | }
48 | ]
49 | });
50 |
--------------------------------------------------------------------------------
/src/views/layout/page-header.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { createElement } from '../../core/utils/dom.js';
3 | import usage from './page-header.usage.js';
4 |
5 | export default function(host) {
6 | host.view.define('page-header', function render(el, config, data, context) {
7 | const { prelude, content, onInit, onChange } = config;
8 | const preludeEl = el.appendChild(createElement('div', 'view-page-header__prelude'));
9 | const contentEl = el.appendChild(createElement('div', 'view-page-header__content'));
10 | const mixinHandlers = (config) =>
11 | typeof onInit !== 'function' && typeof onChange !== 'function'
12 | ? config // left as is since nothing to mix in
13 | : this.composeConfig(config, {
14 | onInit,
15 | onChange
16 | });
17 |
18 | return Promise.all([
19 | host.view.render(preludeEl, mixinHandlers(prelude || []), data, context),
20 | host.view.render(contentEl, mixinHandlers(content || 'text'), data, context)
21 | ]);
22 | }, { usage });
23 | }
24 |
--------------------------------------------------------------------------------
/src/views/layout/page-header.usage.js:
--------------------------------------------------------------------------------
1 | export default (view) => ({
2 | beforeDemo: [
3 | 'md:"A special view to be used as the first view in the body of the page. This view stays in place as the page scrolls (although it may move slightly to the top of the page at the start of the scroll), so that the most relevant information and important action elements can remain accessible despite page scrolling."'
4 | ],
5 | demoFixed: 100,
6 | demo: `${view}:"That's a \\"${view}\\""`,
7 | examples: [
8 | {
9 | title: 'Prelude',
10 | demoFixed: 150,
11 | highlightProps: ['prelude'],
12 | demo: {
13 | view,
14 | prelude: [
15 | 'badge:{ text: "demo" }',
16 | 'badge:{ text: "demo", prefix: "prelude", postfix: "postfix" }'
17 | ],
18 | content: 'h1:"Header"'
19 | }
20 | }
21 | ]
22 | });
23 |
--------------------------------------------------------------------------------
/src/views/layout/popup.css:
--------------------------------------------------------------------------------
1 | .discovery-view-popup {
2 | position: fixed;
3 | min-width: 200px;
4 | box-sizing: border-box;
5 | z-index: 300;
6 | overflow: hidden;
7 | overflow-y: auto;
8 | min-height: 20px;
9 | border: 1px solid rgba(176, 176, 176, 0.65);
10 | box-shadow: 3px 3px 18px rgba(0, 0, 0, .2);
11 | background-color: var(--discovery-background-color);
12 | transition-property: background-color;
13 | transition-duration: .25s;
14 | transition-timing-function: ease-in;
15 | }
16 | .discovery-view-popup.inspect {
17 | z-index: 2002;
18 | }
19 |
20 | .discovery-view-popup.show-on-hover:not([data-pin-mode="popup-hover"]):not(.pinned) {
21 | pointer-events: none;
22 | }
23 |
24 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"]::before,
25 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"]::after {
26 | content: "Click to pin this popup";
27 | display: block;
28 | visibility: hidden;
29 | left: 0;
30 | right: 0;
31 | z-index: 100;
32 | padding: 2px 8px;
33 | background-color: var(--discovery-background-color);
34 | background-image: linear-gradient(to bottom, rgba(0, 0, 0, .05), rgba(0, 0, 0, .05));
35 | color: #888;
36 | font-size: 10px;
37 | text-align: center;
38 | }
39 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"][data-v-to="top"]::before {
40 | position: absolute;
41 | visibility: visible;
42 | bottom: 0;
43 | }
44 | .discovery-view-popup:not(.pinned)[data-pin-mode="trigger-click"][data-v-to="bottom"]::after {
45 | position: absolute;
46 | visibility: visible;
47 | top: 0;
48 | }
49 |
--------------------------------------------------------------------------------
/src/views/layout/section.css:
--------------------------------------------------------------------------------
1 | .view-section {
2 | margin-bottom: 30px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/layout/section.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './section.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('section', function(el, config, data, context) {
6 | const { header, content = [], onInit, onChange } = config;
7 | const mixinHandlersIfNeeded = (config) =>
8 | typeof onInit !== 'function' && typeof onChange !== 'function'
9 | ? config // left as is since nothing to mix in
10 | : this.composeConfig(config, {
11 | onInit,
12 | onChange
13 | });
14 |
15 | return host.view.render(el, [
16 | { view: 'header', content: mixinHandlersIfNeeded(header) },
17 | mixinHandlersIfNeeded(content)
18 | ], data, context);
19 | }, { usage });
20 | }
21 |
--------------------------------------------------------------------------------
/src/views/layout/section.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'section',
4 | header: 'text:"I am section"',
5 | content: [
6 | 'text:"content"'
7 | ]
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/views/layout/toc-section.css:
--------------------------------------------------------------------------------
1 | .view-toc-section > .header {
2 | top: 0px;
3 | position: sticky;
4 | z-index: 1;
5 | padding: 9px 12px;
6 | line-height: 1;
7 | font-size: 14px;
8 | border: 1px solid rgba(85, 85, 85, 0.2);
9 | border-width: 1px 0;
10 | background-color: rgba(250, 250, 250, .92);
11 | text-transform: capitalize;
12 | margin: 0;
13 | margin-bottom: -1px;
14 | transition: background-color .25s ease-in;
15 | }
16 | .discovery-root-darkmode .view-toc-section > .header {
17 | background-color: rgba(50, 50, 50, .92);
18 | border-color: rgba(0, 0, 0, .25);
19 | }
20 | @supports (backdrop-filter: blur(5px)) or (-webkit-backdrop-filter: blur(5px)) {
21 | .view-toc-section > .header {
22 | background-color: rgba(250, 250, 250, .8);
23 | -webkit-backdrop-filter: blur(5px);
24 | backdrop-filter: blur(5px);
25 | }
26 | .discovery-root-darkmode .view-toc-section > .header {
27 | background-color: rgba(50, 50, 50, .8);
28 | border-color: rgba(0, 0, 0, .25);
29 | }
30 | }
31 |
32 | .view-toc-section > .header .view-badge,
33 | .view-toc-section > .header .view-pill-badge {
34 | text-transform: none;
35 | font-weight: normal;
36 | margin-left: 4px;
37 | margin-right: 0;
38 | }
39 | .view-toc-section > .content {
40 | padding: 8px 0 8px 12px;
41 | white-space: nowrap;
42 | }
43 |
--------------------------------------------------------------------------------
/src/views/layout/toc-section.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default function(host) {
4 | host.view.define('toc-section', function(el, config, data, context) {
5 | const { header, content } = config;
6 |
7 | return host.view.render(el, [
8 | {
9 | view: 'block',
10 | className: 'header',
11 | content: header
12 | },
13 | {
14 | view: 'block',
15 | className: 'content',
16 | content
17 | }
18 | ], data, context);
19 | }, {
20 | tag: 'section'
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/src/views/signature/const.js:
--------------------------------------------------------------------------------
1 | export const colors = [
2 | 'rgba(83,211,75,0.75)',
3 | 'rgba(241,235,44,0.75)',
4 | 'rgba(244,152,99,0.75)',
5 | 'rgba(148,99,244,0.75)',
6 | 'rgba(44,132,241,0.75)',
7 | 'rgba(233,117,117,0.75)',
8 | 'rgba(85,187,155,0.75)',
9 | 'rgba(151,147,99,0.75)',
10 | 'rgba(216,107,196,0.75)',
11 | 'rgba(108,204,227,0.75)',
12 | 'rgba(164,164,164,0.75)'
13 | ];
14 |
15 | export const typeOrder = [
16 | 'null',
17 | 'undefined',
18 | 'string',
19 | 'number',
20 | 'bigint',
21 | 'boolean',
22 | 'symbol',
23 | 'function',
24 | 'array',
25 | 'set',
26 | 'object'
27 | ];
28 |
--------------------------------------------------------------------------------
/src/views/signature/signature.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: ['md:"The `signature` view is used to output a type definition for current data using TypeScript like style:"'],
3 | demo: {
4 | view: 'signature',
5 | expanded: true
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/views/signature/style/action-button.css:
--------------------------------------------------------------------------------
1 | .view-signature [data-action] {
2 | display: inline-block;
3 | vertical-align: middle;
4 | margin-left: calc(2em / 9); /* 2px */
5 | padding: min(calc(1em / 3), 5px) min(calc(3px + 1em / 3), 8px);
6 | margin-top: max(1px - calc(1em / 3), -4px);
7 | color: rgba(170, 170, 170, 0.4);
8 | background-color: rgba(195, 195, 195, 0.05);
9 | border: .85px solid rgba(141, 141, 141, 0.15);
10 | border-radius: 3px;
11 | line-height: 1;
12 | font-size: .75em; /* 9px */
13 | cursor: pointer;
14 | }
15 | .discovery-root-darkmode .view-signature [data-action] {
16 | --discovery-view-signature-hover-color: #aaa;
17 | --discovery-view-signature-toggle-color: #72b372;
18 | }
19 | .view-signature [data-action][data-enabled=true],
20 | .view-signature [data-action][data-enabled=true]:hover {
21 | color: var(--discovery-view-signature-toggle-color, #333);
22 | background: rgba(151, 223, 151, 0.25);
23 | border-color: rgba(137, 177, 137, 0.4);
24 | }
25 | .view-signature [data-action="collapse"]::before {
26 | content: "–";
27 | }
28 | .view-signature [data-action="dict-mode"]::before {
29 | content: "dict";
30 | }
31 | .view-signature [data-action="sort-keys"]::before {
32 | content: "keys ↓";
33 | }
34 | .view-signature [data-action]:hover {
35 | color: var(--discovery-view-signature-hover-color, #333);
36 | background: rgba(187, 187, 187, 0.25);
37 | border-color: rgba(137, 137, 137, 0.4);
38 | }
39 |
--------------------------------------------------------------------------------
/src/views/signature/style/img/group.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/views/struct/popup-signature.js:
--------------------------------------------------------------------------------
1 | function findRootData(context) {
2 | while (context.parent !== null) {
3 | context = context.parent;
4 | }
5 |
6 | return context.host[''];
7 | }
8 |
9 | export function createSignaturePopup(host, elementData, elementContext, elementOptions, buildPathForElement) {
10 | return new host.view.Popup({
11 | className: 'view-struct-signature-popup',
12 | hoverPin: 'popup-hover',
13 | hoverTriggers: '.view-struct .show-signature',
14 | showDelay: 50,
15 | render(popupEl, triggerEl) {
16 | const el = triggerEl.parentNode;
17 | const data = elementData.get(el);
18 | const context = elementContext.get(el);
19 | const options = elementOptions.get(el) || {};
20 |
21 | popupEl.classList.add('computing');
22 | setTimeout(() => {
23 | host.view.render(popupEl, {
24 | view: 'signature',
25 | expanded: 2,
26 | rootData: findRootData(context),
27 | path: buildPathForElement(el)
28 | }, data, options.context).then(() => popupEl.classList.remove('computing'));
29 | }, 16);
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/views/switch.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './switch.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('switch', function(el, config, data, context) {
6 | let { content } = config;
7 | let renderConfig = 'alert-warning:"No case choosen"';
8 |
9 | if (Array.isArray(content)) {
10 | for (let i = 0; i < content.length; i++) {
11 | const branch = content[i];
12 |
13 | if (branch && host.queryBool(branch.when || true, data, context)) {
14 | renderConfig = 'data' in branch
15 | ? {
16 | view: 'context',
17 | data: branch.data,
18 | content: branch.content
19 | }
20 | : branch.content;
21 | break;
22 | }
23 | }
24 | }
25 |
26 | return host.view.render(el, renderConfig, data, context);
27 | }, {
28 | tag: false,
29 | usage
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/views/switch.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: [
3 | 'md:"A non-visual view that conditionally renders a single matching section from the provided content. It sequentially evaluates each item\'s `when` expression and renders the content of the first truthy condition. If an item has no `when` property, its condition defaults to truthy, enabling unconditional rendering, often used as a fallback or default content."'
4 | ],
5 | demo: {
6 | view: 'switch',
7 | content: [
8 | {
9 | when: 'expr',
10 | content: 'text:"Renders when `expr` is truthy"'
11 | },
12 | {
13 | content: 'text:"Renders when all other `when` conditions are falsy"'
14 | }
15 | ]
16 | },
17 | examples: [
18 | {
19 | title: 'Using with context and a modifier',
20 | demo: {
21 | view: 'context',
22 | modifiers: {
23 | view: 'tabs',
24 | tabs: ['foo', 'bar', 'baz'],
25 | name: 'section'
26 | },
27 | content: {
28 | view: 'switch',
29 | content: [
30 | { when: '#.section="foo"', content: 'text:"Content for `foo`"' },
31 | { when: '#.section="bar"', content: 'text:"Content for `bar`"' },
32 | { content: 'text:"No content is found for `" + #.section + "`"' }
33 | ]
34 | }
35 | }
36 | }
37 | ]
38 | };
39 |
--------------------------------------------------------------------------------
/src/views/table/table-cell-details-expand.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/table/table-cell.js:
--------------------------------------------------------------------------------
1 | import { applyAlign, createClickHandler, defaultCellRender } from './table-cell-utils.js';
2 |
3 | export default function(host) {
4 | host.view.define('table-cell', function(el, config, data, context) {
5 | let { content, contentWhen = true, details, detailsWhen = true, colSpan, align } = config;
6 | const isDataObject =
7 | !content &&
8 | data !== null &&
9 | (Array.isArray(data) ? data.length > 0 : typeof data === 'object') &&
10 | data instanceof RegExp === false;
11 |
12 | if (typeof colSpan === 'number' && colSpan > 1) {
13 | el.colSpan = colSpan;
14 | }
15 |
16 | if (!host.queryBool(contentWhen, data, context)) {
17 | return;
18 | }
19 |
20 | applyAlign(el, align);
21 |
22 | if ((details || (details === undefined && isDataObject)) && host.queryBool(detailsWhen, data, context)) {
23 | el.classList.add('details');
24 | el.addEventListener('click', createClickHandler(host, el, details, data, context));
25 | }
26 |
27 | if (content) {
28 | return host.view.render(el, content, data, context);
29 | }
30 |
31 | defaultCellRender(el, data, isDataObject);
32 | }, {
33 | tag: 'td'
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/views/table/table-footer-cell.css:
--------------------------------------------------------------------------------
1 | .view-table-footer-cell {
2 | padding:
3 | var(--table-cell-padding-top)
4 | max(calc(var(--table-cell-padding-right) - 1px), 0px)
5 | var(--table-cell-padding-bottom)
6 | var(--table-cell-padding-left);
7 | border: solid var(--discovery-background-color);
8 | border-top-color: transparent;
9 | border-width: 1px 1px 1px 0;
10 | color: #888;
11 | background-color: color-mix(in srgb, var(--discovery-background-color), #8d8d8d 8%);
12 | background-clip: padding-box;
13 | }
14 | .view-table-footer-cell:last-child {
15 | border-right: 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/table/table-footer-cell.js:
--------------------------------------------------------------------------------
1 | import { applyAlign, createClickHandler, defaultCellRender } from './table-cell-utils.js';
2 |
3 | export default function(host) {
4 | host.view.define('table-footer-cell', function(el, config, data, context) {
5 | let { content, contentWhen = true, details, detailsWhen = true, colSpan, align } = config;
6 | const isDataObject =
7 | !content &&
8 | data !== null &&
9 | (Array.isArray(data) ? data.length > 0 : typeof data === 'object') &&
10 | data instanceof RegExp === false;
11 |
12 | el.classList.add('view-table-cell');
13 |
14 | if (typeof colSpan === 'number' && colSpan > 1) {
15 | el.colSpan = colSpan;
16 | }
17 |
18 | if (!host.queryBool(contentWhen, data, context)) {
19 | return;
20 | }
21 |
22 | applyAlign(el, align);
23 |
24 | if ((details || (details === undefined && isDataObject)) && host.queryBool(detailsWhen, data, context)) {
25 | el.classList.add('details');
26 | el.addEventListener('click', createClickHandler(host, el, details, data, context));
27 | }
28 |
29 | if (content) {
30 | return host.view.render(el, content, data, context);
31 | }
32 |
33 | defaultCellRender(el, data, isDataObject);
34 | }, {
35 | tag: 'td'
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/src/views/table/table-footer.css:
--------------------------------------------------------------------------------
1 | .view-table-footer.no-cells-with-content {
2 | display: none;
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/table/table-header-cell.css:
--------------------------------------------------------------------------------
1 | .view-table-header-cell {
2 | padding:
3 | var(--table-cell-padding-top)
4 | var(--table-cell-padding-right)
5 | var(--table-cell-padding-bottom)
6 | var(--table-cell-padding-left);
7 | border: solid var(--discovery-background-color);
8 | border-width: 1px 1px 1px 0;
9 | font-weight: normal;
10 | text-align: left;
11 | background: #8d8d8d26 no-repeat right 1px center;
12 | background-color: color-mix(in srgb, var(--discovery-background-color), #8d8d8d 15%);
13 | background-size: 16px;
14 | background-clip: padding-box;
15 | }
16 | .view-table-header-cell:last-child {
17 | border-right: none;
18 | }
19 | .view-table-header-cell.sortable {
20 | padding-right: 18px;
21 | background-image: url('./table-sortable.svg');
22 | cursor: pointer;
23 | }
24 | .view-table-header-cell.sortable:not(.asc):not(.desc) {
25 | background-size: 14px;
26 | background-position: right 2px center;
27 | }
28 | .view-table-header-cell.sortable.asc {
29 | background-image: url('./table-sort-asc.svg');
30 | }
31 | .view-table-header-cell.sortable.desc {
32 | background-image: url('./table-sort-desc.svg');
33 | }
34 | .view-table-header-cell.sortable:hover {
35 | background-color: rgba(141, 141, 141, 0.3);
36 | background-color: color-mix(in srgb, var(--discovery-background-color), rgba(141, 141, 141) 30%);
37 | }
38 |
--------------------------------------------------------------------------------
/src/views/table/table-header-cell.js:
--------------------------------------------------------------------------------
1 | export default function(host) {
2 | host.view.define('table-header-cell', function(el, config, data, context) {
3 | let { content, text, initSorting, nextSorting } = config;
4 |
5 | if (typeof nextSorting === 'function') {
6 | el.classList.add('sortable');
7 | el.addEventListener('click', () => nextSorting(
8 | el.classList.contains('asc') ? 'asc'
9 | : el.classList.contains('desc') ? 'desc'
10 | : 'none'
11 | ));
12 | }
13 |
14 | if (initSorting) {
15 | el.classList.add(initSorting > 0 ? 'asc' : 'desc');
16 | }
17 |
18 | if (content) {
19 | return host.view.render(el, content, data, context);
20 | } else {
21 | el.textContent = text ?? '';
22 | }
23 | }, {
24 | tag: 'th'
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/src/views/table/table-header.css:
--------------------------------------------------------------------------------
1 | .view-table-header {
2 | position: sticky;
3 | top: 0px;
4 | z-index: 1;
5 | }
6 |
--------------------------------------------------------------------------------
/src/views/table/table-row.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/discoveryjs/discovery/9449adc57b933896b420d1b17f4332032f6d50c6/src/views/table/table-row.css
--------------------------------------------------------------------------------
/src/views/table/table-row.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default function(host) {
4 | host.view.define('table-row', function(el, config, data, context) {
5 | const { cols } = config;
6 |
7 | if (Array.isArray(cols)) {
8 | return Promise.all(cols.map((col, index) =>
9 | host.view.render(el, col, data, { ...context, colIndex: index })
10 | ));
11 | }
12 | }, { tag: 'tr' });
13 | }
14 |
--------------------------------------------------------------------------------
/src/views/table/table-sort-asc.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/table/table-sort-desc.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/table/table-sortable.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/table/table.css:
--------------------------------------------------------------------------------
1 | @import url('./table-cell.css');
2 | @import url('./table-header.css');
3 | @import url('./table-header-cell.css');
4 | @import url('./table-footer.css');
5 | @import url('./table-footer-cell.css');
6 | @import url('./table-row.css');
7 |
8 | .view-table {
9 | --table-cell-padding-top: 2px;
10 | --table-cell-padding-right: 8px;
11 | --table-cell-padding-bottom: 2px;
12 | --table-cell-padding-left: 8px;
13 |
14 | font-size: 12px;
15 | line-height: 19px;
16 | /* border-collapse: collapse; */
17 | border-spacing: 0;
18 | }
19 |
20 | .view-table > tbody > .view-table-row:not(:last-child) > td,
21 | .view-table > tbody:not(:last-child) > .view-table-row:last-child > td,
22 | .view-table > .view-table-more-buttons:not(:last-child) > tr > td:not(:empty) {
23 | border-bottom: 1px solid rgba(170, 170, 170, 0.2);
24 | }
25 | .view-table > tbody.view-table-more-buttons > tr > td > .more-buttons {
26 | margin-bottom: 2px;
27 | }
28 | .view-table > tbody.view-table-more-buttons > tr > td:empty {
29 | padding: 0;
30 | }
31 |
32 | .view-table-cell-details-row > .view-cell-details-content > .view-table:first-child {
33 | margin-top: -1px !important;
34 | }
35 |
--------------------------------------------------------------------------------
/src/views/text-render.css:
--------------------------------------------------------------------------------
1 | .view-text-render {
2 | font-family: var(--discovery-monospace-font-family);
3 | font-size: var(--discovery-monospace-font-size, 12px);
4 | line-height: var(--discovery-monospace-line-height, 1.5);
5 | }
6 |
--------------------------------------------------------------------------------
/src/views/text-render.js:
--------------------------------------------------------------------------------
1 | import usage from './text-render.usage.js';
2 |
3 | export default function(host) {
4 | host.view.define('text-render', async function(el, config, data, context) {
5 | const { content } = config;
6 | const textRenderTree = await host.textView.render('block', content, data, context);
7 |
8 | el.append(host.textView.serialize(textRenderTree).text);
9 | }, { tag: 'pre', usage });
10 |
11 | host.view.define('text-render-tree', async function(el, config, data, context) {
12 | const { content, expanded = 2 } = config;
13 | const textRenderTree = await host.textView.render('block', content, data, context);
14 |
15 | return this.render(el, { view: 'struct', expanded }, host.textView.cleanUpRenderTree(textRenderTree));
16 | }, { tag: false });
17 | };
18 |
--------------------------------------------------------------------------------
/src/views/text-render.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | beforeDemo: [
3 | 'md:"The `text-render` view renders data as text using the `Model#textView` API. For a full list of available text views, refer to the [`text`](#views-showcase&render=text) section."'
4 | ],
5 | demo: {
6 | view: 'text-render',
7 | content: [
8 | 'text:"hello!"',
9 | {
10 | view: 'ul',
11 | data: [1, 2, 3, 4],
12 | item: 'text:"item #" + $'
13 | },
14 | {
15 | view: 'table',
16 | data: [
17 | { name: 'foo', size: 1000 },
18 | { name: 'bar', size: 123 },
19 | { name: 'baz', size: 34555 }
20 | ]
21 | }
22 | ]
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/views/text/alerts.css:
--------------------------------------------------------------------------------
1 | .view-alert {
2 | position: relative;
3 | padding: .75rem 1.25rem;
4 | margin-bottom: 1rem;
5 | border: 1px solid transparent;
6 | border-radius: .25rem;
7 |
8 | color: #383d41;
9 | background-color: rgba(139, 143, 151, 0.25);
10 | border-color: rgba(152, 157, 165, 0.4);
11 | background-clip: padding-box;
12 | }
13 | .discovery-root-darkmode .view-alert {
14 | color: var(--discovery-color);
15 | }
16 | .view-alert:last-child {
17 | margin-bottom: 0;
18 | }
19 |
20 | .view-alert > :first-child {
21 | margin-top: 0;
22 | }
23 |
24 | .page > .view-alert:first-child,
25 | .page > .view-alert-success:first-child,
26 | .page > .view-alert-danger:first-child,
27 | .page > .view-alert-warning:first-child {
28 | margin-top: 15px;
29 | }
30 |
31 | .view-alert-primary {
32 | color: #004085;
33 | background-color: rgba(51, 151, 255, 0.25);
34 | border-color: rgba(77, 162, 255, 0.4);
35 | }
36 | .discovery-root-darkmode .view-alert-primary {
37 | color: #6a96c6;
38 | }
39 |
40 | .view-alert-success {
41 | color: #155724;
42 | background-color: rgba(83, 183, 107, 0.25);
43 | border-color: rgba(105, 192, 125, 0.4);
44 | }
45 | .discovery-root-darkmode .view-alert-success {
46 | color: #5fab70;
47 | }
48 |
49 | .view-alert-danger {
50 | color: #721c24;
51 | background-color: rgba(227, 95, 107, 0.25);
52 | border-color: rgba(230, 112, 125, 0.4);
53 | }
54 | .discovery-root-darkmode .view-alert-danger {
55 | color: #c7888e;
56 | }
57 |
58 | .view-alert-warning {
59 | color: #856404;
60 | background-color: rgba(255, 207, 55, 0.25);
61 | border-color: rgba(255, 212, 82, 0.4);
62 | }
63 | .discovery-root-darkmode .view-alert-warning {
64 | color: #bdab77;
65 | }
66 |
--------------------------------------------------------------------------------
/src/views/text/alerts.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import usage from './alerts.usage.js';
4 |
5 | export default function(host) {
6 | function render(el, config, data, context) {
7 | const { content = 'text' } = config;
8 |
9 | el.classList.add('view-alert');
10 |
11 | return host.view.render(el, content, data, context);
12 | }
13 |
14 | host.view.define('alert', render, { usage });
15 | host.view.define('alert-primary', render, { usage });
16 | host.view.define('alert-success', render, { usage });
17 | host.view.define('alert-danger', render, { usage });
18 | host.view.define('alert-warning', render, { usage });
19 | }
20 |
--------------------------------------------------------------------------------
/src/views/text/alerts.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: {
3 | view,
4 | data: '"Alert"'
5 | },
6 | examples: [
7 | {
8 | title: 'Variations',
9 | demo: group.map(name => `${name}:"${name}"`)
10 | },
11 | {
12 | title: 'Complex content',
13 | demo: {
14 | view,
15 | content: [
16 | 'h3:"Some header"',
17 | 'text:"Hello world!"'
18 | ]
19 | }
20 | }
21 | ]
22 | });
23 |
--------------------------------------------------------------------------------
/src/views/text/app-header.css:
--------------------------------------------------------------------------------
1 | .view-app-header {
2 | display: flex;
3 | align-items: center;
4 | gap: 10px;
5 | }
6 | .view-app-header:not(:last-child) {
7 | margin-bottom: 30px;
8 | }
9 | .view-app-header .icon {
10 | --icon: var(--discovery-app-icon);
11 |
12 | float: left;
13 | display: inline-block;
14 | line-height: 1;
15 |
16 | width: 48px;
17 | aspect-ratio: 1;
18 | vertical-align: middle;
19 | padding: 4px;
20 | background: no-repeat center;
21 | background-image: var(--icon, var(--discovery-app-icon));
22 | background-size: 48px;
23 | background-color: #f4f4f4;
24 | border-radius: 3px;
25 | }
26 | .discovery-root-darkmode .view-app-header .icon {
27 | background-color: #2b2b2b;
28 | }
29 | .view-app-header h1 {
30 | margin: 0;
31 | font-size: 30px;
32 | line-height: 31px;
33 | font-family: Helvetica Neue, Helvetica, Tahoma, Arial, sans-serif;
34 | font-weight: 200;
35 | }
36 | .view-app-header .version {
37 | vertical-align: top;
38 | display: inline-block;
39 | margin-left: 1ex;
40 | font-size: 12px;
41 | line-height: 18px;
42 | opacity: .4;
43 | }
44 | .view-app-header .version:hover {
45 | opacity: .75;
46 | }
47 | .view-app-header .description {
48 | font-size: 12px;
49 | line-height: 19px;
50 | color: #aaa;
51 | }
52 |
--------------------------------------------------------------------------------
/src/views/text/app-header.js:
--------------------------------------------------------------------------------
1 | import { createElement } from '../../core/utils/index.js';
2 | import usage from './app-header.usage.js';
3 |
4 | const props = `is not array? | {
5 | name: name or "Untitled app",
6 | icon,
7 | version,
8 | description
9 | } | overrideProps() | {
10 | ...,
11 | icon is string and $ ~= /\\S/?,
12 | version is string and $ ~= /\\S/?,
13 | description is string and $ ~= /\\S/?
14 | }`;
15 |
16 | export default function(host) {
17 | host.view.define('app-header', function(el, props) {
18 | const { name, icon, version, description } = props;
19 | const headerEl = createElement('h1', null, version
20 | ? [name, createElement('span', 'version', version)]
21 | : [name]
22 | );
23 |
24 | if (icon) {
25 | el.style.setProperty('--icon', /^(?:\.|\/|data:|https?:)/.test(icon)
26 | ? `url(${JSON.stringify(icon)})`
27 | : icon
28 | );
29 | }
30 |
31 | el.append(
32 | createElement('div', 'icon'),
33 | createElement('div', 'content', description
34 | ? [headerEl, createElement('div', 'description', description)]
35 | : [headerEl]
36 | )
37 | );
38 | }, { props, usage });
39 | }
40 |
--------------------------------------------------------------------------------
/src/views/text/app-header.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'app-header',
4 | name: 'My app',
5 | version: '1.2.3',
6 | description: 'The best of the best app'
7 | },
8 | examples: [
9 | {
10 | title: 'Variations',
11 | demo: [
12 | 'app-header',
13 | 'app-header{ name: "Only name" }',
14 | 'app-header{ name: "Name and version", version: "1.2.3" }',
15 | 'app-header{ name: "Name and description", description: "Some description" }',
16 | 'app-header{ name: "Everything set", version: "1.2.3", description: "Very useful description" }'
17 | ]
18 | }
19 | ]
20 | };
21 |
--------------------------------------------------------------------------------
/src/views/text/auto-link.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | export default function(host) {
4 | host.view.define('auto-link', function(el, config, data, context) {
5 | const { content, fallback, href } = config;
6 |
7 | if (!data) {
8 | return;
9 | }
10 |
11 | const links = host.resolveValueLinks(data);
12 | const preprocessHref = typeof href === 'function' ? href : value => value;
13 | const processedHref = links
14 | ? preprocessHref(links[0].href, data, context)
15 | : null;
16 |
17 | if (processedHref) {
18 | return host.view.render(el, { view: 'link', content }, {
19 | ...links[0],
20 | href: processedHref
21 | }, context);
22 | } else {
23 | return host.view.render(el, fallback || content || 'text', data, context);
24 | }
25 | }, {
26 | tag: false
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/views/text/badges.css:
--------------------------------------------------------------------------------
1 | .view-badge,
2 | .view-pill-badge {
3 | --discovery-view-badge-color: rgba(135, 135, 135, 0.2);
4 | --discovery-view-badge-text-color: var(--discovery-color);
5 | padding: 1px 6px 3px;
6 | margin-right: 1ex;
7 | text-shadow: 1px 1px rgba(255, 255, 255, .35);
8 | background: var(--discovery-view-badge-color);
9 | color: var(--discovery-view-badge-text-color);
10 | border-radius: 3px;
11 | font-size: 85%;
12 | text-decoration: none;
13 | white-space: nowrap;
14 | }
15 | .view-pill-badge {
16 | border-radius: 9px;
17 | }
18 | .discovery-root-darkmode .view-badge,
19 | .discovery-root-darkmode .view-pill-badge {
20 | background: var(--discovery-view-badge-dark-color, var(--discovery-view-badge-color));
21 | color: var(--discovery-view-badge-dark-text-color, var(--discovery-view-badge-text-color));
22 | text-shadow: 1px 1px rgba(0, 0, 0, .1);
23 | }
24 |
25 | .view-badge[href]:hover,
26 | .view-badge.onclick:hover,
27 | .view-pill-badge[href]:hover,
28 | .view-pill-badge.onclick:hover {
29 | color: var(--discovery-color);
30 | background-image: linear-gradient(to top, rgba(0, 0, 0, .1), rgba(0, 0, 0, .1));
31 | cursor: pointer;
32 | }
33 |
34 | .view-badge > .prefix {
35 | padding: 1px 6px 3px;
36 | margin: -1px 6px -3px -6px;
37 | border-radius: 3px 0 0 3px;
38 | background-color: rgba(0, 0, 0, .12);
39 | }
40 | .view-badge > .postfix {
41 | padding: 1px 6px 3px;
42 | margin: -1px -6px -3px 6px;
43 | border-radius: 0 3px 3px 0;
44 | background-color: rgba(0, 0, 0, .12);
45 | }
46 | .view-pill-badge > .prefix {
47 | padding: 1px 6px 3px;
48 | margin: -1px 6px -3px -6px;
49 | border-radius: 8px 0 0 8px;
50 | background-color: rgba(0, 0, 0, .12);
51 | }
52 | .view-pill-badge > .postfix {
53 | padding: 1px 6px 3px;
54 | margin: -1px -6px -3px 6px;
55 | border-radius: 0 8px 8px 0;
56 | background-color: rgba(0, 0, 0, .12);
57 | }
58 |
59 | .discovery-buildin-view-tooltip .view-badge,
60 | .discovery-buildin-view-tooltip .view-pill-badge {
61 | display: inline-block;
62 | margin: 0 0 3px -5px;
63 | padding-top: 0;
64 | padding-bottom: 1px;
65 | line-height: 19px;
66 | }
67 |
--------------------------------------------------------------------------------
/src/views/text/blockquote-caution.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/blockquote-important.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/blockquote-note.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/blockquote-tip.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/blockquote-warning.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/blockquote.css:
--------------------------------------------------------------------------------
1 | .view-blockquote {
2 | margin: 1px 0;
3 | padding: 0;
4 | padding-left: 1em;
5 | border-left: .25em solid var(--color, rgba(191, 197, 197, 0.5));
6 | }
7 | .view-blockquote:not([data-kind]) {
8 | color: #6a737d;
9 | }
10 | .discovery-root-darkmode .view-blockquote:not([data-kind]) {
11 | color: #808993;
12 | }
13 |
14 | .view-blockquote[data-kind] {
15 | padding-top: .4em;
16 | padding-bottom: .4em;
17 | }
18 | .view-blockquote[data-kind]::before {
19 | content: var(--blockquote-title-text, attr(data-kind));
20 | display: block;
21 | padding-left: calc(1em + .9ex);
22 | margin-bottom: .2em;
23 | background-image: var(--blockquote-title-icon);
24 | background-size: 1.1em;
25 | background-repeat: no-repeat;
26 | background-position: left center;
27 | color: var(--blockquote-title-color);
28 | text-transform: capitalize;
29 | }
30 |
31 | .view-blockquote[data-kind="note"] {
32 | --color: #1f6feb;
33 | --blockquote-title-icon: url("./blockquote-note.svg");
34 | --blockquote-title-color: #4493f8;
35 | }
36 | .view-blockquote[data-kind="tip"] {
37 | --color: #238636;
38 | --blockquote-title-icon: url("./blockquote-tip.svg");
39 | --blockquote-title-color: #3fb950;
40 | }
41 | .view-blockquote[data-kind="important"] {
42 | --color: #8957e5;
43 | --blockquote-title-icon: url("./blockquote-important.svg");
44 | --blockquote-title-color: #ab7df8;
45 | }
46 | .view-blockquote[data-kind="warning"] {
47 | --color: #9e6a03;
48 | --blockquote-title-icon: url("./blockquote-warning.svg");
49 | --blockquote-title-color: #d29922;
50 | }
51 | .view-blockquote[data-kind="caution"] {
52 | --color: #da3633;
53 | --blockquote-title-icon: url("./blockquote-caution.svg");
54 | --blockquote-title-color: #f85149;
55 | }
56 |
--------------------------------------------------------------------------------
/src/views/text/blockquote.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import usage from './blockquote.usage.js';
4 |
5 | export default function(host) {
6 | function render(el, config, data, context) {
7 | const { content = 'text', kind } = config;
8 |
9 | if (typeof kind === 'string' && /\S/.test(kind)) {
10 | el.dataset.kind = kind.trim();
11 | }
12 |
13 | return host.view.render(el, content, data, context);
14 | }
15 |
16 | host.view.define('blockquote', {
17 | tag: 'blockquote',
18 | render,
19 | usage
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/views/text/blockquote.usage.js:
--------------------------------------------------------------------------------
1 | export default (view) => ({
2 | demo: {
3 | view,
4 | content: [
5 | 'text:"Blockquote text..."',
6 | 'blockquote:"Nested blockquote\\nmulti-line text..."'
7 | ]
8 | },
9 | examples: [
10 | {
11 | title: 'Variations',
12 | demoData: 'Blockquote text...',
13 | demo: [
14 | { kind: '', text: 'Default blockquote without any kind.' },
15 | { kind: 'note', text: 'Useful information that users should know, even when skimming content.' },
16 | { kind: 'tip', text: 'Helpful advice for doing things better or more easily.' },
17 | { kind: 'important', text: 'Key information users need to know to achieve their goal.' },
18 | { kind: 'warning', text: 'Urgent info that needs immediate user attention to avoid problems.' },
19 | { kind: 'caution', text: 'Advises about risks or negative outcomes of certain actions.' }
20 | ]
21 | .map(({ kind, text }) => kind
22 | ? `${view}{ kind: "${kind}", data: ${JSON.stringify(text)} }`
23 | : `${view}:${JSON.stringify(text)}`
24 | )
25 | }
26 | ]
27 | });
28 |
--------------------------------------------------------------------------------
/src/views/text/headers-anchor.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/views/text/headers.css:
--------------------------------------------------------------------------------
1 | .view-header { /* the same as h4 */
2 | font-size: 120%;
3 | font-weight: normal;
4 | position: relative;
5 | }
6 |
7 | .view-header > .view-header__anchor {
8 | position: absolute;
9 | margin-left: -22px;
10 | display: inline-block;
11 | width: 22px;
12 | text-align: center;
13 | color: inherit;
14 | background: url(./headers-anchor.svg) center / 20px no-repeat content-box;
15 | padding-top: 2px;
16 | font-weight: normal;
17 | opacity: 0;
18 | transition: opacity .25s;
19 | }
20 | .view-header:hover > .view-header__anchor,
21 | .view-header:hover > .view-header__anchor {
22 | opacity: 1;
23 | }
24 | .view-header > .view-header__anchor::before {
25 | content: '§';
26 | display: inline-block;
27 | width: 0;
28 | visibility: hidden;
29 | }
30 |
31 | .view-h1 {
32 | font-size: 220%;
33 | font-weight: normal;
34 | margin: .812em 0 .65em;
35 | }
36 |
37 | .view-h2 {
38 | font-size: 150%;
39 | font-weight: normal;
40 | margin: .78em 0;
41 | }
42 |
43 | .view-h3 {
44 | font-size: 135%;
45 | font-weight: normal;
46 | margin: .73em 0;
47 | }
48 |
49 | .view-h4 {
50 | font-size: 120%;
51 | font-weight: normal;
52 | margin: .73em 0;
53 | }
54 |
55 | .view-h5 {
56 | font-size: 110%;
57 | font-weight: normal;
58 | margin: .68em 0;
59 | }
60 |
--------------------------------------------------------------------------------
/src/views/text/headers.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { slug as generateSlug } from 'github-slugger';
3 | import { createElement } from '../../core/utils/dom.js';
4 | import usage from './headers.usage.js';
5 |
6 | export default function(host) {
7 | async function render(el, config, data, context) {
8 | const { content, anchor = false } = config;
9 |
10 | el.classList.add('view-header');
11 |
12 | await host.view.render(el, content || 'text', data, context);
13 |
14 | if (anchor) {
15 | const slug = generateSlug(anchor === true ? el.textContent : String(anchor));
16 | const href = host.encodePageHash(
17 | host.pageId,
18 | host.pageRef,
19 | host.pageParams,
20 | slug
21 | );
22 |
23 | el.prepend(createElement('a', {
24 | class: 'view-header__anchor',
25 | id: `!anchor:${slug}`,
26 | href
27 | }));
28 | }
29 | }
30 |
31 | host.view.define('header', render, { tag: 'h4', usage });
32 | host.view.define('h1', render, { tag: 'h1', usage });
33 | host.view.define('h2', render, { tag: 'h2', usage });
34 | host.view.define('h3', render, { tag: 'h3', usage });
35 | host.view.define('h4', render, { tag: 'h4', usage });
36 | host.view.define('h5', render, { tag: 'h5', usage });
37 | }
38 |
--------------------------------------------------------------------------------
/src/views/text/headers.usage.js:
--------------------------------------------------------------------------------
1 | export default (view, group) => ({
2 | demo: `${view}:"Header \\"${view}\\""`,
3 | examples: [
4 | {
5 | title: 'Variations',
6 | view: group.map(view => `${view}:"Header \\"${view}\\""`)
7 | },
8 | {
9 | title: 'Complex content',
10 | demo: {
11 | view,
12 | content: [
13 | 'text:"Text "',
14 | 'link:{ text: "Link" }'
15 | ]
16 | }
17 | },
18 | {
19 | title: 'Using anchor',
20 | demo: [
21 | {
22 | view,
23 | anchor: 'foo',
24 | content: 'text:"Explicit value for an anchor"'
25 | },
26 | {
27 | view,
28 | anchor: true,
29 | content: 'text:"Auto generated anchor based on text content of header"'
30 | }
31 | ]
32 | }
33 | ]
34 | });
35 |
--------------------------------------------------------------------------------
/src/views/text/html.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './html.usage.js';
3 |
4 | export default function(host) {
5 | const buffer = document.createElement('div');
6 |
7 | host.view.define('html', function(el, config, data) {
8 | buffer.innerHTML = data;
9 | el.append(...buffer.childNodes);
10 | }, {
11 | tag: null,
12 | usage
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/views/text/html.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'html',
4 | data: '"
I am inner HTML
"'
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/views/text/image-preview.css:
--------------------------------------------------------------------------------
1 | .view-image-preview {
2 | --image-preview-bg-color: rgba(119, 119, 119, 0.125);
3 | display: flex;
4 | justify-content: center;
5 | padding: 16px;
6 | background-image:
7 | linear-gradient(45deg, var(--image-preview-bg-color) 25%, transparent 0, transparent 75%, var(--image-preview-bg-color) 75%),
8 | linear-gradient(45deg, var(--image-preview-bg-color) 25%, transparent 0, transparent 75%, var(--image-preview-bg-color) 75%);
9 | background-position: 0 0, 10px 10px;
10 | background-size: 20px 20px;
11 | }
12 |
13 | .view-image-preview .view-image {
14 | /* background-color: transparent; */
15 | opacity: 1;
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/text/image-preview.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 |
3 | import usage from './image-preview.usage.js';
4 |
5 | export default function(host) {
6 | host.view.define('image-preview', function(el, config, data, context) {
7 | this.render(el, {
8 | view: 'image',
9 | ...config
10 | }, data, context);
11 | }, { usage });
12 | }
13 |
--------------------------------------------------------------------------------
/src/views/text/image-preview.usage.js:
--------------------------------------------------------------------------------
1 | import { demoImageSrc } from './image.usage.js';
2 |
3 | export default {
4 | beforeDemo: ['md:"Similar to the `image` view but displays as a block with the image centered. The block has a checkered background to highlight image transparency."'],
5 | demo: {
6 | view: 'image-preview',
7 | src: demoImageSrc,
8 | height: 100
9 | },
10 | examples: [
11 | {
12 | title: 'Src is not defined',
13 | demo: 'image-preview'
14 | },
15 | {
16 | title: 'Bad url',
17 | demo: 'image-preview{ src: "" }'
18 | }
19 | ]
20 | };
21 |
--------------------------------------------------------------------------------
/src/views/text/image.css:
--------------------------------------------------------------------------------
1 | .view-image {
2 | background: no-repeat center / 20px;
3 | }
4 | .view-image:not(.error):not(.loaded) {
5 | background-color: rgba(141, 141, 141, .3);
6 | }
7 |
8 | .view-image.error,
9 | .view-image:not([src]) {
10 | width: 32px;
11 | height: 32px;
12 | background-image: url('./image.svg');
13 | background-color: rgba(141, 70, 70, .3);
14 | }
15 |
16 | .discovery-root-darkmode .view-image {
17 | opacity: .85;
18 | }
19 |
--------------------------------------------------------------------------------
/src/views/text/image.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './image.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('image', function(el, config) {
6 | Object.assign(el, config);
7 | el.onerror = () => el.classList.add('error');
8 | el.onload = () => el.classList.add('loaded');
9 | }, { tag: 'img', usage });
10 | }
11 |
--------------------------------------------------------------------------------
/src/views/text/image.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/views/text/indicator.css:
--------------------------------------------------------------------------------
1 | .view-indicator {
2 | display: inline-flex;
3 | margin: 0 1px 1px 0;
4 | width: 150px;
5 | height: 100px;
6 | background: rgba(181, 181, 181, 0.15);
7 | flex-direction: column;
8 | justify-content: center;
9 | text-align: center;
10 | text-decoration: none;
11 | }
12 | .view-indicator[href]:hover {
13 | background: rgba(165, 165, 165, 0.3);
14 | }
15 |
16 | .view-indicator > .value {
17 | color: #666;
18 | font-size: 40px;
19 | line-height: 1.2;
20 | }
21 | .view-indicator[href] > .value {
22 | color: #1f7ec5;
23 | }
24 | .view-indicator > .label {
25 | font-size: 14px;
26 | color: #888;
27 | box-sizing: border-box;
28 | padding: 0 8px;
29 | white-space: nowrap;
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | }
33 |
--------------------------------------------------------------------------------
/src/views/text/indicator.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './indicator.usage.js';
3 |
4 | export default function(host) {
5 | host.view.define('indicator', function(el, config, data, context) {
6 | const { value, label } = config;
7 | const { href } = data || {};
8 | const valueEl = document.createElement('div');
9 | const labelEl = document.createElement('div');
10 |
11 | valueEl.className = 'value';
12 | labelEl.className = 'label';
13 |
14 | if (href) {
15 | el.href = href;
16 | }
17 |
18 | return Promise.all([
19 | host.view.render(valueEl, value || 'text:value', data, context),
20 | host.view.render(labelEl, label || 'text:label', data, context)
21 | ]).then(() => el.append(valueEl, labelEl));
22 | }, {
23 | tag: 'a',
24 | usage
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/src/views/text/indicator.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'indicator',
4 | data: {
5 | label: 'Label',
6 | value: '1234'
7 | }
8 | },
9 | examples: [
10 | {
11 | title: 'Indicator as link',
12 | demo: {
13 | view: 'indicator',
14 | data: {
15 | label: 'Label',
16 | value: '4321',
17 | href: '#'
18 | }
19 | }
20 | }
21 | ]
22 | };
23 |
--------------------------------------------------------------------------------
/src/views/text/link.css:
--------------------------------------------------------------------------------
1 | .view-link {
2 | color: var(--discovery-link-color, #0099DD);
3 | text-decoration-skip: ink;
4 | text-decoration-color: var(--discovery-link-underline-color, rgba(0, 153, 221, 0.4));
5 | }
6 | .view-link.onclick {
7 | text-decoration-line: underline;
8 | cursor: pointer;
9 | }
10 | .view-link:hover {
11 | color: var(--discovery-link-hover-color, #0077BB);
12 | text-decoration-color: currentColor;
13 | }
14 |
--------------------------------------------------------------------------------
/src/views/text/link.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './link.usage.js';
3 |
4 | const props = `is not array? | {
5 | text: #.props.content is undefined ? is string ?: text,
6 | content: undefined,
7 | href,
8 | external,
9 | onClick: undefined
10 | } | overrideProps() | {
11 | $text; $href;
12 | ...,
13 | text: $text | is not undefined or no $href ?: $href,
14 | href: $href | is not undefined or no $text ?: $text
15 | }`;
16 |
17 | export default function(host) {
18 | host.view.define('link', function(el, props, data, context) {
19 | let {
20 | text,
21 | content,
22 | href,
23 | external,
24 | onClick
25 | } = props;
26 |
27 | if (href) {
28 | el.href = href;
29 | }
30 |
31 | if (external) {
32 | el.setAttribute('target', '_blank');
33 | }
34 |
35 | if (typeof onClick === 'function') {
36 | el.classList.add('onclick');
37 | el.addEventListener('click', (e) => {
38 | e.preventDefault();
39 | onClick(el, data, context);
40 | });
41 | }
42 |
43 | if (content) {
44 | return host.view.render(el, content, data, context);
45 | } else {
46 | el.textContent = text;
47 | }
48 | }, {
49 | tag: 'a',
50 | props,
51 | usage
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/src/views/text/markdown.css:
--------------------------------------------------------------------------------
1 | .view-markdown:first-child > :first-child {
2 | margin-top: 0;
3 | }
4 | .view-markdown:last-child > :last-child {
5 | margin-bottom: 0;
6 | }
7 | .view-markdown :is(p, blockquote, ul, ol, dl, pre, code) {
8 | margin: 0;
9 | }
10 | .view-markdown :is(p, blockquote, ul, ol, dl, table, pre):not(:first-child) {
11 | margin-top: 15px;
12 | }
13 | .view-markdown :is(ul, ol) + :is(ul, ol),
14 | .view-markdown li > :is(ul, ol) {
15 | margin-top: 0 !important;
16 | }
17 |
18 | .view-markdown code {
19 | padding: .2em .4em;
20 | font-family: var(--discovery-monospace-font-family);
21 | font-size: 90%;
22 | background-color: rgba(210, 220, 230, 0.2);
23 | border-radius: 3px;
24 | }
25 | .discovery-root-darkmode .view-markdown blockquote code {
26 | background-color: rgba(116, 126, 136, 0.2);
27 | }
28 |
29 | .view-markdown kbd {
30 | --border-color: #ddd;
31 | --bg-color: #f8f8f8;
32 | padding: .15em .4em;
33 | font-family: var(--discovery-monospace-font-family);
34 | font-size: 90%;
35 | border: 1px solid var(--border-color, #888);
36 | border-radius: 6px;
37 | box-shadow: 0 -1px var(--border-color, #888) inset;
38 | background-color: var(--bg-color);
39 | }
40 | .discovery-root-darkmode .view-markdown kbd {
41 | --border-color: #484848;
42 | --bg-color: #181818;
43 | }
44 |
45 | .view-markdown sup {
46 | vertical-align: top;
47 | }
48 | .view-markdown sub {
49 | vertical-align: bottom;
50 | }
51 |
52 | .view-markdown pre:not(.view-source) {
53 | overflow: auto;
54 | font-family: var(--discovery-monospace-font-family);
55 | font-size: 90%;
56 | line-height: 1.25;
57 | background-color: rgba(155, 155, 155, 0.1);
58 | border-radius: 3px;
59 | word-break: normal;
60 | }
61 | .view-markdown pre:not(.view-source) code {
62 | padding: 0;
63 | font-family: inherit;
64 | font-size: 100%;
65 | background: none;
66 | border-radius: 0;
67 | text-shadow: none;
68 | color: inherit;
69 | }
70 |
71 | .view-markdown .check-list-item {
72 | list-style: none;
73 | }
74 | .view-markdown .check-list-item > .view-checkbox:first-child {
75 | width: 20px;
76 | margin-left: -20px;
77 | }
78 |
--------------------------------------------------------------------------------
/src/views/text/source-copied.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/views/text/source-copy.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/views/text/text-match.css:
--------------------------------------------------------------------------------
1 | .view-text-match {
2 | background: rgba(255, 232, 5, 0.22);
3 | border-bottom: 2px solid rgba(213, 190, 15, 0.8);
4 | line-height: 1.2;
5 | }
6 |
--------------------------------------------------------------------------------
/src/views/text/text-match.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import { createElement } from '../../core/utils/dom.js';
3 | import { matchAll } from '../../core/utils/pattern.js';
4 | import usage from './text-match.usage.js';
5 |
6 | const matchWrapperEl = createElement('span', 'view-text-match');
7 | const props = `is not array? | {
8 | text: #.props has no 'text' ? text,
9 | match,
10 | ignoreCase: ignoreCase or false
11 | } | overrideProps()`;
12 |
13 | export default function(host) {
14 | host.view.define('text-match', function(el, props) {
15 | const {
16 | text,
17 | match: pattern,
18 | ignoreCase
19 | } = props;
20 |
21 | matchAll(
22 | String(text),
23 | pattern,
24 | text => el
25 | .append(text),
26 | text => el
27 | .appendChild(matchWrapperEl.cloneNode())
28 | .append(text),
29 | ignoreCase
30 | );
31 | }, {
32 | tag: false,
33 | props,
34 | usage
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/views/text/text-numeric.css:
--------------------------------------------------------------------------------
1 | .view-text-numeric .num-delim {
2 | padding-left: 0.14em;
3 | }
4 |
--------------------------------------------------------------------------------
/src/views/text/text-numeric.js:
--------------------------------------------------------------------------------
1 | import { numDelim } from '../../core/utils/html.js';
2 | import usage from './text-numeric.usage.js';
3 |
4 | const props = `{
5 | text: #.props has no 'text'?
6 | } | overrideProps()`;
7 |
8 | export default function(host) {
9 | host.view.define('text-numeric', function (el, { text }) {
10 | el.innerHTML = numDelim(text);
11 | }, {
12 | tag: 'span',
13 | props,
14 | usage
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/text/text-numeric.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'text-numeric',
4 | data: '"Like a `text` view but adds a thousands separator to integer part of numbers, e.g. 12345678 or 12345.67890"'
5 | },
6 | examples: [
7 | {
8 | title: 'Shorthand usage',
9 | view: 'text-numeric:1234567'
10 | }
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/src/views/text/text.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser */
2 | import usage from './text.usage.js';
3 |
4 | const props = `{
5 | text: #.props has no 'text'?
6 | } | overrideProps()`;
7 |
8 | export default function(host) {
9 | host.view.define('text', function(el, { text }) {
10 | el.appendChild(document.createTextNode(String(text)));
11 | }, {
12 | tag: false,
13 | props,
14 | usage
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/src/views/text/text.usage.js:
--------------------------------------------------------------------------------
1 | export default {
2 | demo: {
3 | view: 'text',
4 | data: '"Hello world!"'
5 | },
6 | examples: [
7 | {
8 | title: 'Shorthand usage',
9 | view: 'text:"Hello world!"'
10 | }
11 | ]
12 | };
13 |
--------------------------------------------------------------------------------
/src/views/tree/tree.css:
--------------------------------------------------------------------------------
1 | .view-tree {
2 | list-style: none;
3 | margin: 0;
4 | padding: 0;
5 | font-size: 13px;
6 | line-height: 24px;
7 | }
8 | .view-tree:empty::before {
9 | content: attr(emptyText);
10 | color: #888;
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "src/**/*.ts"
4 | ],
5 | "compilerOptions": {
6 | "lib": ["ES2022", "DOM", "DOM.iterable"],
7 | "moduleResolution": "Bundler",
8 | "module": "ESNext",
9 | "target": "ES2022",
10 | "allowJs": true,
11 | "outDir": "./lib", // there are errors without the option (a value doesn't make sense) because of allowJs: true
12 | // "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "declaration": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------