├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── projects
├── demo
│ ├── src
│ │ ├── app
│ │ │ ├── app.component.html
│ │ │ ├── app.component.ts
│ │ │ ├── config.ts
│ │ │ └── pokemon.service.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── environments
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── polyfills.ts
│ │ └── styles.scss
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
└── lib
│ ├── ng-package.json
│ ├── package.json
│ ├── schematics
│ ├── collection.json
│ ├── make-static
│ │ ├── index.ts
│ │ ├── rules
│ │ │ ├── config-rule.ts
│ │ │ └── template-rule.ts
│ │ ├── schema.json
│ │ ├── schema.ts
│ │ └── utils
│ │ │ └── html-transformer.ts
│ └── ng-add
│ │ └── index.ts
│ ├── src
│ ├── conditions
│ │ ├── condition.pipe.ts
│ │ ├── conditions.service.ts
│ │ ├── index.ts
│ │ └── public-api.ts
│ ├── configurable
│ │ ├── configurable.directive.ts
│ │ ├── configurable.service.ts
│ │ ├── index.ts
│ │ └── public-api.ts
│ ├── configuration
│ │ ├── config.actions.ts
│ │ ├── config.model.ts
│ │ ├── config.module.ts
│ │ ├── config.reducer.ts
│ │ ├── config.selectors.ts
│ │ ├── config.service.ts
│ │ ├── index.ts
│ │ └── public-api.ts
│ ├── configurator
│ │ ├── checkbox
│ │ │ └── checkbox.component.ts
│ │ ├── color-picker
│ │ │ └── color-picker.component.ts
│ │ ├── configurator.component.html
│ │ ├── configurator.component.scss
│ │ ├── configurator.component.ts
│ │ ├── configurator.models.ts
│ │ ├── editors
│ │ │ ├── class-editor.component.ts
│ │ │ ├── condition-editor.component.html
│ │ │ ├── condition-editor.component.ts
│ │ │ ├── flex-editor.component.html
│ │ │ ├── flex-editor.component.scss
│ │ │ ├── flex-editor.component.ts
│ │ │ ├── html-editor.component.ts
│ │ │ ├── index.ts
│ │ │ └── spacing-editor.component.ts
│ │ ├── image-selector
│ │ │ └── img-selector.component.ts
│ │ ├── index.ts
│ │ ├── multi-selector
│ │ │ ├── multi-selector.component.html
│ │ │ └── multi-selector.component.ts
│ │ ├── palette
│ │ │ ├── palette.component.html
│ │ │ ├── palette.component.scss
│ │ │ └── palette.component.ts
│ │ ├── public-api.ts
│ │ ├── toolbar
│ │ │ ├── toolbar.component.html
│ │ │ └── toolbar.component.ts
│ │ └── tree
│ │ │ ├── tree.component.css
│ │ │ ├── tree.component.html
│ │ │ └── tree.component.ts
│ ├── dynamic-views
│ │ ├── drag-drop.service.ts
│ │ ├── index.ts
│ │ ├── item
│ │ │ ├── item.component.html
│ │ │ └── item.component.ts
│ │ ├── public-api.ts
│ │ └── zone
│ │ │ ├── zone-context.service.ts
│ │ │ ├── zone.component.html
│ │ │ └── zone.component.ts
│ ├── public-api.ts
│ ├── svg
│ │ └── svg-icons.ts
│ ├── uib.module.ts
│ └── utils
│ │ ├── autocomplete
│ │ └── autocomplete.component.ts
│ │ ├── directive
│ │ ├── model-change.directive.ts
│ │ ├── template-name.directive.ts
│ │ └── tooltip.directive.ts
│ │ ├── index.ts
│ │ ├── modal
│ │ └── modal.component.ts
│ │ ├── public-api.ts
│ │ ├── svg-icon
│ │ ├── index.ts
│ │ ├── public-api.ts
│ │ ├── registry.ts
│ │ ├── svg-icon.component.ts
│ │ ├── svg-icon.module.ts
│ │ └── types.ts
│ │ ├── toast
│ │ ├── toast.component.html
│ │ ├── toast.component.ts
│ │ └── toast.service.ts
│ │ └── typings.ts
│ ├── styles
│ ├── _mixins.scss
│ ├── _selected.scss
│ ├── _uib-bootstrap.scss
│ ├── _zone.scss
│ └── ui-builder.scss
│ ├── tsconfig.doc.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ ├── tsconfig.schematics.json
│ └── tsconfig.spec.json
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the main branch
8 | push:
9 | branches: [ main ]
10 | pull_request:
11 | branches: [ main ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 |
23 | strategy:
24 | matrix:
25 | node-version: [16.x]
26 |
27 | # Steps represent a sequence of tasks that will be executed as part of the job
28 | steps:
29 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
30 | - uses: actions/checkout@v2
31 |
32 | - name: Clean install
33 | run: npm ci --force
34 |
35 | - name: Build lib
36 | run: npm run build:lib
37 |
38 | - name: Build app
39 | run: npm run build demo
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 |
16 | # IDEs and editors
17 | /.idea
18 | .project
19 | .classpath
20 | .c9/
21 | *.launch
22 | .settings/
23 | *.sublime-workspace
24 |
25 | # IDE - VSCode
26 | .vscode/*
27 | !.vscode/settings.json
28 | !.vscode/tasks.json
29 | !.vscode/launch.json
30 | !.vscode/extensions.json
31 | .history/*
32 |
33 | # misc
34 | /.angular/cache
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Sinequa
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 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "demo": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | },
12 | "@schematics/angular:application": {
13 | "strict": true
14 | }
15 | },
16 | "root": "projects/demo",
17 | "sourceRoot": "projects/demo/src",
18 | "prefix": "app",
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-devkit/build-angular:application",
22 | "options": {
23 | "outputPath": {
24 | "base": "dist/demo"
25 | },
26 | "index": "projects/demo/src/index.html",
27 | "polyfills": [
28 | "projects/demo/src/polyfills.ts"
29 | ],
30 | "tsConfig": "projects/demo/tsconfig.json",
31 | "inlineStyleLanguage": "scss",
32 | "stylePreprocessorOptions": {
33 | "includePaths": ["node_modules"]
34 | },
35 | "assets": [
36 | "projects/demo/src/favicon.ico",
37 | "projects/demo/src/assets"
38 | ],
39 | "styles": [
40 | "projects/demo/src/styles.scss"
41 | ],
42 | "scripts": [],
43 | "browser": "projects/demo/src/main.ts"
44 | },
45 | "configurations": {
46 | "production": {
47 | "budgets": [
48 | {
49 | "type": "initial",
50 | "maximumWarning": "500kb",
51 | "maximumError": "1mb"
52 | },
53 | {
54 | "type": "anyComponentStyle",
55 | "maximumWarning": "8kb",
56 | "maximumError": "10kb"
57 | }
58 | ],
59 | "fileReplacements": [
60 | {
61 | "replace": "projects/demo/src/environments/environment.ts",
62 | "with": "projects/demo/src/environments/environment.prod.ts"
63 | }
64 | ],
65 | "outputHashing": "all"
66 | },
67 | "development": {
68 | "optimization": false,
69 | "extractLicenses": false,
70 | "sourceMap": true,
71 | "namedChunks": true
72 | }
73 | },
74 | "defaultConfiguration": "production"
75 | },
76 | "serve": {
77 | "builder": "@angular-devkit/build-angular:dev-server",
78 | "configurations": {
79 | "production": {
80 | "buildTarget": "demo:build:production"
81 | },
82 | "development": {
83 | "buildTarget": "demo:build:development"
84 | }
85 | },
86 | "defaultConfiguration": "development"
87 | },
88 | "extract-i18n": {
89 | "builder": "@angular-devkit/build-angular:extract-i18n",
90 | "options": {
91 | "buildTarget": "demo:build"
92 | }
93 | }
94 | }
95 | },
96 | "lib": {
97 | "projectType": "library",
98 | "root": "projects/lib",
99 | "sourceRoot": "projects/lib/src",
100 | "prefix": "lib",
101 | "architect": {
102 | "build": {
103 | "builder": "@angular-devkit/build-angular:ng-packagr",
104 | "options": {
105 | "project": "projects/lib/ng-package.json"
106 | },
107 | "configurations": {
108 | "production": {
109 | "tsConfig": "projects/lib/tsconfig.lib.prod.json"
110 | },
111 | "development": {
112 | "tsConfig": "projects/lib/tsconfig.lib.json"
113 | }
114 | },
115 | "defaultConfiguration": "production"
116 | }
117 | }
118 | }
119 |
120 | },
121 | "cli": {
122 | "analytics": false
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-ui-builder",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "tsc": "tsc",
7 | "start": "ng serve",
8 | "build": "ng build",
9 | "build:lib": "ng build lib",
10 | "postbuild:lib": "copyfiles README.md dist/lib/ && cd projects/lib && npm run build",
11 | "watch": "ng build --watch --configuration development"
12 | },
13 | "private": false,
14 | "dependencies": {
15 | "@angular/animations": "^18.2.13",
16 | "@angular/common": "^18.2.13",
17 | "@angular/compiler": "^18.2.13",
18 | "@angular/core": "^18.2.13",
19 | "@angular/forms": "^18.2.13",
20 | "@angular/platform-browser": "^18.2.13",
21 | "@angular/platform-browser-dynamic": "^18.2.13",
22 | "@angular/router": "^18.2.13",
23 | "@ngrx/store": "^18.1.1",
24 | "@popperjs/core": "^2.11.5",
25 | "bootstrap": "^5.2.0",
26 | "immer": "^9.0.16",
27 | "ngrx-wieder": "^13.0.0",
28 | "ngx-drag-drop": "^18.0.2",
29 | "rxjs": "~7.5.0",
30 | "tslib": "^2.3.1",
31 | "zone.js": "~0.14.10"
32 | },
33 | "overrides": {
34 | "esbuild": "0.25.0"
35 | },
36 | "devDependencies": {
37 | "@angular-devkit/build-angular": "^18.2.14",
38 | "@angular/cli": "~18.2.14",
39 | "@angular/compiler-cli": "^18.2.13",
40 | "@angular/localize": "^18.2.13",
41 | "@schematics/angular": "^18.2.14",
42 | "@types/bootstrap": "^5.2.0",
43 | "@types/node": "^12.11.1",
44 | "copyfiles": "^2.4.1",
45 | "html-prettify": "1.0.7",
46 | "htmlparser": "^1.7.7",
47 | "ng-packagr": "^18.2.1",
48 | "sanitize-html": "2.12.1",
49 | "typescript": "~5.4.5"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/projects/demo/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Filters
31 |
32 |
33 | {{ config.field | titlecase }}: {{ this[config.field] }}
34 |
35 |
36 |
37 |
38 | Abilities:
39 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {{ data?.name | titlecase }} #{{ data?.id }}
56 |
57 | {{data?.description}}
58 |
59 |
60 |
61 |
62 | {{ config.field }}:
63 | {{meta}}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
92 |
93 |
94 |
95 |
96 |
97 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/projects/demo/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ConfigurableService, ConfigService, ZoneComponent, ToolbarComponent, ConfiguratorComponent, ToastComponent, TemplateNameDirective, NgModelChangeDebouncedDirective, TooltipDirective } from '@sinequa/ngx-ui-builder';
3 | import { PokemonService } from "./pokemon.service";
4 | import { defaultConfig } from "./config";
5 | import { CommonModule } from '@angular/common';
6 | import { FormsModule } from '@angular/forms';
7 |
8 | @Component({
9 | selector: 'app-root',
10 | standalone: true,
11 | imports: [
12 | CommonModule,
13 | FormsModule,
14 | ZoneComponent,
15 | ToolbarComponent,
16 | ConfiguratorComponent,
17 | ToastComponent,
18 | TemplateNameDirective,
19 | NgModelChangeDebouncedDirective,
20 | TooltipDirective
21 | ],
22 | templateUrl: './app.component.html'
23 | })
24 | export class AppComponent implements OnInit {
25 |
26 | // Pokestore state
27 | allAbilities: string[] = [];
28 | searchText?: string;
29 | abilities: string[] = [];
30 | weight?: number;
31 | experience?: number;
32 |
33 | constructor(
34 | public configService: ConfigService,
35 | public configurableService: ConfigurableService,
36 | public pokemonService: PokemonService
37 | ) {}
38 |
39 | ngOnInit() {
40 |
41 | this.allAbilities = this.pokemonService.getAbilities();
42 |
43 | // Initial state of the UI builder
44 | this.configService.init(defaultConfig);
45 | }
46 |
47 | // Pokestore functionalities
48 |
49 | search() {
50 | this.pokemonService.searchPokemons(this.searchText, this.abilities, this.weight, this.experience);
51 | }
52 |
53 | selectAbility(a: string) {
54 | this.abilities.push(a);
55 | this.search();
56 | return false;
57 | }
58 |
59 | clear() {
60 | this.searchText = undefined;
61 | this.abilities = [];
62 | this.experience = undefined;
63 | this.weight = undefined;
64 | this.pokemonService.Reset();
65 | }
66 |
67 | // Utilities
68 |
69 | asArray(value: any) {
70 | return Array.isArray(value)? value : [value];
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/projects/demo/src/app/config.ts:
--------------------------------------------------------------------------------
1 | import { ComponentConfig } from "@sinequa/ngx-ui-builder";
2 |
3 | export const defaultConfig:ComponentConfig[] = [
4 | {
5 | id: 'header',
6 | type: '_container',
7 | items: ['title', 'searchform'],
8 | classes: 'flex-column'
9 | },
10 | {
11 | id: 'title',
12 | type: 'title',
13 | title: 'Pokestore'
14 | },
15 | {
16 | id: 'searchform',
17 | type: '_container',
18 | items: ['searchbar','applied-filters','clear-filters'],
19 | classes: 'flex-row'
20 | },
21 | {
22 | id: "searchbar",
23 | type: "searchbar"
24 | },
25 | {
26 | id: 'results',
27 | type: '_container',
28 | items: ['image','metas'],
29 | classes: 'result-item flex-row mb-3 w-100'
30 | },
31 | {
32 | id: 'metas',
33 | type: '_container',
34 | items: ['pokename', 'description', 'ability', 'weight', 'experience'],
35 | classes: 'flex-column'
36 | },
37 | {
38 | id: 'ability',
39 | type: 'metadata',
40 | field: 'abilities',
41 | },
42 | {
43 | id: 'weight',
44 | type: 'metadata',
45 | field: 'weight',
46 | },
47 | {
48 | id: 'experience',
49 | type: 'metadata',
50 | field: 'experience',
51 | },
52 |
53 | {
54 | id: 'filters',
55 | type: '_container',
56 | items: ['abilities', 'weightFilter', 'xpFilter'],
57 | classes: 'flex-column'
58 | },
59 | {
60 | id: 'weightFilter',
61 | type: 'range',
62 | field: 'weight',
63 | },
64 | {
65 | id: 'xpFilter',
66 | type: 'range',
67 | field: 'experience',
68 | }
69 | ];
--------------------------------------------------------------------------------
/projects/demo/src/app/pokemon.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from "@angular/common/http";
2 | import { inject, Injectable } from "@angular/core";
3 | import { BehaviorSubject, firstValueFrom } from "rxjs";
4 |
5 | export interface Pokemon {
6 | name: string;
7 | description: string;
8 | id: number;
9 | abilities: string[];
10 | weight: number;
11 | experience: number;
12 | }
13 |
14 |
15 | @Injectable({providedIn: 'root'})
16 | export class PokemonService {
17 |
18 | api:Record;
19 |
20 | pokemons: Pokemon[] = [
21 | {
22 | name: 'slowpoke',
23 | description:
24 | "Slowpoke (Japanese: ヤドン Yadon) is a dual-type Water/Psychic Pokémon introduced in Generation I. It evolves into Slowbro starting at level 37 or Slowking when traded while holding a King's Rock. In Galar, Slowpoke has a pure Psychic-type regional form, introduced in Pokémon Sword and Shield's 1.1.0 patch. It evolves into Galarian Slowbro when exposed to a Galarica Cuff or Galarian Slowking when exposed to a Galarica Wreath.",
25 | id: 79,
26 | abilities: ['oblivious', 'own-tempo', 'regenerator'],
27 | weight: 360,
28 | experience: 63,
29 | },
30 | {
31 | name: 'zacian-hero',
32 | description:
33 | 'Zacian (Japanese: ザシアン Zacian) is a Fairy-type Legendary Pokémon introduced in Generation VIII. While it is not known to evolve into or from any other Pokémon, Zacian has a second form activated by giving it a Rusted Sword to hold. Its original form, Hero of Many Battles, will then become the Fairy/Steel-type Crowned Sword. Zacian is the game mascot of Pokémon Sword, appearing on the boxart in its Crowned Sword form. It is a member of the Hero duo with Zamazenta.',
34 | id: 888,
35 | abilities: ['intrepid-sword'],
36 | weight: 1100,
37 | experience: 335,
38 | },
39 | {
40 | name: 'clefairy',
41 | description:
42 | 'Clefairy (Japanese: ピッピ Pippi) is a Fairy-type Pokémon introduced in Generation I. Prior to Generation VI, it was a Normal-type Pokémon. It evolves from Cleffa when leveled up with high friendship and evolves into Clefable when exposed to a Moon Stone.',
43 | id: 35,
44 | abilities: ['cute-charm'],
45 | weight: 75,
46 | experience: 113,
47 | },
48 | {
49 | name: 'pikachu',
50 | description:
51 | 'Pikachu (Japanese: ピカチュウ Pikachu) is an Electric-type Pokémon introduced in Generation I. It evolves from Pichu when leveled up with high friendship and evolves into Raichu when exposed to a Thunder Stone. In Alola, Pikachu will evolve into Alolan Raichu when exposed to a Thunder Stone.',
52 | id: 25,
53 | abilities: ['static', 'lightning-rod'],
54 | weight: 60,
55 | experience: 112,
56 | },
57 | {
58 | name: 'bulbasaur',
59 | description:
60 | 'Bulbasaur (Japanese: フシギダネ Fushigidane) is a dual-type Grass/Poison Pokémon introduced in Generation I. It evolves into Ivysaur starting at level 16, which evolves into Venusaur starting at level 32. Along with Charmander and Squirtle, Bulbasaur is one of three starter Pokémon of Kanto available at the beginning of Pokémon Red, Green, Blue, FireRed, and LeafGreen.',
61 | id: 1,
62 | abilities: ['overgrow', 'chlorophyll'],
63 | weight: 69,
64 | experience: 64,
65 | },
66 | {
67 | name: 'charmander',
68 | description:
69 | 'Charmander (Japanese: ヒトカゲ Hitokage) is a Fire-type Pokémon introduced in Generation I. It evolves into Charmeleon starting at level 16, which evolves into Charizard starting at level 36. Along with Bulbasaur and Squirtle, Charmander is one of three starter Pokémon of Kanto available at the beginning of Pokémon Red, Green, Blue, FireRed, and LeafGreen.',
70 | id: 4,
71 | abilities: ['blaze', 'solar-power'],
72 | weight: 85,
73 | experience: 62,
74 | },
75 | {
76 | name: 'sandshrew',
77 | description:
78 | 'Sandshrew (Japanese: サンド Sand) is a Ground-type Pokémon introduced in Generation I. It evolves into Sandslash starting at level 22. In Alola, Sandshrew has a dual-type Ice/Steel regional form. It evolves into Alolan Sandslash when exposed to an Ice Stone.',
79 | id: 27,
80 | abilities: ['sand-veil', 'sand-rush'],
81 | weight: 120,
82 | experience: 60,
83 | },
84 | {
85 | name: 'wartortle',
86 | description:
87 | 'Wartortle (Japanese: カメール Kameil) is a Water-type Pokémon introduced in Generation I. It evolves from Squirtle starting at level 16 and evolves into Blastoise starting at level 36.',
88 | id: 142,
89 | abilities: ['torrent', 'rain-dish'],
90 | weight: 225,
91 | experience: 142,
92 | },
93 | {
94 | name: 'poliwhirl',
95 | description:
96 | "Poliwhirl (Japanese: ニョロゾ Nyorozo) is a Water-type Pokémon introduced in Generation I. It evolves from Poliwag starting at level 25. It evolves into Poliwrath when exposed to a Water Stone or Politoed when traded while holding a King's Rock.",
97 | id: 61,
98 | abilities: ['water-absorb', 'damp', 'swift-swim'],
99 | weight: 200,
100 | experience: 135,
101 | },
102 | {
103 | name: 'vileplume',
104 | description:
105 | "Vileplume (Japanese: ラフレシア Ruffresia) is a dual-type Grass/Poison Pokémon introduced in Generation I. It evolves from Gloom when exposed to a Leaf Stone. It is one of Oddish's final forms, the other being Bellossom.",
106 | id: 45,
107 | abilities: ['chlorophyll', 'effect-spore'],
108 | weight: 186,
109 | experience: 221,
110 | },
111 | ];
112 |
113 | pokemons$ = new BehaviorSubject([]);
114 |
115 | httpClient = inject(HttpClient);
116 |
117 | constructor() {
118 | // custom API Proxy
119 | this.api = this.createApi("https://pokeapi.co/api/v2");
120 | this.getPokemon();
121 | }
122 |
123 | async getPokemon() {
124 | // retrieve 100 pokemon, and for each retrieve informations
125 | const pokemon = await this.api.pokemon();
126 | const pokemonPromises = pokemon.results.map(p => this.createPokemon(p));
127 |
128 | // when all pokemon are created emit signal
129 | Promise.all(pokemonPromises).then(values => {
130 | this.pokemons = values;
131 | this.pokemons$.next(values)
132 | });
133 | }
134 |
135 | async createPokemon(p): Promise {
136 | // call: https://pokeapi.co/api/v2/pokemon/:name
137 | const poke = await this.api.pokemon(p.name);
138 |
139 | // call: https://pokeapi.co/api/v2/pokemon-species/:id
140 | const desc = await this.api['pokemon-species'](poke.id);
141 |
142 | // create a Pokemon object
143 | return ({
144 | id: poke.id,
145 | name: p.name,
146 | description: desc.flavor_text_entries.filter(({ language, version }) => language.name === "en" && version.name === "x")
147 | .map(({ flavor_text }) => flavor_text)
148 | .join()
149 | .replaceAll("", " "),
150 | abilities: poke.abilities.map(({ability}) => ability.name),
151 | weight: poke.weight,
152 | experience: poke.base_experience
153 | });
154 | }
155 |
156 | private createApi = (url) => {
157 | const http = this.httpClient;
158 | return new Proxy({}, {
159 | get(target, key:string) {
160 | return async function (id = "") {
161 | const response = await firstValueFrom(http.get(`${url}/${key}/${id}?limit=100`));
162 | if (response) {
163 | return response;
164 | }
165 | return Promise.resolve({error: "Malformed Request"})
166 | }
167 | }
168 | })
169 | }
170 |
171 | searchPokemons(search = "", abilities: string[] = [], weight?: number, experience?: number) {
172 | this.pokemons$.next(this.pokemons.filter(p => {
173 | if (abilities.length && !p.abilities.find(a => abilities.includes(a)))
174 | return false;
175 | if (weight && p.weight > weight) return false;
176 | if (experience && p.experience > experience)
177 | return false;
178 | if (search && !p.name.includes(search)) return false;
179 | return true;
180 | }));
181 | }
182 |
183 | Reset() {
184 | this.pokemons$.next(this.pokemons);
185 | }
186 |
187 | getAbilities(): string[] {
188 | return this.pokemons
189 | .map((p) => p.abilities)
190 | .reduce((v1, v2) => v1.concat(v2), []);
191 | }
192 |
193 | }
194 |
--------------------------------------------------------------------------------
/projects/demo/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinequa/ngx-ui-builder/b4bdcb6e4ddf8696e22b452908960b8a73590269/projects/demo/src/assets/.gitkeep
--------------------------------------------------------------------------------
/projects/demo/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/projects/demo/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/projects/demo/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinequa/ngx-ui-builder/b4bdcb6e4ddf8696e22b452908960b8a73590269/projects/demo/src/favicon.ico
--------------------------------------------------------------------------------
/projects/demo/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Pokestore
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/projects/demo/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode, importProvidersFrom } from '@angular/core';
2 | import { bootstrapApplication } from '@angular/platform-browser';
3 |
4 | import { environment } from './environments/environment';
5 | import { AppComponent } from "./app/app.component";
6 | import { StoreModule } from '@ngrx/store';
7 | import { ConfigModule, SvgIconsModule, icons } from '@sinequa/ngx-ui-builder';
8 | import { provideHttpClient } from '@angular/common/http';
9 |
10 | if (environment.production) {
11 | enableProdMode();
12 | }
13 |
14 | bootstrapApplication(AppComponent, {
15 | providers: [
16 | provideHttpClient(),
17 | importProvidersFrom([
18 | SvgIconsModule.forRoot({icons: icons}),
19 | StoreModule.forRoot({}),
20 | ConfigModule
21 | ])
22 | ]
23 | })
24 |
--------------------------------------------------------------------------------
/projects/demo/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /***************************************************************************************************
2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
3 | */
4 | import '@angular/localize/init';
5 | /**
6 | * This file includes polyfills needed by Angular and is loaded before the app.
7 | * You can add your own extra polyfills to this file.
8 | *
9 | * This file is divided into 2 sections:
10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
12 | * file.
13 | *
14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
17 | *
18 | * Learn more in https://angular.io/guide/browser-support
19 | */
20 |
21 | /***************************************************************************************************
22 | * BROWSER POLYFILLS
23 | */
24 |
25 | /**
26 | * IE11 requires the following for NgClass support on SVG elements
27 | */
28 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
29 |
30 | /**
31 | * Web Animations `@angular/platform-browser/animations`
32 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
33 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
34 | */
35 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
36 |
37 | /**
38 | * By default, zone.js will patch all possible macroTask and DomEvents
39 | * user can disable parts of macroTask/DomEvents patch by setting following flags
40 | * because those flags need to be set before `zone.js` being loaded, and webpack
41 | * will put import in the top of bundle, so user need to create a separate file
42 | * in this directory (for example: zone-flags.ts), and put the following flags
43 | * into that file, and then add the following code before importing zone.js.
44 | * import './zone-flags';
45 | *
46 | * The flags allowed in zone-flags.ts are listed here.
47 | *
48 | * The following flags will work for all browsers.
49 | *
50 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
51 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
52 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
53 | *
54 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
55 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
56 | *
57 | * (window as any).__Zone_enable_cross_context_check = true;
58 | *
59 | */
60 |
61 | /***************************************************************************************************
62 | * Zone JS is required by default for Angular itself.
63 | */
64 | import 'zone.js'; // Included with Angular CLI.
65 |
66 |
67 | /***************************************************************************************************
68 | * APPLICATION IMPORTS
69 | */
70 |
--------------------------------------------------------------------------------
/projects/demo/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* bootstrap's utilities only */
2 | @import "bootstrap/scss/mixins/banner";
3 | @include bsBanner(Utilities);
4 | @import "bootstrap/scss/functions";
5 | @import "bootstrap/scss/variables";
6 | @import "bootstrap/scss/maps";
7 | @import "bootstrap/scss/mixins";
8 | @import "bootstrap/scss/utilities";
9 | @import "bootstrap/scss/root";
10 | @import "bootstrap/scss/helpers";
11 | @import "bootstrap/scss/utilities/api";
12 |
13 | /* specific import used by the ui builder lib */
14 | @import "../../lib/styles/ui-builder";
15 |
16 | body {
17 | font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
18 | }
19 |
20 | .filters {
21 | h3, h4 {
22 | margin: 0;
23 | }
24 | h4 {
25 | margin-top: 1em;
26 | }
27 | }
28 |
29 | .main {
30 | display: flex;
31 | max-width: 1000px;
32 | .result-item:hover {
33 | background: aliceblue;
34 | }
35 | }
36 |
37 | .metadata {
38 | color: #212529;
39 | background-color: #e3e6e9;
40 | display: inline-block;
41 | padding: 0.25em 0.4em;
42 | margin: 0 0.25em 0.25em 0.25em;
43 | font-size: 85%;
44 | font-weight: 700;
45 | line-height: 1;
46 | text-align: center;
47 | white-space: nowrap;
48 | vertical-align: baseline;
49 | padding-right: 0.6em;
50 | padding-left: 0.6em;
51 | border-radius: 10rem;
52 | text-decoration: none;
53 | }
54 |
55 | a.metadata:hover {
56 | background-color: #cfd5db;
57 | }
58 |
--------------------------------------------------------------------------------
/projects/demo/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/projects/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/app",
6 | "types": [],
7 | "paths": {
8 | "@sinequa/ngx-ui-builder": ["./projects/lib/src/public-api"],
9 | "@sinequa/ngx-ui-builder/*": ["./projects/lib/src/*"]
10 | }
11 | },
12 | "files": [
13 | "src/main.ts",
14 | "src/polyfills.ts"
15 | ],
16 | "include": [
17 | "src/**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/projects/demo/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/projects/lib/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/lib",
4 | "assets": [
5 | "styles/*.scss"
6 | ],
7 | "lib": {
8 | "entryFile": "src/public-api.ts"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/projects/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@sinequa/ngx-ui-builder",
3 | "version": "1.1.0",
4 | "author": "Sinequa",
5 | "repository": "github:sinequa/ngx-ui-builder",
6 | "homepage": "https://sinequa.github.io/ngx-ui-builder/",
7 | "description": "An Angular library for creating no-code tools & applications",
8 | "keywords": ["no code", "angular", "angular2", "sinequa", "drag and drop", "point and click", "ui builder", "configurable"],
9 | "license": "MIT",
10 | "scripts": {
11 | "build": "tsc -p tsconfig.schematics.json",
12 | "postbuild": "copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ../../dist/lib/"
13 | },
14 | "schematics": "./schematics/collection.json",
15 | "ng-add": {
16 | "save": "dependencies"
17 | },
18 | "peerDependencies": {
19 | "@angular/common": "^18.0.0",
20 | "@angular/core": "^18.0.0",
21 | "@ngrx/store": "^18.1.1",
22 | "@popperjs/core": "^2.11.5",
23 | "bootstrap": "^5.2.0",
24 | "immer": "^9.0.16",
25 | "ngrx-wieder": "^13.0.0",
26 | "ngx-drag-drop": "^18.0.0"
27 | },
28 | "dependencies": {
29 | "tslib": "^2.3.0"
30 | },
31 | "devDependencies": {
32 | "@types/bootstrap": "^5.1.6",
33 | "@angular/localize": "^14.1.0",
34 | "copyfiles": "file:../../node_modules/copyfiles",
35 | "typescript": "file:../../node_modules/typescript"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/projects/lib/schematics/collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3 | "schematics": {
4 | "ng-add": {
5 | "description": "Add ngx-ui-builder to the project",
6 | "factory": "./ng-add/index#ngAdd"
7 | },
8 | "make-static": {
9 | "description": "Turn a UI Builder application into a static application",
10 | "factory": "./make-static/index#makeStatic",
11 | "schema": "./make-static/schema.json"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/projects/lib/schematics/make-static/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Rule, Tree, SchematicContext, chain
3 | } from '@angular-devkit/schematics';
4 |
5 | import { MakeStaticOptions } from "./schema";
6 | import { updateHTML } from './rules/template-rule';
7 | import { addConfigObjectRule } from './rules/config-rule';
8 |
9 | // Entry point of the make-static schematic
10 | export function makeStatic(options: MakeStaticOptions): Rule {
11 | return (tree: Tree, context: SchematicContext) => {
12 | const rule = chain([
13 | updateHTML(options),
14 | addConfigObjectRule(options)
15 | ])
16 | return rule(tree, context);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/projects/lib/schematics/make-static/rules/config-rule.ts:
--------------------------------------------------------------------------------
1 | import { Tree, Rule, SchematicsException } from "@angular-devkit/schematics";
2 | import ts, {Expression, factory} from "typescript";
3 | import { MakeStaticOptions } from "../schema";
4 |
5 | /**
6 | * `LiteralValueType` is a type alias that represents the possible types of
7 | * literal values in TypeScript, such as strings, numbers, and booleans. It is
8 | * used in the `createProperty` and `createValue` functions to ensure that the
9 | * correct type of value is created for a given property assignment.
10 | */
11 | type LiteralValueType = string | number | boolean | [];
12 |
13 | let CONFIG_IDENTIFIER = 'GLOBAL_CONFIG';
14 |
15 | /**
16 | * This variable is later used to store
17 | * the configuration data read from a JSON file and to update an existing
18 | * TypeScript object literal expression or create a new one.
19 | */
20 | let configuration = [];
21 |
22 | /**
23 | * This function reads a configuration file and updates a TypeScript source file
24 | * with a custom transformer or creates a new config object literal if it doesn't
25 | * exist.
26 | * @param options - An object containing options for the function, including:
27 | * @returns A function that takes in an options object and returns a Rule.
28 | */
29 | export function addConfigObjectRule(options: MakeStaticOptions): Rule {
30 | return async (tree: Tree) => {
31 |
32 | // exit when 'config-path' or 'config' flag are not provided
33 | if (!options?.configPath) return;
34 | if (!options?.config) return;
35 |
36 | CONFIG_IDENTIFIER = options.configIdentifier || CONFIG_IDENTIFIER;
37 |
38 | // first read the configuration json file
39 | const configFileContent = tree.read(options.config);
40 | configuration = JSON.parse(configFileContent?.toString() || '');
41 | if (!Array.isArray(configuration)) {
42 | throw new SchematicsException(`Expected an array of configuration objects in ${options.config}: ${configuration}`);
43 | }
44 |
45 | // create ts source file
46 | const code = tree.read(options.configPath)?.toString("utf-8");
47 | const sourceFile = ts.createSourceFile(options.configPath, code || "", ts.ScriptTarget.Latest, true)
48 |
49 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
50 | let content: string;
51 |
52 | // first check if config exists
53 | let found = visit(sourceFile, CONFIG_IDENTIFIER);
54 | if (found) {
55 | // transform ts file with custom transformer
56 | const transformedSourceFile = ts.transform(sourceFile, [updateExistingConfigObject]).transformed[0];
57 | content = printer.printFile(transformedSourceFile);
58 |
59 | } else {
60 | const c = createDefaultConfigObject();
61 | content = printer.printFile(sourceFile);
62 | content +=printer.printNode(ts.EmitHint.Unspecified, c, sourceFile);
63 | }
64 | tree.overwrite(options.configPath, content);
65 | };
66 | }
67 |
68 |
69 |
70 | /**
71 | * This function updates an existing TypeScript configuration object with
72 | * additional properties.
73 | * @param context - The context parameter is an object that provides access to the
74 | * TypeScript compiler's API, including the factory object used to create new AST
75 | * nodes. It is used to generate new AST nodes that will be used to update the
76 | * existing configuration object.
77 | * @returns The function `updateExistingConfigObject` returns a higher-order
78 | * function that takes a TypeScript AST node as an argument and returns a
79 | * transformed version of that node. The transformed node will have additional
80 | * configuration objects appended to an array literal expression if the parent node
81 | * is a variable declaration with a specific identifier.
82 | */
83 | const updateExistingConfigObject = (context) => (root) => {
84 | function visit(node: ts.Node): ts.VisitResult {
85 | if (ts.isVariableDeclaration(node)) {
86 | if (node.name.getText() === CONFIG_IDENTIFIER && node.type === undefined) {
87 | return factory.updateVariableDeclaration(node,
88 | factory.createIdentifier(CONFIG_IDENTIFIER),
89 | undefined,
90 | factory.createArrayTypeNode(factory.createTypeReferenceNode(
91 | factory.createIdentifier("ComponentConfig"),
92 | undefined
93 | )),
94 | factory.createArrayLiteralExpression(
95 | [...configuration.map(c => ts.factory.createObjectLiteralExpression([
96 | ...Object.keys(c).map(k => createProperty(k, c[k]))
97 | ]))],
98 | false
99 | )
100 | )
101 | }
102 | }
103 | if (ts.isArrayLiteralExpression(node)) {
104 | const { factory } = context;
105 | const parent: ts.VariableDeclaration = node.parent as ts.VariableDeclaration;
106 | if (parent.name && parent.name.getText() === CONFIG_IDENTIFIER) {
107 | return factory.updateArrayLiteralExpression(node, [
108 | ...node.elements,
109 | ...configuration.map(c => ts.factory.createObjectLiteralExpression([
110 | ...Object.keys(c).map(k => createProperty(k, c[k]))
111 | ])),
112 | ])
113 | }
114 | }
115 | return ts.visitEachChild(node, visit, context);
116 | }
117 | return ts.visitNode(root, visit);
118 | }
119 |
120 | /**
121 | * The function visits each node in a TypeScript AST and checks if it is an array
122 | * literal expression with a specific identifier.
123 | * @param node - A TypeScript Node object representing a node in the abstract
124 | * syntax tree (AST) of a TypeScript program.
125 | * @param {string} identifier - The identifier parameter is a string that
126 | * represents the name of a variable that we are searching for in the TypeScript
127 | * AST (Abstract Syntax Tree).
128 | * @returns The function does not have a return statement, so it will return
129 | * `undefined` by default.
130 | */
131 | function visit(node: ts.Node, identifier: string) {
132 | if (ts.isVariableDeclaration(node)) {
133 | if (node.name.getText() === identifier) {
134 | return true;
135 | }
136 | }
137 | return node.forEachChild((child) => visit(child, identifier));
138 | }
139 |
140 |
141 | /**
142 | * This function creates a default configuration object in TypeScript.
143 | * @returns a TypeScript AST (Abstract Syntax Tree) node that represents a variable
144 | * statement. Specifically, it is creating a default configuration object by using
145 | * the `factory` object to create a `VariableStatement` node that exports a `const`
146 | * variable declaration with an array literal expression containing object literal
147 | * expressions for each configuration object in the `configuration` array.
148 | */
149 | function createDefaultConfigObject() {
150 | return factory.createVariableStatement(
151 | [factory.createToken(ts.SyntaxKind.ExportKeyword)],
152 | factory.createVariableDeclarationList(
153 | [factory.createVariableDeclaration(
154 | factory.createIdentifier(CONFIG_IDENTIFIER),
155 | undefined,
156 | factory.createArrayTypeNode(factory.createTypeReferenceNode(
157 | factory.createIdentifier("ComponentConfig"),
158 | undefined
159 | )),
160 | factory.createArrayLiteralExpression(
161 | [...configuration.map(c => ts.factory.createObjectLiteralExpression([
162 | ...Object.keys(c).map(k => createProperty(k, c[k]))
163 | ]))],
164 | false
165 | )
166 | )],
167 | ts.NodeFlags.Const
168 | )
169 | )
170 | }
171 |
172 | /**
173 | * The function creates a property assignment with a given identifier and value.
174 | * @param {string} identifier - A string representing the name of the property to
175 | * be created.
176 | * @param {LiteralValueType} value - LiteralValueType is a type that represents a
177 | * literal value in TypeScript, such as a string, number, boolean, null, or
178 | * undefined. The `value` parameter in the `createProperty` function is expected to
179 | * be a value of this type.
180 | * @returns The function `createProperty` is returning a property assignment node
181 | * created using the TypeScript factory function
182 | * `factory.createPropertyAssignment`. The property assignment node consists of an
183 | * identifier node created using `factory.createIdentifier` and a value node
184 | * created using the `createValue` function.
185 | */
186 | function createProperty(identifier: string, value: LiteralValueType) {
187 | return factory.createPropertyAssignment(
188 | factory.createIdentifier(identifier),
189 | createValue(value)
190 | )
191 | }
192 | /**
193 | * The function creates a TypeScript value based on the type of the input value.
194 | * @param {LiteralValueType} value - The value parameter is of type
195 | * LiteralValueType, which means it can be a number, boolean, string, null, or
196 | * undefined.
197 | * @returns The function `createValue` takes in a parameter `value` of type
198 | * `LiteralValueType` and returns a TypeScript AST node based on the type of the
199 | * input value. If the input value is a number, it returns a numeric literal node
200 | * using the TypeScript factory method `createNumericLiteral()`. If the input value
201 | * is a boolean, it returns a boolean literal node using the TypeScript factory
202 | * method `create
203 | */
204 | function createValue(value: LiteralValueType): Expression {
205 | switch (typeof(value)) {
206 | case "number":
207 | return factory.createNumericLiteral(value);
208 | case "boolean":
209 | return value ? factory.createTrue() : factory.createFalse()
210 | case "object":
211 | if (Array.isArray(value)) {
212 | return factory.createArrayLiteralExpression([
213 | ...value.map(v => createValue(v))
214 | ])
215 | }
216 | return factory.createObjectLiteralExpression(
217 | Object.keys(value).map(k => createProperty(k, value[k]))
218 | )
219 |
220 | default:
221 | return factory.createStringLiteral(String(value));
222 | }
223 | }
--------------------------------------------------------------------------------
/projects/lib/schematics/make-static/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema",
3 | "$id": "SchematicsMakeStatic",
4 | "title": "Make Static Schema",
5 | "type": "object",
6 | "properties": {
7 | "config": {
8 | "description": "The path to the configuration (.json) of the application.",
9 | "type": "string",
10 | "format": "path"
11 | },
12 | "configPath": {
13 | "description": "The path to the typescript configuration of the application (e.g: path/to/file/config.ts)",
14 | "type": "string",
15 | "format": "path"
16 | },
17 | "configIdentifier": {
18 | "description": "The identifier name used to store de configuration",
19 | "type": "string",
20 | "default": ""
21 | },
22 | "override": {
23 | "description": "If true, will override the source code of the HTML templated rather than generating alternate .static.html files.",
24 | "type": "boolean",
25 | "default": false
26 | },
27 | "updateTemplateUrls": {
28 | "description": "If true (and if 'override' is false), update the component .ts files to use the .static.html templates",
29 | "type": "boolean",
30 | "default": true
31 | },
32 | "commentScssImport": {
33 | "description": "If true, comment out the import of the UI Builder stylesheets in the project's stylesheets",
34 | "type": "boolean",
35 | "default": true
36 | },
37 | "appModuleDependencies": {
38 | "description": "Path to a JSON file mapping a module's name to a list of component types (eg. {PokemonModule: ['pokemon-image', 'pokemon-name']})",
39 | "type": "string",
40 | "format": "path"
41 | },
42 | "createBase64Images": {
43 | "description": "Detect when the configuration contains images encoded as base64 strings and transform them as files in the assets folder",
44 | "type": "boolean",
45 | "default": true
46 | },
47 | "project": {
48 | "type": "string",
49 | "description": "The name of the project.",
50 | "$default": {
51 | "$source": "projectName"
52 | }
53 | }
54 | },
55 | "required": [
56 | "config"
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/projects/lib/schematics/make-static/schema.ts:
--------------------------------------------------------------------------------
1 | export interface MakeStaticOptions {
2 | /**
3 | * The path to the json config file.
4 | */
5 | config: string;
6 |
7 | /**
8 | * The path to the ts configuration file (usually config.ts).
9 | *
10 | * @example
11 | * --config-path=path/to/file/config.ts
12 | */
13 | configPath: string;
14 |
15 | /**
16 | * The configuration identifier to use (e.g: CONFIG)
17 | *
18 | * @example
19 | * --config-identifier=CONFIG
20 | *
21 | * // This will produce this typescript code
22 | * export const CONFIG = {};
23 | */
24 | configIdentifier: string;
25 |
26 | /**
27 | * If true, will override the source code of the HTML templates rather than generating .static.html files
28 | */
29 | override: boolean;
30 |
31 | /**
32 | * If true (and if 'override' is false), update the component .ts files to use the .static.html templates
33 | */
34 | updateTemplateUrls: boolean;
35 |
36 | /**
37 | * If true, attempts to comment out the import of the UI Builder stylesheet in the project's stylesheet
38 | */
39 | commentScssImport: boolean;
40 |
41 | /**
42 | * Path to a JSON file mapping a module's name to a list of component types (eg. {PokemonModule: ['pokemon-image', 'pokemon-name']})
43 | */
44 | appModuleDependencies?: string;
45 |
46 | /**
47 | * Detect when the configuration contains images encoded as base64 strings and transform them as files in the assets folder
48 | */
49 | createBase64Images: boolean;
50 |
51 | /**
52 | * The name of the project.
53 | */
54 | project?: string;
55 | }
56 |
--------------------------------------------------------------------------------
/projects/lib/schematics/ng-add/index.ts:
--------------------------------------------------------------------------------
1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2 | import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
3 | import { addPackageJsonDependency, NodeDependencyType } from '@schematics/angular/utility/dependencies';
4 |
5 | const packageJson = {
6 | peerDependencies: {
7 | "@ngrx/store": "^14.3.2",
8 | "@popperjs/core": "^2.11.5",
9 | "bootstrap": "^5.2.0",
10 | "immer": "^9.0.16",
11 | "ngrx-wieder": "^9.0.0",
12 | "ngx-drag-drop": "^2.0.0"
13 | },
14 | devDependencies: {
15 | "@angular/localize": "^14.1.0",
16 | "@types/bootstrap": "^5.2.0",
17 | "htmlparser": "1.7.7",
18 | "html-prettify": "1.0.7",
19 | "sanitize-html": "2.7.1",
20 | }
21 | }
22 |
23 | // Just return the tree
24 | export function ngAdd(): Rule {
25 | return (tree: Tree, context: SchematicContext) => {
26 |
27 | // Add peerDependencies to the dependencies of the app
28 | Object.keys(packageJson.peerDependencies)
29 | .map(lib => ({
30 | type: NodeDependencyType.Default,
31 | name: lib,
32 | version: packageJson.peerDependencies[lib],
33 | overwrite: true,
34 | }))
35 | .forEach(dep => addPackageJsonDependency(tree, dep));
36 |
37 | // Add devDependency (specifically, bootstrap types)
38 | Object.keys(packageJson.devDependencies)
39 | .map(lib => ({
40 | type: NodeDependencyType.Dev,
41 | name: lib,
42 | version: packageJson.devDependencies[lib],
43 | overwrite: true,
44 | }))
45 | .forEach(dep => addPackageJsonDependency(tree, dep));
46 |
47 | // Run npm install
48 | context.addTask(new NodePackageInstallTask());
49 |
50 | return tree;
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/projects/lib/src/conditions/condition.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from "@angular/core";
2 | import { Condition, ConditionsService } from "./conditions.service";
3 |
4 | @Pipe({
5 | name: 'uibCondition',
6 | standalone: true
7 | })
8 | export class ConditionPipe implements PipeTransform {
9 |
10 | constructor(
11 | public conditionService: ConditionsService
12 | ){}
13 |
14 | transform(value: any, params?: Condition): boolean {
15 | if(!params) return true;
16 | return this.conditionService.checkData(params, value);
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/projects/lib/src/conditions/conditions.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 |
3 |
4 | export interface Condition {
5 | data?: string; // conditionsData selector (or undefined for the main data object)
6 | field: string; // The field to select from the data object
7 | type: 'equals' | 'regexp';
8 | values: ConditionValue[];
9 | or?: boolean;
10 | }
11 |
12 | export interface ConditionValue {
13 | value: string;
14 | not?: boolean;
15 | $regexp?: RegExp;
16 | }
17 |
18 |
19 | @Injectable({providedIn: 'root'})
20 | export class ConditionsService {
21 |
22 | public selectData(condition: Condition, conditionsData?: Record, data?: any): any {
23 | return condition.data? conditionsData?.[condition.data] : data;
24 | }
25 |
26 | public check(condition: Condition | undefined, conditionsData?: Record, data?: any): boolean {
27 | if(!condition) return true;
28 | const selectData = this.selectData(condition, conditionsData, data);
29 | return this.checkData(condition, selectData);
30 | }
31 |
32 | public writeCondition(condition: Condition) {
33 | const operator = condition.or? ') OR (' : ') AND (';
34 | return '('+condition.values.map(v => this.writeValue(condition, v)).join(operator)+')';
35 | }
36 |
37 | public writeValue(condition: Condition, value: ConditionValue) {
38 | if(condition.type === 'regexp') {
39 | const operator = value.not? '!' : '';
40 | return `${operator}${condition.data || 'data'}['${condition.field}'].match(/${value.value}/)'`;
41 | }
42 | else if (condition.type === 'equals') {
43 | const operator = value.not? '!==' : '==='
44 | return `${condition.data || 'data'}['${condition.field}'] ${operator} '${value.value}'`;
45 | }
46 | return '';
47 | }
48 |
49 | public checkData(condition: Condition, data?: any) {
50 | const value = data?.[condition.field]?.toString() || '';
51 | return this.checkCondition(condition, value);
52 | }
53 |
54 | private checkCondition(condition: Condition, data: string): boolean {
55 | if(condition.or) {
56 | return !!condition.values.find(v => this.checkSingleValue(condition, v, data));
57 | }
58 | else {
59 | return condition.values.every(v => this.checkSingleValue(condition, v, data));
60 | }
61 | }
62 |
63 | private checkSingleValue(condition: Condition, value: ConditionValue, data: string) {
64 | let test;
65 | if(condition.type === 'regexp') {
66 | test = this.checkRegexp(value, data);
67 | }
68 | else if(condition.type === 'equals') {
69 | test = this.checkEquals(value, data);
70 | }
71 | return test? !value.not : !!value.not;
72 | }
73 |
74 |
75 | private checkEquals(condition: ConditionValue, value: string): boolean {
76 | return condition.value.toLowerCase() === value.toLowerCase();
77 | }
78 |
79 |
80 | private checkRegexp(condition: ConditionValue, value: string) {
81 | if(!condition.$regexp) {
82 | try {
83 | condition.$regexp = new RegExp(condition.value, 'i');
84 | }
85 | catch(e) {
86 | console.warn("Incorrect regular expression ", condition.value);
87 | return true;
88 | }
89 | }
90 | return value.match(condition.$regexp);
91 | }
92 |
93 | }
--------------------------------------------------------------------------------
/projects/lib/src/conditions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
2 |
--------------------------------------------------------------------------------
/projects/lib/src/conditions/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './condition.pipe';
2 | export * from './conditions.service';
3 |
--------------------------------------------------------------------------------
/projects/lib/src/configurable/configurable.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectorRef,
3 | Directive,
4 | ElementRef,
5 | HostBinding,
6 | HostListener,
7 | Input,
8 | OnDestroy,
9 | OnInit,
10 | Renderer2,
11 | } from '@angular/core';
12 | import { filter, fromEvent, map, merge, Subscription } from 'rxjs';
13 | import { ZoneContextService } from '../dynamic-views/zone/zone-context.service';
14 |
15 | import { Configurable, ConfigurableService } from './configurable.service';
16 |
17 | @Directive({
18 | selector: '[uib-configurable]',
19 | standalone: true
20 | })
21 | export class ConfigurableDirective implements OnInit, OnDestroy {
22 | /**
23 | * Element's id (not zone's id)
24 | */
25 | @Input() id: string;
26 | /**
27 | * Parent's element id
28 | */
29 | @Input() parentId: string;
30 | /**
31 | * When zone's data exists, which index is used to display the relative data's informations
32 | */
33 | @Input() dataIndex?: number;
34 | @Input("uib-disable-if") disableIf?: any;
35 |
36 | nativeElement: HTMLElement;
37 |
38 | private subscription: Subscription;
39 |
40 | constructor(
41 | public zoneRef: ZoneContextService,
42 | private configurableService: ConfigurableService,
43 | private renderer: Renderer2,
44 | private cdr: ChangeDetectorRef,
45 | { nativeElement }: ElementRef
46 | ) {
47 | this.nativeElement = nativeElement;
48 | }
49 |
50 | ngOnInit(): void {
51 | this.configurableService.configurableDirectiveMap.set(this.id, this);
52 |
53 | this.subscription = merge(
54 | fromEvent(this.nativeElement, "mouseover"),
55 | fromEvent(this.nativeElement, "mouseenter"),
56 | fromEvent(this.nativeElement, "mouseleave")
57 | ).pipe(
58 | filter(() => !this.disableIf),
59 | map(x => x?.type)
60 | ).subscribe(type => {
61 | switch (type) {
62 | case "mouseover":
63 | this.configurableService.mouseoverConfigurable(this.id);
64 | break;
65 | case "mouseenter":
66 | this.configurableService.mouseenterConfigurable(this.id);
67 | break;
68 | case "mouseleave":
69 | this.configurableService.mouseleaveConfigurable();
70 | break;
71 | }
72 | this.cdr.markForCheck();
73 | })
74 | }
75 |
76 | ngOnDestroy(): void {
77 | this.subscription.unsubscribe();
78 | }
79 |
80 | @HostBinding('class')
81 | get _class() {
82 | if(this.disableIf) return '';
83 | return `uib-configurable ${this.highlight() ? 'highlight' : ''}`;
84 | }
85 |
86 | @HostListener('click', ['$event'])
87 | click(event: MouseEvent) {
88 | if(this.disableIf) return;
89 | event.stopPropagation();
90 |
91 | // before to set 'edited' class to current element
92 | // send it to configurableService to update the previous element and call it's removeEdited() method
93 | const conf: Configurable = {
94 | id: this.id,
95 | parentId: this.parentId,
96 | dataIndex: this.dataIndex,
97 | zone: this.zoneRef.id,
98 | templates: this.zoneRef.templates,
99 | data: this.zoneRef.data,
100 | conditionsData: this.zoneRef.conditionsData,
101 | removeEdited: this.removeEdited.bind(this),
102 | removeSelected: this.removeSelected.bind(this)
103 | };
104 | this.configurableService.clickConfigurable(conf);
105 |
106 | // now, previous 'edited' class should be correct,
107 | // we can safely set the 'edited' class to the current element
108 | if (this.nativeElement.classList.contains('edited')) {
109 | this.removeEdited();
110 | this.removeSelected();
111 | } else {
112 | if (this.zoneRef.id) {
113 | const el = this.configurableService.configurableDirectiveMap.get(this.zoneRef.id);
114 | el?.nativeElement.setAttribute("selected","")
115 | }
116 |
117 | this.renderer.addClass(this.nativeElement, 'edited');
118 | }
119 | }
120 |
121 | /**
122 | * removes the `edited` class from the `nativeElement`
123 | */
124 | removeEdited() {
125 | this.renderer.removeClass(this.nativeElement, 'edited');
126 | }
127 |
128 | /**
129 | * removes the `selected` class from the `nativeElement`
130 | */
131 | removeSelected() {
132 | if (this.zoneRef.id) {
133 | const el = this.configurableService.configurableDirectiveMap.get(this.zoneRef.id);
134 | el?.nativeElement.removeAttribute("selected");
135 | }
136 | }
137 |
138 | /**
139 | * removes the `highlight` class from the `nativeElement`
140 | */
141 | removeHighlight() {
142 | this.renderer.removeClass(this.nativeElement, 'highlight');
143 | }
144 |
145 | /**
146 | * adds the `highlight` class to the `nativeElement`
147 | */
148 | addHighlight() {
149 | this.renderer.addClass(this.nativeElement, 'highlight');
150 | }
151 |
152 | /**
153 | * If the id of the current instance of the component is the same as the id of
154 | * the hovered component, then return true
155 | * @returns The id of the current item is being compared to the hoveredId of the
156 | * configurableService.
157 | */
158 | highlight() {
159 | return this.id === this.configurableService.hoveredId;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/projects/lib/src/configurable/configurable.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { BehaviorSubject, Observable } from 'rxjs';
3 | import { filter } from 'rxjs/operators';
4 | import { TemplateNameDirective } from '../utils/directive/template-name.directive';
5 | import {ConfigurableDirective} from './configurable.directive';
6 |
7 | export interface Configurable {
8 | id: string;
9 | parentId: string;
10 | zone: string;
11 | templates?: Record;
12 | data?: any;
13 | dataIndex?: number;
14 | conditionsData?: Record;
15 | removeEdited: () => void;
16 | removeSelected: () => void;
17 | removeHighlight?: () => void;
18 | addHighlight?: () => void;
19 | highlight?: () => void;
20 | }
21 |
22 | @Injectable({ providedIn: 'root' })
23 | export class ConfigurableService {
24 | private _hoveredId?: string;
25 |
26 | /**
27 | * currently edited element'id.
28 | * Can be undefined when element is unselected
29 | */
30 | edited$ = new BehaviorSubject(undefined);
31 |
32 | /**
33 | * behavior subject as we need to retrieve the previous value to toggle it
34 | */
35 | editorEnabled$ = new BehaviorSubject(false);
36 |
37 | /**
38 | * previous edited element
39 | */
40 | previousConfigurableElement?: Configurable;
41 |
42 | /**
43 | * configurable service must subscribe to store'changes events (config service)
44 | */
45 | configurableDirectiveMap: Map = new Map();
46 |
47 | set hoveredId(id: string | undefined) {
48 | this._hoveredId = id;
49 | }
50 | get hoveredId(): string | undefined {
51 | return this._hoveredId;
52 | }
53 |
54 | /**
55 | * Set the hover's id element when not undefined
56 | * @param {string | undefined} id - The id of the configurable element
57 | */
58 | mouseoverConfigurable(id: string | undefined) {
59 | if (this._hoveredId === undefined) {
60 | this._hoveredId = id;
61 | }
62 | }
63 |
64 | /**
65 | * Set the hover's id element when different from the current hovered element
66 | * @param id {string | undefined} id - The id of the configurable element
67 | */
68 | mouseenterConfigurable(id: string | undefined) {
69 | if (id !== this.hoveredId) {
70 | this._hoveredId = id;
71 | }
72 | }
73 |
74 | /**
75 | * When mouse leave configurable element, set hover's id to undefined
76 | */
77 | mouseleaveConfigurable() {
78 | this._hoveredId = undefined;
79 | }
80 |
81 | clickConfigurable(configurable: Configurable) {
82 | if (!this.previousConfigurableElement) {
83 | // previous is undefined
84 | this.previousConfigurableElement = configurable;
85 | }
86 | else if (this.previousConfigurableElement.id !== configurable.id
87 | || (this.previousConfigurableElement.id === configurable.id && this.previousConfigurableElement.zone !== configurable.zone)) {
88 | // previous element exist and his id don't match with the new configurable element
89 |
90 | this.previousConfigurableElement.removeEdited();
91 | this.previousConfigurableElement.removeSelected();
92 | this.previousConfigurableElement = configurable;
93 | }
94 | else if (this.previousConfigurableElement.id === configurable.id && this.previousConfigurableElement.zone === configurable.zone) {
95 | // same id and same zone
96 | this.previousConfigurableElement = undefined;
97 | this.edited$.next(undefined);
98 | return;
99 | }
100 |
101 | this.edited$.next(configurable);
102 | }
103 |
104 | stopEditing() {
105 | this.previousConfigurableElement?.removeEdited();
106 | this.previousConfigurableElement?.removeSelected();
107 | this.previousConfigurableElement = undefined;
108 | this.edited$.next(undefined);
109 | }
110 |
111 | watchEdited(): Observable {
112 | return this.edited$.pipe(filter(this.isConfigurable));
113 | }
114 |
115 | isConfigurable = (configurable: Configurable | undefined): configurable is Configurable => {
116 | return !!configurable;
117 | }
118 |
119 | toggleEditor() {
120 | const enabled = !this.editorEnabled$.value;
121 | this.editorEnabled$.next(enabled);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/projects/lib/src/configurable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
--------------------------------------------------------------------------------
/projects/lib/src/configurable/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './configurable.directive';
2 | export * from './configurable.service';
3 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.actions.ts:
--------------------------------------------------------------------------------
1 | import { createAction, props } from "@ngrx/store";
2 | import { ComponentConfig } from "./config.model";
3 |
4 | /**
5 | * Initialize the configuration
6 | */
7 | export const initConfig = createAction('INIT', props<{config: ComponentConfig[]}>());
8 |
9 | /**
10 | * Remove the current configuration and set a new one
11 | */
12 | export const setConfig = createAction('SET', props<{config: ComponentConfig[]}>());
13 |
14 | /**
15 | * Add a new configuration item
16 | */
17 | export const addConfig = createAction('ADD', props<{config: ComponentConfig}>());
18 |
19 | /**
20 | * Remove an existing configuration item
21 | */
22 | export const removeConfig = createAction('REMOVE', props<{id: string}>());
23 |
24 | /**
25 | * Update an existing configuration item (or list of items)
26 | */
27 | export const updateConfig = createAction('UPDATE', props<{config: ComponentConfig | ComponentConfig[]}>());
28 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.model.ts:
--------------------------------------------------------------------------------
1 | import { Condition } from "../conditions";
2 |
3 | type StringWithAutocomplete = T | (string & Record);
4 |
5 | /**
6 | * An object containing the configuration of a component.
7 | */
8 | export type ComponentConfig = {
9 | /** Unique string identifying the component */
10 | readonly id: string,
11 | /** The type is should correspond to a template injected in a
12 | uib-zone component, so that this component can be displayed */
13 | type: StringWithAutocomplete<'_container' | '_raw'>,
14 | /** Optional list of CSS class injected in the div
15 | wrapping the template of this component */
16 | classes?: string,
17 | /** Any parameter needed to display the component */
18 | [key: string]: any;
19 | /** An optional condition to display the component only when
20 | some conditions are met */
21 | condition?: Condition,
22 | /** Any custom display name */
23 | display?: string,
24 | }
25 |
26 | /**
27 | * A specific type of object configuration for "containers", including
28 | * the list of items that this container contains.
29 | */
30 | export interface ContainerConfig extends ComponentConfig {
31 | type: '_container';
32 | /** List of component ids displayed in this container */
33 | items: string[];
34 | }
35 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 | import { StoreModule } from "@ngrx/store";
3 | import { uibConfig } from "./config.reducer";
4 |
5 | @NgModule({imports: [StoreModule.forFeature(uibConfig)]})
6 | export class ConfigModule {}
7 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.reducer.ts:
--------------------------------------------------------------------------------
1 | import { createFeature, on } from "@ngrx/store";
2 | import { initialUndoRedoState, undoRedo, UndoRedoState } from "ngrx-wieder";
3 | import { addConfig, initConfig, removeConfig, setConfig, updateConfig } from "./config.actions";
4 | import { ComponentConfig } from "./config.model";
5 |
6 | export interface ConfigState extends UndoRedoState {
7 | config: {[id: string]: ComponentConfig};
8 | }
9 |
10 | const { createUndoRedoReducer } = undoRedo({
11 | allowedActionTypes: [addConfig.type, removeConfig.type, updateConfig.type],
12 | maxBufferSize: Number.MAX_SAFE_INTEGER
13 | });
14 |
15 | function storeConfig(state: ConfigState, config: ComponentConfig[]) {
16 | for(let c of config) {
17 | state.config[c.id] = c;
18 | }
19 | return state;
20 | }
21 |
22 | const reducer = createUndoRedoReducer(
23 | {config: {}, ...initialUndoRedoState},
24 |
25 | on(initConfig, (state, {config}) => storeConfig(state, config)),
26 |
27 | on(setConfig, (state, {config}) => {
28 | // Clear current state
29 | for(let key of Object.keys(state)) {
30 | delete state[key];
31 | }
32 | // Add new state
33 | return storeConfig(state, config);
34 | }),
35 |
36 | on(addConfig, (state, {config}) => {
37 | if(state.config[config.id]) {
38 | throw new Error(`Config ${config.id} already exists.`);
39 | }
40 | return storeConfig(state, [config]);
41 | }),
42 |
43 | on(removeConfig, (state, {id}) => {
44 | delete state.config[id];
45 | return state;
46 | }),
47 |
48 | on(updateConfig, (state, {config}) => {
49 | if(!Array.isArray(config)) config = [config];
50 | return storeConfig(state, config);
51 | })
52 | );
53 |
54 |
55 | export const uibConfig = createFeature({ name: 'uibConfig', reducer });
56 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.selectors.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from "@ngrx/store";
2 | import { createHistorySelectors } from "ngrx-wieder";
3 | import { uibConfig, ConfigState } from "./config.reducer";
4 |
5 | const selectConfig = uibConfig.selectUibConfigState;
6 |
7 | export const selectAll = createSelector(selectConfig, (s: ConfigState) => Object.values(s.config));
8 | export const selectItem = (id: string) => createSelector(selectConfig, (s: ConfigState) => s.config[id]);
9 | export const {
10 | selectCanUndo,
11 | selectCanRedo,
12 | } = createHistorySelectors(selectConfig);
13 |
14 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import { Observable } from 'rxjs';
4 | import { filter, map, take } from 'rxjs/operators';
5 | import { initConfig, removeConfig, setConfig, updateConfig } from './config.actions';
6 | import { ComponentConfig, ContainerConfig } from './config.model';
7 | import { selectAll, selectCanRedo, selectCanUndo, selectItem } from './config.selectors';
8 |
9 | @Injectable({ providedIn: 'root' })
10 | export class ConfigService {
11 |
12 | constructor(
13 | public store: Store
14 | ){}
15 |
16 | /**
17 | * Initialize configuration assuming the store is empty.
18 | * No object is removed and the state history is not modified.
19 | * @param config list of configuration items to manage
20 | */
21 | public init(config: ComponentConfig[]) {
22 | this.store.dispatch(initConfig({config}));
23 | }
24 |
25 | /**
26 | * Set the configuration assuming the store is not empty.
27 | * All previously existing objects are removed and the state
28 | * history is reset.
29 | * @param config
30 | */
31 | public set(config: ComponentConfig[]) {
32 | this.store.dispatch({type: 'CLEAR'}); // Clear the previous history, as the previous actions won't work on new objects
33 | this.store.dispatch(setConfig({config}));
34 | }
35 |
36 | /**
37 | * Watch any change in the configuration objects.
38 | */
39 | public watchAllConfig(): Observable {
40 | return this.store.select(selectAll);
41 | }
42 |
43 | /**
44 | * @returns all current configuration
45 | */
46 | public getAllConfig(): ComponentConfig[] {
47 | let config: ComponentConfig[] = [];
48 | this.watchAllConfig().pipe(take(1)).subscribe(c => config = c);
49 | return config;
50 | }
51 |
52 | /**
53 | * Watch changes of one specific configuration object
54 | */
55 | public watchConfig(id: string): Observable {
56 | this.getConfig(id); // Ensure a value exists (if 'id' has no config)
57 | return this.store.select(selectItem(id)).pipe(
58 | filter(config => !!config),
59 | map((config: ComponentConfig) => JSON.parse(JSON.stringify(config)))
60 | );
61 | }
62 |
63 | private _getConfig(id: string): ComponentConfig | undefined {
64 | let config: ComponentConfig | undefined;
65 | this.store.select(selectItem(id))
66 | .pipe(take(1))
67 | .subscribe(c => config = c);
68 | return config;
69 | }
70 |
71 | /**
72 | * @returns the current configuration of a specific item with the given id
73 | */
74 | public getConfig(id: string): ComponentConfig {
75 | let config = this._getConfig(id);
76 | if (!config) {
77 | config = { id, type: id };
78 | this.store.dispatch(initConfig({config: [config]})); // Use init instead of add, because add is undoable
79 | }
80 | return JSON.parse(JSON.stringify(config)); // Deep copy
81 | }
82 |
83 | /**
84 | * @returns the current configuration of a specific container item with the given id
85 | */
86 | public getContainer(id: string): ContainerConfig {
87 | const config = this.getConfig(id);
88 | if (!this.isContainerConfig(config)) {
89 | throw `${id} is not a container`;
90 | }
91 | return config;
92 | }
93 |
94 | /**
95 | * @returns true if the configuration with the given id is a container
96 | */
97 | public isContainer(id: string): boolean {
98 | return this.isContainerConfig(this._getConfig(id));
99 | }
100 |
101 | /**
102 | * @returns true if the given configuration is a container
103 | */
104 | public isContainerConfig(conf: ComponentConfig|undefined): conf is ContainerConfig {
105 | return conf?.type === '_container';
106 | }
107 |
108 | /**
109 | * Test whether a given component id is used within the hierarchy of a container
110 | */
111 | public isUsed(id: string) {
112 | return !!this.findParent(id);
113 | }
114 |
115 | /**
116 | * @returns the configuration of a container that includes the given id as a child item
117 | */
118 | public findParent(id: string): ContainerConfig | undefined {
119 | return this.getAllConfig()
120 | .find(item => this.isContainerConfig(item) && item.items.includes(id)) as ContainerConfig | undefined;
121 | }
122 |
123 | /**
124 | * Update the configuration of a given component or list of components
125 | */
126 | public updateConfig(config: ComponentConfig | ComponentConfig[]) {
127 | this.store.dispatch(updateConfig({config}));
128 | }
129 |
130 | /**
131 | * Remove the configuration of a component with the given id
132 | */
133 | public removeConfig(id: string) {
134 | this.store.dispatch(removeConfig({id}));
135 | }
136 |
137 | /**
138 | * @returns a new unique component id for the given component type
139 | */
140 | public generateId(type: string) {
141 | let idx = 1;
142 | let root = type.startsWith("_")? type.slice(1) : type;
143 | const tokens = type.split("-");
144 | if(tokens[tokens.length-1].match(/\d+/)) {
145 | idx = +tokens[tokens.length-1];
146 | root = tokens.slice(0, tokens.length-1).join('-');
147 | }
148 | let id = root;
149 | do {
150 | id = `${root}-${idx++}`;
151 | } while (this._getConfig(id) || id === type);
152 | return id;
153 | }
154 |
155 | /**
156 | * @returns an observable state for the possibility of undoing the last action
157 | */
158 | public canUndo$(): Observable {
159 | return this.store.select(selectCanUndo());
160 | }
161 |
162 | /**
163 | * @returns an observable state for the possibility of redoing the next action
164 | */
165 | public canRedo$(): Observable {
166 | return this.store.select(selectCanRedo());
167 | }
168 |
169 | /**
170 | * Undo the last action if possible
171 | */
172 | public undo() {
173 | this.canUndo$().pipe(take(1)).subscribe(
174 | can => can && this.store.dispatch({type: "UNDO"})
175 | );
176 | }
177 |
178 | /**
179 | * Redo the next action if possible
180 | */
181 | public redo() {
182 | this.canRedo$().pipe(take(1)).subscribe(
183 | can => can && this.store.dispatch({type: "REDO"})
184 | );
185 | }
186 |
187 | public exportConfiguration() {
188 | const config = JSON.stringify(this.getAllConfig(), null, 2);
189 | var element = document.createElement('a');
190 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config));
191 | element.setAttribute('download', "config.json");
192 | element.style.display = 'none';
193 | document.body.appendChild(element);
194 | element.click();
195 | document.body.removeChild(element);
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/projects/lib/src/configuration/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
--------------------------------------------------------------------------------
/projects/lib/src/configuration/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './config.service';
2 | export * from './config.model';
3 | export * from './config.module';
4 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/checkbox/checkbox.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core";
2 | import { FormsModule } from "@angular/forms";
3 |
4 | import { ConfiguratorContext } from "../configurator.models";
5 |
6 | @Component({
7 | selector: 'uib-checkbox',
8 | imports: [FormsModule],
9 | standalone: true,
10 | template: `
11 |
12 |
13 |
14 |
15 | `
16 | })
17 | export class CheckboxComponent implements OnChanges {
18 | @Input() context: ConfiguratorContext;
19 | @Input() property: string;
20 | @Input() label?: string;
21 |
22 | @Output() modelChanged = new EventEmitter();
23 |
24 | _path: string[];
25 |
26 | get model(): boolean {
27 | let val = this.context.config;
28 | for (let p of this._path) {
29 | val = val[p];
30 | }
31 | return !!val;
32 | }
33 |
34 | set model(value: boolean) {
35 | let val = this.context.config;
36 | let i = 0;
37 | for (; i < this._path.length - 1; i++) {
38 | val = val[this._path[i]];
39 | }
40 | if (value !== val[this._path[i]]) {
41 | val[this._path[i]] = value;
42 | this.context.configChanged();
43 | this.modelChanged.emit(value);
44 | }
45 | }
46 |
47 | ngOnChanges(changes: SimpleChanges): void {
48 | this._path = this.property.split('.');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/color-picker/color-picker.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
2 | import { FormsModule } from "@angular/forms";
3 |
4 | import { ConfiguratorContext } from "../configurator.models";
5 | import { NgModelChangeDebouncedDirective, TooltipDirective } from "../../utils";
6 | import { CommonModule } from "@angular/common";
7 |
8 | @Component({
9 | selector: 'uib-color-picker',
10 | imports: [CommonModule, FormsModule, TooltipDirective, NgModelChangeDebouncedDirective],
11 | standalone: true,
12 | template: `
13 |
14 |
15 |
21 |
22 |
23 | `,
24 | styles: [`
25 | :host {
26 | width: 100px;
27 | }
28 |
29 | .color-picker-wrapper {
30 | overflow: hidden;
31 | width: 48px;
32 | height: 48px;
33 | border-radius: 50%;
34 | box-shadow: 1px 1px 3px 0px grey;
35 | margin: auto;
36 | }
37 |
38 | .color-picker-wrapper input[type=color] {
39 | width: 150%;
40 | /* height: 150%; */
41 | padding: 0;
42 | margin: -25%;
43 | }
44 |
45 | .color-picker-reset {
46 | position: relative;
47 | margin: 0;
48 | padding: 0;
49 | width: 125%;
50 | height: 60%;
51 | top: -10px;
52 | left: -5px;
53 | }
54 | `]
55 | })
56 | export class ColorPickerComponent implements OnChanges {
57 | @Input() context: ConfiguratorContext;
58 | @Input() property: string;
59 | @Input() label?: string;
60 | @Input() defaultColor = '#ffffff';
61 | @Input() tooltip: string = '';
62 |
63 | _path: string[];
64 |
65 | get color() {
66 | let val = this.context.config;
67 | for(let p of this._path) {
68 | val = val[p];
69 | }
70 | return val || this.defaultColor;
71 | }
72 |
73 | set color(value: string) {
74 | if(value.toLowerCase() === this.defaultColor.toLowerCase()) {
75 | value = '';
76 | }
77 | let val = this.context.config;
78 | let i = 0;
79 | // walk through nested object's properties
80 | // at the for loop end, val will contains the seeked ref property
81 | for(; i
2 |
3 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{description}}
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 | Component palette
39 |
44 |
45 |
46 |
47 |
48 | Layout
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
74 | Conditional display
75 |
76 |
77 |
78 |
79 |
87 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/configurator.component.scss:
--------------------------------------------------------------------------------
1 | .offcanvas-body {
2 | padding-bottom: 90px; /* avoid toolbar hiding bottom of configurator */
3 | }
4 |
5 | .ltr > svg-icon {
6 | transform: rotate(180deg) translateY(-4px);
7 | }
--------------------------------------------------------------------------------
/projects/lib/src/configurator/configurator.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | ChangeDetectorRef,
4 | Component,
5 | ContentChildren,
6 | ElementRef,
7 | Input,
8 | OnInit,
9 | QueryList,
10 | ViewChild,
11 | } from '@angular/core';
12 | import { Offcanvas } from 'bootstrap';
13 | import { switchMap, map, tap } from 'rxjs/operators';
14 | import { Observable } from 'rxjs';
15 | import { Configurable, ConfigurableService } from '../configurable/configurable.service';
16 | import { ComponentConfig, ConfigService, ContainerConfig } from '../configuration';
17 | import { Mutable } from '../utils/typings';
18 | import { PaletteComponent, defaultPaletteOptions } from './palette/palette.component';
19 | import { ConfiguratorContext, ConfiguratorOptions } from './configurator.models';
20 | import { TemplateNameDirective } from '../utils/directive/template-name.directive';
21 | import { CommonModule } from '@angular/common';
22 | import { SvgIconComponent, TooltipDirective } from '../utils';
23 | import { ClassEditorComponent, ConditionEditorComponent, FlexEditorComponent, HtmlEditorComponent, SpacingEditorComponent } from './editors';
24 | import { TreeComponent } from './public-api';
25 |
26 | export const defaultConfiguratorOptions: ConfiguratorOptions = {
27 | paletteOptions: defaultPaletteOptions,
28 | showFlexEditor: true,
29 | showHtmlEditor: true,
30 | showCssClasses: true,
31 | showSpacingEditor: true,
32 | showConditionalDisplay: true,
33 | showRemove: true,
34 | showDuplicate: true
35 | }
36 |
37 | @Component({
38 | selector: 'uib-configurator',
39 | standalone: true,
40 | imports: [
41 | CommonModule,
42 | TooltipDirective,
43 | SvgIconComponent,
44 | TreeComponent,
45 | HtmlEditorComponent,
46 | FlexEditorComponent,
47 | PaletteComponent,
48 | ClassEditorComponent,
49 | SpacingEditorComponent,
50 | ConditionEditorComponent,
51 | ],
52 | templateUrl: './configurator.component.html',
53 | styleUrls: ['./configurator.component.scss'],
54 | changeDetection: ChangeDetectionStrategy.OnPush,
55 | })
56 | export class ConfiguratorComponent implements OnInit {
57 | // Capture configurator templates
58 | @ContentChildren(TemplateNameDirective)
59 | children: QueryList;
60 | configurators: Record = {};
61 |
62 | @ViewChild('offcanvas') offcanvasEl: ElementRef;
63 | offcanvas: Offcanvas;
64 |
65 | @ViewChild('offcanvasBody') offcanvasBodyEl:ElementRef;
66 |
67 | @Input() options = defaultConfiguratorOptions;
68 | @Input() zoneOptions: Record = {};
69 |
70 | edited$: Observable;
71 |
72 | configuration: ComponentConfig[] = [];
73 |
74 | isTree: boolean;
75 | ltr = false;
76 | parentId: string;
77 |
78 | constructor(
79 | private cdr: ChangeDetectorRef,
80 | public configurableService: ConfigurableService,
81 | public configService: ConfigService
82 | ) {}
83 |
84 | ngOnInit(): void {
85 | this.edited$ = this.configurableService.watchEdited().pipe(
86 | tap(() => this.offcanvas.show()),
87 | tap(() => this.showTree(false)),
88 | tap((edited) => this.parentId = edited.parentId),
89 | switchMap((context) =>
90 | this.configService.watchConfig(context!.id).pipe(
91 | map(config => ({
92 | context,
93 | config,
94 | options: this.resolveOptions(context.zone),
95 | configurators: this.configurators,
96 | configChanged: () => this.configService.updateConfig(config)
97 | }))
98 | )
99 | )
100 | );
101 |
102 | // subscribe to configuration events
103 | this.configService.watchAllConfig().subscribe(config => {
104 | this.configuration = config!;
105 | this.cdr.markForCheck();
106 | });
107 |
108 | // when edition is disabled, close side panel
109 | this.configurableService.editorEnabled$.subscribe(value => {
110 | if (value === false && this.offcanvas) {
111 | this.offcanvas.hide();
112 | }
113 | });
114 | }
115 |
116 | /**
117 | * Create Bootstrap OffCanvas component
118 | */
119 | ngAfterViewInit() {
120 | this.offcanvas = Offcanvas.getOrCreateInstance(this.offcanvasEl.nativeElement, {
121 | backdrop: false,
122 | scroll: true
123 | });
124 | this.offcanvasEl.nativeElement.addEventListener('hide.bs.offcanvas', _ => {
125 | this.configurableService.stopEditing();
126 | });
127 | }
128 |
129 | /**
130 | * Extract list of configuration editors
131 | */
132 | ngAfterContentInit() {
133 | this.children.forEach(
134 | template => (this.configurators[template.name] = template)
135 | );
136 | }
137 |
138 | showTree(showTree = true) {
139 | this.isTree = showTree;
140 | this.offcanvasBodyEl.nativeElement.scroll(0, 0);
141 | }
142 |
143 | resolveOptions(zone: string) {
144 | // First set defaults, then the configurator options, then zone-specific options
145 | const options = Object.assign({}, defaultConfiguratorOptions, this.options, this.zoneOptions[zone] || {});
146 | // Same thing for the nested palette options
147 | options.paletteOptions = Object.assign({}, defaultPaletteOptions, this.options.paletteOptions, this.zoneOptions[zone]?.paletteOptions || {});
148 | return options;
149 | }
150 |
151 | /**
152 | * It removes the item from the parent container.
153 | * @param {Event} event - Event
154 | */
155 | remove(context: Configurable) {
156 | // only uib-zone cannot self remove
157 | if (context.parentId) {
158 | const container = this.configService.getContainer(context.parentId);
159 | const index = container.items.findIndex(item => item === context.id);
160 | if (index !== -1) {
161 | container.items.splice(index, 1);
162 | this.configService.updateConfig([container]);
163 | this.offcanvas.toggle();
164 | }
165 | }
166 | }
167 |
168 | duplicate(context: Configurable) {
169 | const config: Mutable = this.configService.getConfig(context.id);
170 | config.id = this.configService.generateId(config.id); // Generate a new config id
171 | if(context.parentId) {
172 | const container = this.configService.getContainer(context.parentId);
173 | const index = container.items.findIndex(item => item === context.id);
174 | if (index !== -1) {
175 | container.items.splice(index+1, 0, config.id);
176 | this.configService.updateConfig([config, container]);
177 | }
178 | }
179 | // Special case of a zone
180 | else if(context.zone === context.id) {
181 | // Create another copy
182 | const config2: Mutable = this.configService.getConfig(context.id);
183 | config2.id = this.configService.generateId(config.id);
184 |
185 | const container: ContainerConfig = {
186 | id: context.id,
187 | type: '_container',
188 | items: [config.id, config2.id],
189 | classes: "flex-column",
190 | };
191 |
192 | this.configService.updateConfig([config, config2, container]);
193 | }
194 | }
195 |
196 | goToParent() {
197 | const el = this.configurableService.configurableDirectiveMap.get(this.parentId);
198 | el?.click(new MouseEvent("click"));
199 | el?.nativeElement.scrollIntoView({behavior: 'smooth', inline: 'nearest', block: 'center'});
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/configurator.models.ts:
--------------------------------------------------------------------------------
1 | import { Configurable } from "../configurable";
2 | import { ComponentConfig } from "../configuration";
3 | import { TemplateNameDirective } from "../utils";
4 |
5 | export interface ConfiguratorOptions {
6 | paletteOptions?: PaletteOptions;
7 | showFlexEditor?: boolean;
8 | showHtmlEditor?: boolean;
9 | showCssClasses?: boolean;
10 | showSpacingEditor?: boolean;
11 | showConditionalDisplay?: boolean;
12 | showRemove?: boolean;
13 | showDuplicate?: boolean;
14 | }
15 |
16 | export interface PaletteOptions {
17 | enableSubcontainers?: boolean;
18 | enableRawHtml?: boolean;
19 | rawHtmlPlaceholder?: string;
20 | showStandardPalette?: boolean;
21 | showExistingPalette?: boolean;
22 | }
23 |
24 | export interface ConfiguratorContext {
25 | /** Object storing the configuration of the component */
26 | config: ComponentConfig;
27 | /** Options of the configurators (may change depending on zone) */
28 | options: ConfiguratorOptions;
29 | /** Register of all the components configurators */
30 | configurators: Record;
31 | /** Context of the zone of the edited component */
32 | context: Configurable;
33 | /** Callback that the configurator should call to update the configuration */
34 | configChanged: () => void;
35 | };
36 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/class-editor.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2 | import { ConfiguratorContext } from '../configurator.models';
3 | import { FormsModule } from '@angular/forms';
4 | import { NgModelChangeDebouncedDirective } from '../../utils';
5 |
6 | @Component({
7 | selector: 'uib-class-editor',
8 | standalone: true,
9 | imports: [FormsModule, NgModelChangeDebouncedDirective],
10 | template: `
11 |
12 |
22 | `,
23 | changeDetection: ChangeDetectionStrategy.OnPush
24 | })
25 | export class ClassEditorComponent {
26 | @Input() context: ConfiguratorContext;
27 | }
28 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/condition-editor.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
46 |
47 |
48 |
1">
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Debug text
56 |
{{debugText}}
57 |
58 |
59 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/condition-editor.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input, OnChanges } from "@angular/core";
2 | import { Condition, ConditionsService, ConditionValue } from "../../conditions/conditions.service";
3 | import { ConfiguratorContext } from "../configurator.models";
4 | import { AutocompleteComponent, NgModelChangeDebouncedDirective } from "../../utils";
5 | import { FormsModule } from "@angular/forms";
6 | import { CommonModule } from "@angular/common";
7 |
8 | @Component({
9 | selector: 'uib-condition-editor',
10 | standalone: true,
11 | imports: [CommonModule, FormsModule, AutocompleteComponent, NgModelChangeDebouncedDirective],
12 | templateUrl: './condition-editor.component.html',
13 | styles: [`
14 | .condition-value .btn {
15 | width: 40px;
16 | }
17 | .autocomplete .list-group {
18 | top: 0;
19 | width: 100%;
20 | max-height: 200px;
21 | overflow: auto;
22 | }
23 | `],
24 | changeDetection: ChangeDetectionStrategy.OnPush
25 | })
26 | export class ConditionEditorComponent implements OnChanges {
27 | @Input() context: ConfiguratorContext;
28 |
29 | // The data on which the current condition is evaluated
30 | data: any;
31 | // The data field on which the current condition is evaluated
32 | fields: string[];
33 | // The list of values tested by the current condition
34 | values: string[];
35 | // A debug text to understand the current condition
36 | debugText: string;
37 |
38 | constructor(
39 | public conditionsService: ConditionsService
40 | ){}
41 |
42 | ngOnChanges() {
43 | this.updateConfig(false);
44 | }
45 |
46 | updateConfig(notify = true) {
47 | if(this.context.config.condition) {
48 | this.updateData(this.context.config.condition);
49 | this.updateFields();
50 | this.updateValues(this.context.config.condition);
51 | this.updateDebugText(this.context.config.condition);
52 | }
53 | if(notify) {
54 | this.context.configChanged();
55 | }
56 | }
57 |
58 | updateData(condition: Condition) {
59 | const data = typeof this.context.context.dataIndex === 'undefined'?
60 | this.context.context.data : this.context.context.data[this.context.context.dataIndex];
61 | this.data = this.conditionsService.selectData(condition, this.context.context.conditionsData, data);
62 | }
63 |
64 | updateFields() {
65 | this.fields = this.data? Object.keys(this.data) : [];
66 | }
67 |
68 | updateValues(condition: Condition) {
69 | this.values = [];
70 | if(condition.field && this.data) {
71 | if(condition.data || typeof this.context.context.dataIndex === 'undefined') {
72 | this.addValueToList(this.values, this.data, condition.field);
73 | }
74 | // Special case of the data list
75 | else {
76 | this.context.context.data.forEach(item => this.addValueToList(this.values, item, condition.field));
77 | }
78 | }
79 | }
80 |
81 | private addValueToList(values: string[], item: any, field: string) {
82 | const data = item[field]?.toString();
83 | if(data && !values.includes(data)) {
84 | values.push(data);
85 | }
86 | }
87 |
88 | updateDebugText(condition: Condition) {
89 | this.debugText = this.conditionsService.writeCondition(condition);
90 | }
91 |
92 | get activate(): boolean {
93 | return !!this.context.config.condition;
94 | }
95 |
96 | set activate(value: boolean) {
97 | if(value) { // Create a new condition from scratch
98 | this.context.config.condition = {data: '', type: 'equals', field: '', values: [{value: ''}]};
99 | }
100 | else { // Erase the current condition
101 | delete this.context.config.condition;
102 | }
103 | this.updateConfig();
104 | }
105 |
106 | addValue() {
107 | this.context.config.condition?.values.push({value: ''});
108 | }
109 |
110 | removeValue(i: number) {
111 | this.context.config.condition?.values.splice(i, 1);
112 | this.updateConfig();
113 | }
114 |
115 | selectField(field: string) {
116 | this.context.config.condition!.field = field;
117 | this.updateConfig();
118 | }
119 |
120 | selectValue(value: string, condition: ConditionValue) {
121 | condition.value = value;
122 | this.updateConfig();
123 | }
124 |
125 | // This item instance might change, so we track by index
126 | trackByFn(index, item) {
127 | return index;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/flex-editor.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Direction
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
{{ name | titlecase }}
28 |
29 |
30 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/flex-editor.component.scss:
--------------------------------------------------------------------------------
1 | .editor-line {
2 | display: grid;
3 | grid-template-columns: 105px 1fr;
4 | align-items: center;
5 | margin-bottom: .5rem;
6 | }
7 |
8 | /* ================ RADIO BUTTONS ================= */
9 |
10 | .radio-group {
11 | // --rotate: 90deg;
12 | --bg-checked: hsl(80, 73%, 75%);
13 | --bg-hover: hsl(80, 61%, 50%);
14 |
15 | display: flex;
16 | align-items: center;
17 | gap: 0.2rem;
18 | }
19 |
20 | .radio-group label {
21 | position: relative;
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 |
26 | width: 36px;
27 | height: 36px;
28 |
29 | > svg {
30 | transform: rotate(var(--rotate, 0deg));
31 | transition: transform 0.2s ease-in-out;
32 | }
33 | }
34 |
35 | .radio-group input[type='radio'] {
36 | display: none;
37 | }
38 |
39 | .radio-group input[type='radio'] + label {
40 | cursor: pointer;
41 | padding: 0.3rem 0.5rem;
42 | border-radius: 4px;
43 | border: 1px dashed #ccc;
44 | transition: background-color 0.2s ease-in-out, fill 0.2s ease-in-out, color 0.2s ease-in-out;
45 | }
46 |
47 | .radio-group input[type='radio']:checked + label {
48 | border: 1px solid #000;
49 | background-color: var(--bg-checked);
50 | }
51 |
52 | .radio-group input[type='radio'] + label:hover {
53 | color: white;
54 | fill: white;
55 | background-color: var(--bg-hover);
56 | }
57 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/flex-editor.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2 | import { ComponentConfig, ConfigService } from '../../configuration';
3 | import { TooltipDirective, TooltipPlacement } from '../../utils';
4 | import { SvgIconComponent } from '../../utils/svg-icon/svg-icon.component';
5 | import { CommonModule } from '@angular/common';
6 |
7 | declare interface FlexOption {
8 | key: string;
9 | text: string;
10 | value: string;
11 | bootstrap: string;
12 | }
13 |
14 | @Component({
15 | selector: 'uib-flex-editor',
16 | standalone: true,
17 | imports: [CommonModule, TooltipDirective, SvgIconComponent],
18 | templateUrl: 'flex-editor.component.html',
19 | styleUrls: ['./flex-editor.component.scss'],
20 | })
21 | export class FlexEditorComponent implements OnChanges {
22 | @Input() config: ComponentConfig;
23 |
24 | // tooltip's placement
25 | placement: TooltipPlacement = "bottom";
26 |
27 | classes: string[];
28 |
29 | get direction(): 'row' | 'column' {
30 | return this.classes.includes('flex-column') ? 'column' : 'row';
31 | }
32 |
33 | directions: FlexOption[] = [
34 | {key: 'direction-right', text: $localize `horizontal`, value: 'row', bootstrap: 'flex-row'},
35 | {key: 'direction-bottom', text: $localize `vertical`, value: 'column', bootstrap: 'flex-column'}
36 | ]
37 |
38 | justify: FlexOption[] = [
39 | {key:'align_x_start', text: $localize `start`, value: 'flex-start', bootstrap: 'justify-content-start'},
40 | {key:'align_x_center', text: $localize `center`, value: 'center', bootstrap: 'justify-content-center'},
41 | {key:'align_x_end', text: $localize `end`, value: 'flex-end', bootstrap: 'justify-content-end'},
42 | {key:'align_x_space_around', text: $localize `space around`, value: 'space-around', bootstrap: 'justify-content-around'},
43 | {key:'align_x_space_between', text: $localize `space between`, value: 'space-between', bootstrap: 'justify-content-between'}
44 | ]
45 |
46 | alignmentHorizontal: FlexOption[] = [
47 | {key: 'align_y_start', text: $localize `top`, value: 'flex-start', bootstrap: 'align-items-start'},
48 | {key: 'align_y_center', text: $localize `center`, value: 'center', bootstrap: 'align-items-center'},
49 | {key: 'align_y_end', text: $localize `bottom`, value: 'flex-end', bootstrap: 'align-items-end'},
50 | {key: 'align_y_stretch', text: $localize `stretch`, value: 'stretch', bootstrap: 'align-items-stretch'},
51 | {key: 'align_y_baseline', text: $localize `baseline`, value: 'baseline', bootstrap: 'align-items-baseline'},
52 | ]
53 |
54 | alignmentVertical: FlexOption[] = [
55 | {key:'align_x_start', text: $localize `start`, value: 'flex-start', bootstrap: 'align-items-start'},
56 | {key:'align_x_center', text: $localize `center`, value: 'center', bootstrap: 'align-items-center'},
57 | {key:'align_x_end', text: $localize `end`, value: 'flex-end', bootstrap: 'align-items-end'},
58 | {key:'align_x_stretch', text: $localize `stretch`, value: 'stretch', bootstrap: 'align-items-stretch'},
59 | ]
60 |
61 | get alignment(): FlexOption[] {
62 | return this.direction === 'column'? this.alignmentVertical : this.alignmentHorizontal;
63 | }
64 |
65 |
66 | constructor(
67 | public configService: ConfigService
68 | ) {}
69 |
70 | ngOnChanges(changes: SimpleChanges): void {
71 | if (changes.config) {
72 | // convert "classes" string into array
73 | this.classes = this.config.classes?.split(' ') || [];
74 | }
75 | }
76 |
77 | private updateConfig() {
78 | this.config.classes = this.classes.join(' ');
79 | this.configService.updateConfig(this.config);
80 | }
81 |
82 | toggleClass(style: string, turnOff: FlexOption[] = []) {
83 | const i = this.classes.indexOf(style);
84 | // Remove all classes matching a flex option
85 | this.classes = this.classes.filter(c => c !== style && !turnOff.find(o => o.bootstrap === c));
86 | // Then add the class, unless it was already there (in which case, the filter turned it off)
87 | if(i === -1) {
88 | this.classes.push(style);
89 | }
90 | this.updateConfig();
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/html-editor.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from "@angular/core";
2 | import { ConfiguratorContext } from "../configurator.models";
3 | import { FormsModule } from "@angular/forms";
4 | import { NgModelChangeDebouncedDirective } from "../../utils";
5 |
6 | @Component({
7 | selector: 'uib-html-editor',
8 | standalone: true,
9 | imports: [FormsModule, NgModelChangeDebouncedDirective],
10 | template: `
11 |
12 | Static HTML content (text, images). Does not support Angular component and syntax.
13 |
20 | `,
21 | styles: [`
22 | textarea {
23 | font-family: monospace;
24 | font-size: 12px !important;
25 | }
26 | `]
27 | })
28 | export class HtmlEditorComponent {
29 | @Input() context: ConfiguratorContext;
30 |
31 | }
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/index.ts:
--------------------------------------------------------------------------------
1 | export * from './class-editor.component';
2 | export * from './condition-editor.component';
3 | export * from './flex-editor.component';
4 | export * from './html-editor.component';
5 | export * from './spacing-editor.component';
6 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/editors/spacing-editor.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, QueryList, Renderer2, SimpleChanges, ViewChildren } from "@angular/core";
2 | import { ComponentConfig, ConfigService } from "../../configuration";
3 | import { TooltipDirective } from "../../utils";
4 | import { SvgIconComponent } from "../../utils/svg-icon/svg-icon.component";
5 | import { CommonModule } from "@angular/common";
6 |
7 | @Component({
8 | selector: 'uib-bs-spacing-editor',
9 | standalone: true,
10 | imports: [CommonModule, TooltipDirective, SvgIconComponent],
11 | template: `
12 |
13 | Spacing and borders
14 |
32 |
33 | `,
34 | styleUrls: ['./flex-editor.component.scss'],
35 | changeDetection: ChangeDetectionStrategy.OnPush
36 | })
37 | export class SpacingEditorComponent implements AfterViewInit, OnChanges {
38 | @Input() config: ComponentConfig;
39 |
40 | @ViewChildren("details") details: QueryList>;
41 |
42 | componentId: string;
43 |
44 | classes: string[];
45 | state: {[type: string]: {index: number, direction: string|undefined, magnitude: string|undefined}};
46 |
47 | directions = {
48 | name: "direction" as const,
49 | title: $localize `Direction`,
50 | options: [
51 | {description: $localize `All around`, code: "", icon: "direction-left-right-top-bottom"},
52 | {description: $localize `On the left side`, code: "s", icon: "direction-left"},
53 | {description: $localize `On the right side`, code: "e", icon: "direction-right"},
54 | {description: $localize `On the top side`, code: "t", icon: "direction-top"},
55 | {description: $localize `On the bottom side`, code: "b", icon: "direction-bottom"},
56 | {description: $localize `On the left and right`, code: "x", icon: "direction-left-right"},
57 | {description: $localize `On the top and bottom`, code: "y", icon: "direction-top-bottom"}
58 | ]
59 | };
60 |
61 | magnitudes = {
62 | name: "magnitude" as const,
63 | title: $localize `Space`,
64 | options: [
65 | {description: $localize `None`, code: "0", icon: "no-space"},
66 | {description: $localize `Extra Small`, code: "1", icon: "space-xs"},
67 | {description: $localize `Small`, code: "2", icon: "space-s"},
68 | {description: $localize `Medium`, code: "3", icon: "space-m"},
69 | {description: $localize `Large`, code: "4", icon: "space-l"},
70 | {description: $localize `Extra Large`, code: "5", icon: "space-xl"}
71 | ]
72 | };
73 |
74 | borderDirections = {
75 | name: "direction" as const,
76 | title: $localize `Direction`,
77 | options: [
78 | {description: $localize `All around`, code: "", icon: "direction-left-right-top-bottom"},
79 | {description: $localize `On the left side`, code: "-start", icon: "direction-left"},
80 | {description: $localize `On the right side`, code: "-end", icon: "direction-right"},
81 | {description: $localize `On the top side`, code: "-top", icon: "direction-top"},
82 | {description: $localize `On the bottom side`, code: "-bottom", icon: "direction-bottom"},
83 | ]
84 | }
85 |
86 | marginMagnitudes = {
87 | ...this.magnitudes,
88 | options: [
89 | ...this.magnitudes.options,
90 | {description: $localize `Auto`, code: "auto", icon: "auto"}
91 | ]
92 | };
93 |
94 | options = [
95 | {name: "m", title: $localize `Margin`, description: $localize `Margin is the space around the component`, properties: [this.directions, this.marginMagnitudes]},
96 | {name: "p", title: $localize `Padding`, description: $localize `Padding is the space within the component`, properties: [this.directions, this.magnitudes]},
97 | {name: "border", title: $localize `Border`, description: $localize `Border around the component`, properties: [this.borderDirections, this.magnitudes]}
98 | ];
99 |
100 |
101 | constructor(
102 | public configService: ConfigService,
103 | public renderer: Renderer2,
104 | public cdRef: ChangeDetectorRef
105 | ){}
106 |
107 | ngOnChanges(changes: SimpleChanges): void {
108 | if (changes.config) {
109 | this.updateState();
110 | if(this.config.id !== this.componentId) {
111 | this.componentId = this.config.id;
112 | this.updateDetails(); // Update the when a new component is open
113 | }
114 | }
115 | }
116 |
117 | /**
118 | * Initialize the state of the elements upon ViewInit
119 | * (the first call to updateDetails() from ngOnChanges() does not
120 | * yet have access to these elements)
121 | */
122 | ngAfterViewInit(): void {
123 | this.updateDetails();
124 | }
125 |
126 | /**
127 | * Take the input configuration (config.classes) and parse the content
128 | * to feed the state object (which determines the state of each option).
129 | */
130 | updateState() {
131 | this.classes = this.config.classes?.split(' ') || [];
132 | this.state = {};
133 | for(let index=0; index o.code)) {
152 | if(c === `border${direction}`) {
153 | this.state[`border${direction}`] = {index, direction, magnitude: undefined};
154 | }
155 | }
156 | }
157 | }
158 |
159 | /**
160 | * This method opens or closes the elements, depending on the classes
161 | * contained in the configuration
162 | */
163 | updateDetails() {
164 | if(this.details) {
165 | const props = Object.keys(this.state);
166 | for(let detail of this.details) {
167 | let open: boolean = true;
168 | switch(detail.nativeElement.id) {
169 | case "spacing-options": open = props.length > 0; break;
170 | case "m": open = props.includes('m'); break;
171 | case "p": open = props.includes('p'); break;
172 | case "border": open = !!props.find(p => p.startsWith("border")); break;
173 | }
174 | if(open) {
175 | this.renderer.setAttribute(detail.nativeElement, "open", "");
176 | }
177 | else {
178 | this.renderer.removeAttribute(detail.nativeElement, "open")
179 | }
180 | }
181 | this.cdRef.detectChanges();
182 | }
183 | }
184 |
185 | /**
186 | * Returns the activation state of a given property.
187 | * This function is called by the template to set the "checked" state
188 | * of each radio button.
189 | */
190 | isChecked(type: 'p'|'m'|'border'|string, prop: 'direction'|'magnitude', code: string): boolean {
191 | if(type === 'border') {
192 | type = prop === 'magnitude'? `${type}-${prop}` : `${type}${code}`;
193 | }
194 | return this.state[type]?.[prop] === code;
195 | }
196 |
197 | /**
198 | * Toggles the state of a given property.
199 | * This function is called by the template upon a click on a radio button.
200 | */
201 | toggle(type: 'p'|'m'|'border'|string, prop: 'direction'|'magnitude', code: string) {
202 | if(type === 'border') {
203 | type = prop === 'magnitude'? `${type}-${prop}` : `${type}${code}`;
204 | }
205 | const match = this.state[type];
206 | if(match) {
207 | const currentVal = match[prop];
208 | if(currentVal === code) {
209 | this.classes[match.index] = "";
210 | delete this.state[type];
211 | }
212 | else {
213 | match[prop] = code;
214 | }
215 | }
216 | else {
217 | this.state[type] = {index: this.classes.length, direction: '', magnitude: '0'};
218 | this.state[type][prop] = code;
219 | }
220 |
221 | this.updateClasses();
222 | }
223 |
224 | /**
225 | * Update the class list and propagate the update to the config service
226 | */
227 | updateClasses() {
228 | for(const [type,match] of Object.entries(this.state)) {
229 | if(type.startsWith('border')) {
230 | this.classes[match.index] = type === 'border-magnitude' ? `border-${match.magnitude}` : `border${match.direction}`;
231 | }
232 | else {
233 | this.classes[match.index] = `${type}${match.direction}-${match.magnitude}`;
234 | }
235 | }
236 | this.config.classes = this.classes.filter(c => c).join(' ');
237 | this.configService.updateConfig(this.config);
238 | }
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/image-selector/img-selector.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
2 | import { FormsModule } from "@angular/forms";
3 |
4 | import { ConfiguratorContext } from "../configurator.models";
5 | import { NgModelChangeDebouncedDirective } from "../../utils";
6 | import { CommonModule } from "@angular/common";
7 |
8 | @Component({
9 | selector: "uib-image-selector",
10 | imports: [CommonModule, FormsModule, NgModelChangeDebouncedDirective],
11 | standalone: true,
12 | template: `
13 |
67 | `
68 | })
69 | export class ImageSelectorComponent implements OnChanges {
70 | @Input() context: ConfiguratorContext;
71 | @Input() param: string;
72 | @Input() description: string;
73 | @Input() sizeable: boolean = true;
74 |
75 |
76 | ngOnChanges(changes: SimpleChanges): void {
77 | // TODO: to remove with the next release
78 | // convert old format to the new one
79 | if (!this.context.config.images || !this.context.config.images[this.param]) {
80 | this.context.config.images = {...this.context.config.images, [this.param]: { filename: this.context.config[this.param]}}
81 | delete this.context.config[this.param];
82 | }
83 | }
84 |
85 | onImageLoaded(event: Event) {
86 | const file = (event.target as HTMLInputElement).files?.[0];
87 | if (file) {
88 | const reader = new FileReader();
89 | reader.onload = () => {
90 | if (reader.result) {
91 | this.context.config.images[this.param].filename = reader.result;
92 | this.context.configChanged();
93 | }
94 | }
95 | reader.readAsDataURL(file);
96 | }
97 | }
98 |
99 | onImageDimensionChange(value: string, size: 'width' | 'height') {
100 | if (value.trim().length === 0) {
101 | this.context.config.images[this.param][size] = undefined;
102 | }
103 | this.context.configChanged();
104 | }
105 |
106 | onChange(value: string) {
107 | this.context.configChanged();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
2 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/multi-selector/multi-selector.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
27 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/multi-selector/multi-selector.component.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from "@angular/common";
2 | import { Component, forwardRef, HostBinding, Input, OnChanges, SimpleChanges } from "@angular/core";
3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
4 | import { DndDropEvent, DndModule } from "ngx-drag-drop";
5 |
6 | @Component({
7 | selector: "uib-multi-selector",
8 | imports: [CommonModule, DndModule],
9 | standalone: true,
10 | templateUrl: './multi-selector.component.html',
11 | styles: [`
12 | .dndDraggable, .dndDraggable label {
13 | cursor: grab;
14 | }
15 | [dndPlaceholderRef] {
16 | height: 1rem;
17 | background: #ccc;
18 | border: dotted 3px #999;
19 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
20 | }
21 | :host {
22 | max-height: var(--multi-select-max-height);
23 | overflow: auto;
24 | margin-bottom: 0.5rem;
25 | }
26 | `],
27 | providers: [{
28 | provide: NG_VALUE_ACCESSOR,
29 | useExisting: forwardRef(() => MultiSelectComponent),
30 | multi: true
31 | }],
32 | host: {
33 | class: "form-control"
34 | }
35 | })
36 | export class MultiSelectComponent implements OnChanges, ControlValueAccessor {
37 | @Input() options: T[] | undefined;
38 | @Input() enableReorder = false;
39 | @Input() valueField?: string;
40 | @Input() displayField?: string;
41 | @Input() compareWith: (a: T, b:T) => boolean = Object.is;
42 | @Input() @HostBinding("style.--multi-select-max-height") maxHeight = "300px";
43 |
44 | // Copy of options, so we can change the order without affecting the original list of options
45 | // If the options change, the order is reinitialized.
46 | _options: T[] | undefined;
47 | _optionsSelected: (T|null)[] | undefined;
48 |
49 | _values: T[] | undefined;
50 |
51 | static idCpt = 0;
52 | selectId: string;
53 |
54 | constructor(){
55 | this.selectId = `select-${MultiSelectComponent.idCpt++}`;
56 | }
57 |
58 | ngOnChanges(changes: SimpleChanges): void {
59 | if(changes.options) {
60 | this._options = undefined;
61 | this.updateOptions();
62 | }
63 | }
64 |
65 | updateOptions() {
66 | if(this.options) {
67 | this._options = [...this.options];
68 | if(this.enableReorder && this._values) {
69 | this._optionsSelected = this._values.map(val => {
70 | const i = this._options!.findIndex(o => this.compareWith(val, this.getValue(o))); // For each value, find its corresponding option
71 | return i !== -1? this._options!.splice(i, 1)[0] : null; // i === -1 can happen with incomplete list of metadata. null allows the Drag & Drop directive to have the right number of items
72 | });
73 | }
74 | }
75 | }
76 |
77 | getValue(option: T) {
78 | return this.valueField? option[this.valueField] : option;
79 | }
80 |
81 | valueIndex(value: T) {
82 | return this._values?.findIndex(v => this.compareWith(v, value)) ?? -1;
83 | }
84 |
85 | isChecked(option: T): boolean {
86 | if(!this._values) return false;
87 | const value = this.getValue(option);
88 | return this.valueIndex(value) !== -1;
89 | }
90 |
91 | onChecked(option: T, checked: boolean) {
92 | if(!this._values) {
93 | this._values = [];
94 | }
95 | const value = this.getValue(option);
96 | if(checked) {
97 | this._values.push(value);
98 | }
99 | else {
100 | this._values.splice(this.valueIndex(value), 1);
101 | }
102 | this.triggerChange();
103 | }
104 |
105 | onDrop(event: DndDropEvent) {
106 | const oldIdx = event.data;
107 | const newIdx = event.index;
108 | if(this._values && typeof oldIdx === 'number' && typeof newIdx === 'number' && oldIdx !== newIdx) {
109 | const [obj] = this._values.splice(oldIdx, 1);
110 | this._values?.splice(newIdx {};
121 | onTouched = () => {};
122 |
123 | writeValue(obj: T[]): void {
124 | this._values = obj;
125 | this.updateOptions();
126 | }
127 |
128 | registerOnChange(fn: any): void {
129 | this.onChange = fn;
130 | }
131 |
132 | registerOnTouched(fn: any): void {
133 | this.onTouched = fn;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/palette/palette.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Drag & drop a new component
7 |
8 | Drag & drop an item from this palette into the UI to create and configure a new component.
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Drag & drop an existing component
20 |
21 | Drag & drop an item from this palette into the UI to insert a component whose configuration already exists.
22 |
23 | ⓘ
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 | {{item.display || item.type}}
47 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/palette/palette.component.scss:
--------------------------------------------------------------------------------
1 | small, span {
2 | cursor: default;
3 | }
4 |
5 | .palette-item {
6 | display: inline-block;
7 | border: 1px solid grey;
8 | padding: 0.25rem 0.5rem;
9 | border-radius: 3px;
10 | background: rgb(0,0,0,0.1);
11 | cursor: grab;
12 | margin-right: 5px;
13 | margin-bottom: 5px;
14 | }
15 |
16 | .palette-item .btn-close {
17 | font-size: 0.7em;
18 | }
19 |
20 | .palette-item .grip {
21 | position: relative;
22 | top: -2px;
23 | color: #7c7c7c;
24 | margin-right: 3px;
25 | }
26 |
27 | .palette-item.uib-unused {
28 | border-style: dashed;
29 | }
30 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/palette/palette.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
2 | import { Observable, of, Subject, Subscription } from 'rxjs';
3 | import { Configurable } from '../../configurable/configurable.service';
4 | import { ComponentConfig, ConfigService } from '../../configuration';
5 | import { ComponentCreator, DragDropService } from '../../dynamic-views/drag-drop.service';
6 | import { ModalComponent, SvgIconComponent, TemplateNameDirective, TooltipDirective } from '../../utils';
7 | import { PaletteOptions } from '../configurator.models';
8 | import { CommonModule } from '@angular/common';
9 | import { DndModule } from 'ngx-drag-drop';
10 |
11 | declare interface PaletteItem extends ComponentCreator {
12 | type: string;
13 | display?: string;
14 | iconClass?: string;
15 | title?: string;
16 | removable?: boolean;
17 | createConfig: (id: string, creator?: ComponentCreator) => Observable;
18 | }
19 |
20 | declare interface ConfigModal {
21 | configurator: TemplateNameDirective;
22 | config: ComponentConfig;
23 | configChanged: () => void;
24 | title: string;
25 | close: Subject;
26 | }
27 |
28 | export const defaultPaletteOptions: PaletteOptions = {
29 | enableSubcontainers: true,
30 | enableRawHtml: true,
31 | rawHtmlPlaceholder: "Edit me
",
32 | showStandardPalette: true,
33 | showExistingPalette: true
34 | };
35 |
36 | @Component({
37 | selector: 'uib-palette',
38 | standalone: true,
39 | imports: [CommonModule, DndModule, TooltipDirective, SvgIconComponent, ModalComponent],
40 | templateUrl: './palette.component.html',
41 | styleUrls: ['./palette.component.scss'],
42 | changeDetection: ChangeDetectionStrategy.OnPush
43 | })
44 | export class PaletteComponent implements OnInit, OnChanges, OnDestroy {
45 | @Input() context: Configurable;
46 | @Input() configurators: Record = {};
47 | @Input() options = defaultPaletteOptions;
48 |
49 | standardPalette: PaletteItem[];
50 | existingPalette: PaletteItem[];
51 |
52 | modal?: ConfigModal;
53 |
54 | constructor(
55 | public dragDropService: DragDropService,
56 | public configService: ConfigService,
57 | public cdRef: ChangeDetectorRef
58 | ) {}
59 |
60 | ngOnInit() {
61 | // The palette of existing components is constructed from the complete configuration
62 | this.sub = this.configService.watchAllConfig()
63 | .subscribe(configs => {
64 | this.generateExistingPalette(configs)
65 | this.cdRef.markForCheck();
66 | });
67 | }
68 |
69 | ngOnChanges() {
70 | // Initialize options with default, then custom
71 | this.options = Object.assign({}, defaultPaletteOptions, this.options);
72 | this.generateAutoPalette();
73 | // The existing palette must be update when the standard palette changes
74 | this.generateExistingPalette(this.configService.getAllConfig());
75 | }
76 |
77 | sub: Subscription;
78 | ngOnDestroy(): void {
79 | this.sub.unsubscribe();
80 | }
81 |
82 | generateAutoPalette() {
83 | this.standardPalette = [];
84 | if(!this.options.showStandardPalette) {
85 | return;
86 | }
87 | if (this.options.enableSubcontainers) {
88 | this.standardPalette.push({
89 | type: '_container',
90 | display: $localize `Container`,
91 | title: $localize `A component to arrange various sub-components`,
92 | createConfig: (id: string) => of({ type: '_container', id, items: [] }),
93 | });
94 | }
95 | if (this.options.enableRawHtml) {
96 | this.standardPalette.push({
97 | type: '_raw-html',
98 | display: $localize `Raw HTML`,
99 | title: $localize `A component to write HTML freely`,
100 | createConfig: (id: string) => of({ type: '_raw-html', id, rawHtml: this.options.rawHtmlPlaceholder})
101 | })
102 | }
103 | if (this.context.templates) {
104 | Object.keys(this.context.templates).forEach((type) => {
105 | let template = this.context.templates![type];
106 | this.standardPalette.push({
107 | type,
108 | display: template.display || type,
109 | iconClass: template.iconClass,
110 | title: template.description,
111 | createConfig: (id: string) => this.openModal(id, type, this.configurators[type]),
112 | });
113 | });
114 | }
115 | }
116 |
117 | generateExistingPalette(configs: ComponentConfig[]) {
118 | if(!this.options.showExistingPalette) {
119 | this.existingPalette = [];
120 | return;
121 | }
122 | this.existingPalette = configs.filter(c =>
123 | // Add any non-container config whose type is compatible with the standard palette
124 | this.standardPalette
125 | .find(p => p.type !== '_container' && p.type === c.type))
126 | .map(c => ({
127 | type: c.type,
128 | display: c.id,
129 | title: $localize `Type: ${this.context.templates?.[c.type]?.display || c.type}`,
130 | removable: !this.configService.isUsed(c.id),
131 | createConfig: _ => of(c) // The config already exists
132 | })
133 | )
134 | }
135 |
136 | onDndStart(item: PaletteItem) {
137 | this.dragDropService.draggedCreator = item;
138 | }
139 |
140 | onDndEnd() {
141 | this.dragDropService.draggedCreator = undefined;
142 | }
143 |
144 | openModal(id: string, type: string, configurator?: TemplateNameDirective): Observable {
145 | const config = {type, id};
146 | if(configurator) {
147 | this.modal = {
148 | configurator,
149 | config,
150 | configChanged: () => {}, // do nothing when the configurator changes the config (before user presses 'OK')
151 | title: $localize `Create new ${type} component`,
152 | close: new Subject()
153 | }
154 | return this.modal.close;
155 | }
156 | return of(config);
157 | }
158 |
159 | onModalClose(success: boolean) {
160 | if(this.modal?.close) {
161 | this.modal.close.next(success? this.modal.config : undefined);
162 | this.modal.close.complete();
163 | this.modal = undefined;
164 | }
165 | }
166 |
167 | removeItem(item: PaletteItem) {
168 | console.log("remove", item);
169 | this.configService.removeConfig(item.display!); // The display is the component id
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './editors';
2 | export * from './palette/palette.component';
3 | export * from './toolbar/toolbar.component';
4 | export * from './tree/tree.component';
5 | export * from './checkbox/checkbox.component';
6 | export * from './color-picker/color-picker.component';
7 | export * from './image-selector/img-selector.component';
8 | export * from './multi-selector/multi-selector.component';
9 |
10 | export * from './configurator.component';
11 | export * from './configurator.models';
12 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/toolbar/toolbar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/toolbar/toolbar.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from "@angular/core";
2 |
3 | import { CommonModule } from "@angular/common";
4 | import { ConfigurableService } from "../../configurable";
5 | import { ConfigService } from "../../configuration";
6 | import { TooltipDirective } from "../../utils";
7 | import { SvgIconComponent } from "../../utils/svg-icon/svg-icon.component";
8 |
9 | @Component({
10 | selector: 'uib-toolbar',
11 | standalone: true,
12 | imports: [CommonModule, SvgIconComponent, TooltipDirective],
13 | templateUrl: './toolbar.component.html',
14 | styles: [
15 | `
16 | .uib-toolbar .btn.disabled {
17 | /* allow tooltip on disabled buttons */
18 | pointer-events: visible;
19 | /* but display a simple arrow cursor */
20 | cursor: default;
21 | }
22 |
23 | .uib-toolbar .btn svg-icon {
24 | display: flex;
25 | align-items: center;
26 | width: 22px;
27 | height: 25px;
28 | }
29 | .uib-toolbar-anim {
30 | transition: ease .3s;
31 | }
32 | `
33 | ],
34 | changeDetection: ChangeDetectionStrategy.OnPush
35 | })
36 | export class ToolbarComponent {
37 |
38 | constructor(
39 | public configService: ConfigService,
40 | public configurableService: ConfigurableService
41 | ) {}
42 |
43 | toggleEditor() {
44 | this.configurableService.toggleEditor();
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/projects/lib/src/configurator/tree/tree.component.css:
--------------------------------------------------------------------------------
1 | .tree,
2 | .section ul {
3 | list-style: none;
4 | padding: 0;
5 | margin: 0;
6 | }
7 |
8 | .tree li {
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: flex-start;
12 | align-items: flex-start;
13 |
14 | list-style-type: none;
15 | margin: 10px;
16 | position: relative;
17 | width: 100%;
18 | }
19 |
20 | .tree li:not(.section) {
21 | max-width: max-content;
22 | }
23 |
24 |
25 | .tree li::before {
26 | content: "";
27 | position: absolute;
28 | top: -9px;
29 | left: -20px;
30 | border-left: 1px solid #ccc;
31 | border-bottom: 1px solid #ccc;
32 | border-radius: 0 0 0 0px;
33 | width: 20px;
34 | height: 22px;
35 | }
36 |
37 | .tree li::after {
38 | position: absolute;
39 | content: "";
40 | top: 12px;
41 | left: -20px;
42 | border-left: 1px solid #ccc;
43 | border-top: 1px solid #ccc;
44 | border-radius: 0px 0 0 0;
45 | width: 20px;
46 | height: 100%;
47 | }
48 |
49 | .tree li:last-child::after {
50 | display: none;
51 | }
52 |
53 | .tree li:last-child:before {
54 | border-radius: 0 0 0 5px;
55 | }
56 |
57 | ul.tree > li:first-child::before {
58 | display: none;
59 | }
60 |
61 | ul.tree > li:first-child::after {
62 | border-radius: 5px 0 0 0;
63 | }
64 |
65 | .tree li a {
66 | border: 1px #ccc solid;
67 | border-radius: 5px;
68 | padding: 2px 5px;
69 | text-decoration: none;
70 | }
71 |
72 | .tree li a.selected {
73 | box-shadow: 0 0 0 3px red;
74 | z-index: 1;
75 | }
76 |
77 | .tree li a:hover,
78 | .tree li a:hover + ul li a,
79 | .tree li a:focus,
80 | .tree li a:focus + ul li a {
81 | background: #ccc;
82 | color: #000;
83 | border: 1px solid #000;
84 | cursor: pointer;
85 | }
86 |
87 | .tree li a:hover + ul li::after,
88 | .tree li a:focus + ul li::after,
89 | .tree li a:hover + ul li::before,
90 | .tree li a:focus + ul li::before .tree li a:hover + ul::before,
91 | .tree li a:focus + ul::before .tree li a:hover + ul ul::before,
92 | .tree li a:focus + ul ul::before {
93 | border-color: #000; /*connector color on hover*/
94 | }
95 |
96 | .tree li svg-icon {
97 | cursor: pointer;
98 | }
99 |
100 | .section ul {
101 | transition: opacity 0.3s linear;
102 | height: 0;
103 | transform: translate(9999px);
104 | opacity: 0;
105 | }
106 | .section input:checked ~ ul {
107 | transform: translate(0);
108 | opacity: 1;
109 | height: auto;
110 | }
111 |
112 | .section input[type="checkbox"] {
113 | display: none;
114 | }
115 |
116 | .section {
117 | position: relative;
118 | padding-left: 35px;
119 | }
120 |
121 | .section label + a {
122 | margin-left: -35px;
123 | padding-left: 22px;
124 | text-decoration: none;
125 | color: #000;
126 | }
127 |
128 | .section label:before {
129 | content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
130 | position: absolute;
131 | top: 4px;
132 | left: 2px;
133 | text-align: center;
134 | display: inline-block;
135 | transition: transform 0.5s;
136 | z-index: 2;
137 | user-select: none;
138 | cursor: pointer;
139 | }
140 |
141 | .section input:checked ~ label:before {
142 | transform: rotate(90deg);
143 | }
144 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/tree/tree.component.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
42 |
--------------------------------------------------------------------------------
/projects/lib/src/configurator/tree/tree.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2 | import { ConfigurableService} from '../../configurable';
3 | import { ComponentConfig, ConfigService } from '../../configuration';
4 | import { CommonModule } from '@angular/common';
5 | import { SvgIconComponent } from "../../utils/svg-icon/svg-icon.component";
6 | import { ModalComponent } from '../../utils';
7 | import { FormsModule } from '@angular/forms';
8 |
9 | @Component({
10 | selector: 'uib-tree',
11 | standalone: true,
12 | templateUrl: './tree.component.html',
13 | styleUrls: ['tree.component.css'],
14 | imports: [CommonModule, SvgIconComponent, ModalComponent, FormsModule]
15 | })
16 | export class TreeComponent implements OnChanges {
17 | @Input() configuration: ComponentConfig[];
18 |
19 | root: Partial[];
20 | hoveredId?: string;
21 | configToEdit?: {id: string, display: string};
22 | displayName?: string;
23 |
24 | private configurationMap: Map>;
25 |
26 | constructor(public configurableService: ConfigurableService,
27 | public configService: ConfigService) { }
28 |
29 | ngOnChanges(changes: SimpleChanges) {
30 | // set our Map object,
31 | this.configurationMap = new Map();
32 | this.configuration.forEach((el) => this.configurationMap.set(el.id, el));
33 |
34 | // set children parent_id and order
35 | this.configuration
36 | .filter((parent) => parent.items && parent.items.length > 0)
37 | .forEach((parent) =>
38 | parent.items.forEach((id: string, index: number) => {
39 | // a child could have multiple parents
40 | // get child's parents
41 | const {parents = [], orders = {}} = this.configurationMap.get(id) || {};
42 | this.configurationMap.set(id, {...this.configurationMap.get(id), id: id, parents: [...parents, parent.id], orders: {...orders, [parent.id]: index }} )
43 | })
44 | );
45 |
46 | // items without parent_id (root level)
47 | this.root = [...this.configurationMap.values()].filter((el) => el.parents === undefined);
48 | }
49 |
50 | /**
51 | * Given an id, return all the children of that id.
52 | * @param {string} id - string - The id of the component.
53 | * @returns The children of the component with the given id.
54 | */
55 | children(id: string): Partial[] {
56 | return [...this.configurationMap.values()].filter((el) => el.parents ? el.parents.includes(id) : false).sort((a, b) => a.orders[id] - b.orders[id]);
57 | }
58 |
59 | select(id: string) {
60 | const el = this.configurableService.configurableDirectiveMap.get(id);
61 | el?.click(new MouseEvent("click"));
62 | el?.nativeElement.scrollIntoView({behavior: 'smooth', inline: 'nearest', block: 'center'});
63 | }
64 |
65 | hover(id: string | undefined) {
66 | this.hoveredId = id; // used to display and hide the edit icons
67 | const hoveredId = this.configurableService.hoveredId;
68 | if (hoveredId) {
69 | const prev = this.configurableService.configurableDirectiveMap.get(hoveredId);
70 | prev?.removeHighlight();
71 | }
72 |
73 | this.configurableService.hoveredId = id;
74 | if (id) {
75 | const el = this.configurableService.configurableDirectiveMap.get(id);
76 | el?.addHighlight();
77 |
78 | el?.nativeElement.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'center' });
79 | }
80 | }
81 |
82 | editConfigDisplayName(context: {id: string, display: string}) {
83 | this.configToEdit = context;
84 | this.displayName = context.display ? String(context.display) : undefined;
85 | }
86 |
87 | onModalClose(validated: boolean) {
88 | if (validated) {
89 | const config = this.configService.getConfig(this.configToEdit!.id);
90 | config.display = this.displayName;
91 | this.configService.updateConfig(config);
92 | }
93 | this.configToEdit = undefined;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/drag-drop.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Observable } from 'rxjs';
3 | import {
4 | ComponentConfig,
5 | ConfigService,
6 | ContainerConfig,
7 | } from '../configuration';
8 | import { ToastAction, ToastService } from '../utils';
9 |
10 | export interface ContainerIndex {
11 | container: string;
12 | index: number;
13 | }
14 |
15 | export interface ComponentCreator {
16 | type: string;
17 | createConfig: (id: string, creator?: ComponentCreator) => Observable;
18 | }
19 |
20 | @Injectable({ providedIn: 'root' })
21 | export class DragDropService {
22 |
23 | constructor(
24 | public configService: ConfigService,
25 | public toastService: ToastService
26 | ) {}
27 |
28 | draggedCreator?: ComponentCreator;
29 |
30 | undoAction: ToastAction = {
31 | text: $localize `Undo`,
32 | hideToast: true,
33 | action: () => this.configService.undo()
34 | }
35 |
36 | public handleDrop(
37 | containerId: string,
38 | index: number,
39 | dropped: ContainerIndex | string
40 | ) {
41 | const container = this.configService.getContainer(containerId);
42 | // Drop a component from a container to another container
43 | if ((dropped as ContainerIndex).container) {
44 | dropped = dropped as ContainerIndex;
45 | const og = this.configService.getContainer(dropped.container);
46 | if (dropped.container === containerId) {
47 | this.moveWithin(og, dropped.index, index);
48 | } else {
49 | this.moveBetween(container, index, og, dropped.index);
50 | }
51 | }
52 | // Drag a component creator (from a palette)
53 | else if (this.draggedCreator?.type === dropped) {
54 | const newId = this.configService.generateId(dropped);
55 | this.draggedCreator
56 | .createConfig(newId, this.draggedCreator)
57 | .subscribe((config) => this.insertNew(container, index, config));
58 | }
59 | else {
60 | console.error("Unexpected dropped item:", dropped);
61 | }
62 | }
63 |
64 | public handleCancel(
65 | index: number,
66 | containerId: string
67 | ) {
68 | const container = this.configService.getContainer(containerId);
69 | const config = container.items.splice(index, 1);
70 | this.configService.updateConfig([container]);
71 | this.toastService.show(
72 | $localize `Component '${config[0]}' removed`,
73 | "warning text-dark",
74 | [this.undoAction]
75 | );
76 | }
77 |
78 | private insertNew(
79 | container: ContainerConfig,
80 | index: number,
81 | component: ComponentConfig|undefined
82 | ) {
83 | if(component) {
84 | container.items.splice(index, 0, component.id);
85 | this.configService.updateConfig([container, component]); // addEntities might be needed
86 | }
87 | }
88 |
89 | private moveBetween(
90 | container: ContainerConfig,
91 | index: number,
92 | ogContainer: ContainerConfig,
93 | ogIndex: number
94 | ) {
95 | let item = ogContainer.items.splice(ogIndex, 1);
96 | container.items.splice(index, 0, item[0]);
97 | this.configService.updateConfig([ogContainer, container]);
98 | }
99 |
100 | private moveWithin(
101 | container: ContainerConfig,
102 | oldIndex: number,
103 | newIndex: number
104 | ) {
105 | if (oldIndex !== newIndex && oldIndex !== newIndex - 1) {
106 | let item = container.items.splice(oldIndex, 1);
107 | if (newIndex > oldIndex) {
108 | newIndex--;
109 | }
110 | container.items.splice(newIndex, 0, item[0]);
111 | this.configService.updateConfig(container);
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
2 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/item/item.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
30 |
31 |
32 |
33 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/item/item.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | ChangeDetectorRef,
4 | Component,
5 | ElementRef,
6 | HostBinding,
7 | Input,
8 | OnChanges,
9 | OnDestroy,
10 | OnInit,
11 | SimpleChanges,
12 | } from '@angular/core';
13 | import { CommonModule } from '@angular/common';
14 | import { Subscription } from 'rxjs';
15 |
16 | import { ConditionsService } from '../../conditions/conditions.service';
17 | import { ComponentConfig, ConfigService } from '../../configuration';
18 | import { ZoneContextService } from '../zone/zone-context.service';
19 | import { ContainerIndex, DragDropService } from '../drag-drop.service';
20 | import { TooltipDirective } from '../../utils';
21 | import { ConfigurableDirective } from '../../configurable';
22 | import { DndDropEvent, DndModule } from 'ngx-drag-drop';
23 |
24 | @Component({
25 | selector: '[uib-item]',
26 | standalone: true,
27 | imports: [CommonModule, TooltipDirective, ConfigurableDirective, DndModule],
28 | templateUrl: './item.component.html',
29 | changeDetection: ChangeDetectionStrategy.OnPush
30 | })
31 | export class ItemComponent implements OnInit, OnChanges, OnDestroy {
32 | /**
33 | * id's element
34 | */
35 | @Input('uib-item') id: string;
36 | @Input() parentId: string;
37 |
38 | /**
39 | * index of element displayed when data is an Array
40 | */
41 | @Input() dataIndex?: number;
42 |
43 | /**
44 | * Is the component configurable
45 | */
46 | @Input() configurable: boolean = true;
47 |
48 | @HostBinding('class')
49 | classes?: string;
50 |
51 |
52 | // zone's conext informations
53 | /**
54 | * zone's name/id
55 | */
56 | zone: string;
57 |
58 | /**
59 | * used to store the configuration object for the component.
60 | */
61 | config: ComponentConfig;
62 | /**
63 | * used to determine if the component should be displayed or not.
64 | */
65 | condition = true;
66 | _data: any;
67 |
68 | private subscription = new Subscription();
69 |
70 | /**
71 | * used to determine if the flex direction is horizontal or not.
72 | * Always `false` when the component's type is not `_container`
73 | */
74 | isHorizontal: boolean = false;
75 |
76 | constructor(
77 | public zoneRef: ZoneContextService,
78 | public configService: ConfigService,
79 | public conditionsService: ConditionsService,
80 | public dragDropService: DragDropService,
81 | public cdr: ChangeDetectorRef,
82 | public el: ElementRef
83 | ) {
84 | // retrieve all zone's available informations
85 | this.zone = this.zoneRef.id;
86 |
87 | this.subscription.add(
88 | this.zoneRef.changes$.subscribe(({ data, conditionsData }) => {
89 | this.updateData();
90 | this.updateCondition();
91 | })
92 | );
93 | }
94 |
95 | ngOnChanges(changes: SimpleChanges): void {
96 | if(changes.dataIndex) {
97 | this.updateData();
98 | this.updateCondition();
99 | }
100 | }
101 |
102 | ngOnInit() {
103 | const configChanges$ = this.configService.watchConfig(this.id).subscribe((config) => this.updateConfig(config));
104 | const allConfigChanges$ = this.configService.watchAllConfig().subscribe(() => this.cdr.markForCheck());
105 |
106 | this.subscription.add(configChanges$);
107 | this.subscription.add(allConfigChanges$);
108 | }
109 |
110 | ngOnDestroy() {
111 | this.subscription.unsubscribe();
112 | }
113 |
114 | // updates
115 |
116 | /**
117 | * It updates the component's configuration and condition, and sets the classes
118 | * for the component
119 | * @param {ComponentConfig} config - ComponentConfig - the configuration object
120 | * for the component
121 | */
122 | private updateConfig(config: ComponentConfig) {
123 | this.config = config;
124 | this.updateCondition();
125 | this.classes = this.config.classes || '';
126 | if (config.type === '_container') {
127 | this.classes += ' uib-container';
128 | if (this.configurable) {
129 | this.classes += ' uib-dropzone-content';
130 | }
131 | }
132 | if(!this.condition && !this.configurable) {
133 | this.classes += ' d-none'; // hide component unless in edit mode
134 | }
135 | this.isHorizontal = this.horizontal();
136 | }
137 |
138 | /**
139 | * If the dataIndex property is undefined, then the data property is assigned to
140 | * the `_data` property.
141 | *
142 | * Otherwise, the data property is indexed by the dataIndex
143 | * property and the result is assigned to the _data property
144 | */
145 | private updateData() {
146 | this._data = typeof this.dataIndex === 'undefined'? this.zoneRef.data : this.zoneRef.data[this.dataIndex];
147 | }
148 |
149 | /**
150 | * If the condition is not null, then check the condition and update the
151 | * condition variable
152 | */
153 | private updateCondition() {
154 | this.condition = this.conditionsService.check(this.config?.condition, this.zoneRef.conditionsData, this._data);
155 | }
156 |
157 | // Drag & Drop
158 |
159 | onDndDrop(event: DndDropEvent) {
160 | const dropped: string | ContainerIndex = (typeof event.data === 'string')
161 | ? event.data
162 | : { container: event.data.container, index: event.data.index }
163 |
164 | if(typeof event.index === 'number') {
165 | this.dragDropService.handleDrop(this.id, event.index, dropped);
166 | }
167 | }
168 |
169 | onDndCanceled(item: string, index: number) {
170 | console.log('cancelled', item, this.id);
171 | this.dragDropService.handleCancel(index, this.id);
172 | }
173 |
174 | /**
175 | * * If the element has a class of flex-column, it's not horizontal.
176 | * * If it has a class of d-flex or uib-dropzone, it's horizontal.
177 | * * If it has a style of display: flex, it's horizontal.
178 | *
179 | * Otherwise, it's not horizontal
180 | * @returns A boolean value.
181 | */
182 | private horizontal(): boolean {
183 | if(this.config.classes?.includes('flex-column')) {
184 | return false;
185 | }
186 | if (this.config.classes?.includes('d-flex') || this.config.classes?.includes('uib-dropzone') || this.el.nativeElement.style.display === "flex") {
187 | return true;
188 | }
189 | return false;
190 | }
191 |
192 | getItemTooltip(id: string): string {
193 | const config = this.configService.getConfig(id);
194 | return config.display || id;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './drag-drop.service';
2 | export * from './item/item.component';
3 | export * from './zone/zone.component';
4 | export * from './zone/zone-context.service';
5 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/zone/zone-context.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from "@angular/core";
2 | import { Subject } from "rxjs";
3 | import { TemplateNameDirective } from "../../utils/directive/template-name.directive";
4 |
5 | @Injectable()
6 | export class ZoneContextService {
7 | /**
8 | * zone's id
9 | */
10 | id: string;
11 | /**
12 | * additional template's data. If data is an array, an *ngFor will be done using
13 | * each data's items
14 | */
15 | data?: any;
16 | /**
17 | * conditional display rules
18 | */
19 | conditionsData?: Record;
20 |
21 | /**
22 | * TemplateNameDirective's collection
23 | */
24 | templates: Record = {};
25 |
26 | /**
27 | * When data or conditionsData change, emit changes
28 | */
29 | changes$ = new Subject<{data: unknown, conditionsData: Record | undefined}>();
30 |
31 | update(state:{data: unknown, conditionsData: Record | undefined}) {
32 | this.data = state.data;
33 | this.conditionsData = state.conditionsData;
34 |
35 | this.changes$.next(state);
36 | }
37 | }
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/zone/zone.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/projects/lib/src/dynamic-views/zone/zone.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | ChangeDetectorRef,
4 | Component,
5 | OnDestroy,
6 | OnInit,
7 | Output,
8 | EventEmitter,
9 | ContentChildren,
10 | Input,
11 | QueryList,
12 | SimpleChanges,
13 | } from '@angular/core';
14 | import { BehaviorSubject, Subscription } from 'rxjs';
15 |
16 | import { ConfigService } from '../../configuration/config.service';
17 | import { ConfigurableDirective, ConfigurableService } from '../../configurable';
18 | import { ZoneContextService } from './zone-context.service';
19 | import { TemplateNameDirective } from '../../utils';
20 | import { CommonModule } from '@angular/common';
21 | import { ItemComponent } from '../public-api';
22 |
23 |
24 | @Component({
25 | selector: 'uib-zone',
26 | standalone: true,
27 | imports: [CommonModule, ConfigurableDirective, ItemComponent],
28 | templateUrl: './zone.component.html',
29 | changeDetection: ChangeDetectionStrategy.OnPush,
30 | providers: [ZoneContextService]
31 | })
32 | export class ZoneComponent implements OnInit, OnDestroy {
33 | @ContentChildren(TemplateNameDirective)
34 | children: QueryList;
35 |
36 | /**
37 | * zone's id
38 | */
39 | @Input() id: string;
40 | /**
41 | * additional template's data. If data is an array, an *ngFor will be done using
42 | * each data's items
43 | */
44 | @Input() data?: any;
45 | /**
46 | * conditional display rules
47 | */
48 | @Input() conditionsData?: Record;
49 |
50 | /**
51 | * Emit an event when a zone's element is clicked
52 | * sending it's index and additional data
53 | */
54 | @Output()
55 | itemClicked = new EventEmitter<{data: any, index?: number, event: Event}>();
56 |
57 | /**
58 | * return `true` when edit mode is enabled
59 | */
60 | enabled$: BehaviorSubject;
61 |
62 | private subscription = new Subscription();
63 |
64 | constructor(
65 | public readonly zoneContext: ZoneContextService,
66 | public readonly configService: ConfigService,
67 | public readonly configurableService: ConfigurableService,
68 | public readonly cdr: ChangeDetectorRef
69 | ) {}
70 |
71 | ngOnInit() {
72 | this.zoneContext.id = this.id;
73 | this.enabled$ = this.configurableService.editorEnabled$
74 |
75 | this.subscription.add(
76 | this.configService
77 | .watchConfig(this.id)
78 | .subscribe(() => {
79 | this.cdr.markForCheck(); // necessary to apply config changes
80 | })
81 | );
82 | }
83 |
84 |
85 | ngOnDestroy() {
86 | this.subscription.unsubscribe();
87 | }
88 |
89 | ngOnChanges(changes: SimpleChanges): void {
90 | this.zoneContext.update({data: this.data, conditionsData: this.conditionsData});
91 | }
92 |
93 | ngAfterContentInit() {
94 | this.children.forEach(
95 | (template) => (this.zoneContext.templates[template.name] = template)
96 | );
97 | }
98 |
99 | get isArray(): boolean {
100 | return Array.isArray(this.zoneContext.data);
101 | }
102 |
103 | /**
104 | * Propagate the element's click event
105 | *
106 | * If the index is undefined, then data is the data object, otherwise data is the
107 | * data object's index
108 | * @param {Event} event - The event that triggered the click.
109 | * @param {any} [data] - The data object that was clicked.
110 | * @param {number} [index] - The index of the item in the list.
111 | */
112 | onItemClicked(event: Event, data?: any, index?: number) {
113 | data = typeof index === 'undefined'? data : data[index];
114 | this.itemClicked.next({data, index, event});
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/projects/lib/src/public-api.ts:
--------------------------------------------------------------------------------
1 | // Necessary to resolve the '$localize' marker
2 | import '@angular/localize/init';
3 |
4 | /*
5 | * Public API Surface of lib
6 | */
7 |
8 | export * from './configurable';
9 | export * from './configuration';
10 | export * from './configurator';
11 | export * from './conditions';
12 | export * from './dynamic-views';
13 | export * from './utils';
14 |
15 | export * from './svg/svg-icons';
16 | export * from "./utils/svg-icon";
17 |
18 | export * from "./uib.module";
--------------------------------------------------------------------------------
/projects/lib/src/uib.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from "@angular/core";
2 |
3 | import { AutocompleteComponent, CheckboxComponent, ColorPickerComponent, ConditionPipe, ConfigModule, ConfiguratorComponent, ImageSelectorComponent, ItemComponent, MultiSelectComponent, NgModelChangeDebouncedDirective, PaletteComponent, TemplateNameDirective, ToastComponent, ToolbarComponent, TooltipDirective, ZoneComponent } from "./public-api";
4 |
5 |
6 | const components = [
7 | ConfigModule,
8 | // standalone components
9 | ConditionPipe,
10 | ConfiguratorComponent,
11 | ZoneComponent,
12 | ItemComponent,
13 | ToolbarComponent,
14 | ToastComponent,
15 | AutocompleteComponent,
16 | CheckboxComponent,
17 | ImageSelectorComponent,
18 | ColorPickerComponent,
19 | MultiSelectComponent,
20 | PaletteComponent,
21 | // directives
22 | TemplateNameDirective,
23 | TooltipDirective,
24 | NgModelChangeDebouncedDirective,
25 | ]
26 |
27 | @NgModule({
28 | imports: [...components],
29 | exports: [...components]
30 | })
31 | export class uibModule {}
--------------------------------------------------------------------------------
/projects/lib/src/utils/autocomplete/autocomplete.component.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from "@angular/common";
2 | import { Component, Input, OnInit, Output, EventEmitter, ContentChild, TemplateRef } from "@angular/core";
3 | import { fromEvent, merge, Observable, of } from "rxjs";
4 | import { filter, switchMap } from "rxjs/operators";
5 |
6 | @Component({
7 | selector: 'uib-autocomplete',
8 | standalone: true,
9 | imports: [CommonModule],
10 | template: `
11 |
18 |
19 | {{suggest}}
20 | `,
21 | styles: [`
22 | .card {
23 | z-index: 3;
24 | box-shadow: 0 5px 7px rgb(0 0 0 / 8%);
25 | }
26 | .list-group {
27 | top: 0;
28 | width: 100%;
29 | max-height: 200px;
30 | overflow: auto;
31 | }
32 | .list-group-item-action {
33 | cursor: pointer;
34 | }
35 | `]
36 | })
37 | export class AutocompleteComponent implements OnInit {
38 | @Input() inputElement: HTMLInputElement;
39 | @Input() suggestGenerator?: (value: string) => Observable;
40 | @Input() allSuggests?: string[];
41 | @Output() select = new EventEmitter();
42 | @ContentChild("itemTpl", {static: false}) itemTpl: TemplateRef;
43 | suggests$ = new Observable();
44 | clickInProgress = false;
45 |
46 | ngOnInit() {
47 | this.suggests$ = merge(
48 | fromEvent(this.inputElement, 'input'),
49 | fromEvent(this.inputElement, 'focus'),
50 | fromEvent(this.inputElement, 'blur').pipe(filter(() => (!this.clickInProgress))), // Filter out blur event caused by a click on the autocomplete
51 | this.select // Hide the autocomplete when an item has been selected
52 | ).pipe(
53 | switchMap(event => {
54 | if(typeof event !== 'string' && event.type !== 'blur') {
55 | if(this.suggestGenerator) {
56 | return this.suggestGenerator(this.inputElement.value);
57 | }
58 | else if(this.allSuggests) {
59 | return of(this.allSuggests.filter(suggest => suggest !== this.inputElement.value && suggest.includes(this.inputElement.value)));
60 | }
61 | }
62 | return of([]);
63 | })
64 | );
65 | }
66 |
67 | onSelect(value: string) {
68 | this.inputElement.value = value;
69 | this.select.next(value);
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/projects/lib/src/utils/directive/model-change.directive.ts:
--------------------------------------------------------------------------------
1 | import { AfterContentInit, Directive, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
2 | import { NgModel } from '@angular/forms';
3 | import { Subscription } from 'rxjs';
4 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
5 |
6 | @Directive({
7 | selector: '[ngModelChangeDebounced]',
8 | standalone: true
9 | })
10 | export class NgModelChangeDebouncedDirective implements OnDestroy, AfterContentInit {
11 | @Input() ngModelDebounceTime = 1000;
12 | @Output() ngModelChangeDebounced = new EventEmitter()
13 |
14 | private subs: Subscription;
15 |
16 | constructor(private ngModel: NgModel) {}
17 |
18 | ngAfterContentInit(): void {
19 | this.subs = this.ngModel.update
20 | .pipe(
21 | distinctUntilChanged(),
22 | debounceTime(this.ngModelDebounceTime),
23 | )
24 | .subscribe((value) => {
25 | this.ngModelChangeDebounced.emit(value);
26 | });
27 | }
28 |
29 | ngOnDestroy(): void {
30 | this.subs.unsubscribe();
31 | }
32 | }
--------------------------------------------------------------------------------
/projects/lib/src/utils/directive/template-name.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input, TemplateRef } from '@angular/core';
2 |
3 | @Directive({
4 | selector: '[uib-template]',
5 | standalone: true
6 | })
7 | export class TemplateNameDirective {
8 | /**
9 | * template's name
10 | */
11 | @Input('uib-template') name: string;
12 |
13 | // Properties below are used by the configurator
14 | /**
15 | * name used by the configurator to identify the component
16 | */
17 | @Input('uib-templateDisplay') display?: string;
18 | /**
19 | * icon used by the configurator to identify the component
20 | */
21 | @Input('uib-templateIconClass') iconClass?: string;
22 | /**
23 | * description used by the configurator
24 | */
25 | @Input('uib-templateDescription') description?: string;
26 |
27 | templateRef: TemplateRef;
28 |
29 | constructor(protected readonly _templateRef: TemplateRef) {
30 | this.templateRef = _templateRef;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/directive/tooltip.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, ElementRef, Input, OnChanges, OnDestroy } from "@angular/core";
2 | import { Tooltip } from "bootstrap";
3 |
4 | export type TooltipPlacement = 'auto' | 'top' | 'bottom' | 'left' | 'right';
5 |
6 | @Directive({
7 | selector: '[uib-tooltip]',
8 | standalone: true
9 | })
10 | export class TooltipDirective implements OnChanges, OnDestroy {
11 | @Input('uib-tooltip') html: string;
12 |
13 | /**
14 | * Allow Bootstrap tooltip's options configuration
15 | *
16 | * * `title`, `placement` and `container` options are removed from Bootstrap options as they use their own inputs.
17 | * * `html`, `sanitize`, `delay` and `trigger` options are sets with different values as well by default but can
18 | * be overriden here.
19 | */
20 | @Input() config?: Partial> = {
21 | html: true,
22 | sanitize: false,
23 | delay: { show: 300, hide: 0 },
24 | trigger: 'hover'
25 | };
26 |
27 | /**
28 | * How to position the tooltip.
29 | */
30 | @Input() placement: TooltipPlacement = "auto";
31 |
32 | /**
33 | * Append the tooltip to a specific element.
34 | *
35 | * By default, tooltip is append to `uib-bootstrap` component.
36 | * When `undefined`, tooltip is append to his host element.
37 | */
38 | @Input() container?: string | Element = ".uib-bootstrap";
39 |
40 | tooltip?: Tooltip;
41 |
42 | constructor(public el: ElementRef){}
43 |
44 | ngOnChanges() {
45 | this.tooltip?.dispose(); // Force the creation of a new tooltip, or else the content is not updated
46 | if(this.html) {
47 | this.tooltip = Tooltip.getOrCreateInstance(this.el.nativeElement, {
48 | ...this.config,
49 | placement: this.placement,
50 | title: this.html,
51 | container: this.container || this.el.nativeElement
52 | });
53 | }
54 | }
55 |
56 | ngOnDestroy() {
57 | this.tooltip?.dispose();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
2 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/modal/modal.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, ViewChild } from "@angular/core";
2 | import { Modal } from "bootstrap";
3 |
4 | @Component({
5 | selector: 'uib-modal',
6 | standalone: true,
7 | template: `
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 | `,
26 | changeDetection: ChangeDetectionStrategy.OnPush
27 | })
28 | export class ModalComponent implements AfterViewInit, OnChanges, OnDestroy {
29 |
30 | @ViewChild('modal', {static: true}) el: ElementRef;
31 | @Input() title: string;
32 | @Input() show: boolean;
33 | @Output() close = new EventEmitter();
34 |
35 | modal?: Modal;
36 |
37 | ngOnChanges() {
38 | if(this.show && !this.modal) {
39 | this.modal = Modal.getOrCreateInstance(this.el.nativeElement, {backdrop: false}); // Backdrop causes issues when the modal is embedded in a fixed container
40 | this.modal.show();
41 | }
42 | }
43 |
44 | ngOnDestroy() {
45 | this.modal?.dispose();
46 | }
47 |
48 | ngAfterViewInit() {
49 | this.el.nativeElement.addEventListener('hidden.bs.modal', () => this.onModalClose(false));
50 | }
51 |
52 | onModalClose(success: boolean) {
53 | this.close.next(success);
54 | this.modal?.hide();
55 | this.modal = undefined;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './autocomplete/autocomplete.component';
2 |
3 | export * from './directive/template-name.directive';
4 | export * from './directive/model-change.directive';
5 | export * from './directive/tooltip.directive';
6 |
7 | export * from './modal/modal.component';
8 |
9 | export * from './toast/toast.component'
10 | export * from './toast/toast.service';
11 |
12 | export * from "./typings";
13 |
14 | export * from '../dynamic-views/zone/zone-context.service';
15 |
16 | export * from "./svg-icon/registry";
17 | export * from "./svg-icon/svg-icon.component";
18 | export * from "./svg-icon/types";
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './public-api';
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/public-api.ts:
--------------------------------------------------------------------------------
1 | export * from './svg-icon.module';
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/registry.ts:
--------------------------------------------------------------------------------
1 | import { DOCUMENT } from '@angular/common';
2 | import { Inject, Injectable, Injector } from '@angular/core';
3 |
4 | import { SVG_CONFIG, SVG_ICONS_CONFIG, SvgIconType } from './types';
5 |
6 | class SvgIcon {
7 | init = false;
8 |
9 | constructor(public content: string) {}
10 | }
11 |
12 | @Injectable({ providedIn: 'root' })
13 | export class SvgIconRegistry {
14 | private readonly svgMap = new Map();
15 | private readonly document: Document;
16 |
17 | constructor(injector: Injector, @Inject(SVG_ICONS_CONFIG) config: SVG_CONFIG) {
18 | this.document = injector.get(DOCUMENT);
19 |
20 | if (config.icons) {
21 | this.register(config.icons);
22 | }
23 |
24 | if (config.missingIconFallback) {
25 | this.register(config.missingIconFallback);
26 | }
27 | }
28 |
29 | getAll() {
30 | return this.svgMap;
31 | }
32 |
33 | get(key: string | undefined): string | undefined {
34 | const icon = key && this.svgMap.get(key);
35 |
36 | if (!icon) {
37 | return undefined;
38 | }
39 |
40 | if (!icon.init) {
41 | const svg = this.toElement(icon.content);
42 | svg.setAttribute('fit', '');
43 | svg.setAttribute('height', '100%');
44 | svg.setAttribute('width', '100%');
45 | svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
46 | svg.setAttribute('focusable', 'false');
47 |
48 | icon.content = svg.outerHTML;
49 | icon.init = true;
50 | }
51 |
52 | return icon.content;
53 | }
54 |
55 | register(icons: SvgIconType | SvgIconType[]) {
56 | for (const { name, data } of Array.isArray(icons) ? icons : [icons]) {
57 | if (!this.svgMap.has(name)) {
58 | this.svgMap.set(name, new SvgIcon(data));
59 | }
60 | }
61 | }
62 |
63 | getSvgElement(name: string): SVGSVGElement | undefined {
64 | const content = this.get(name);
65 |
66 | if (!content) {
67 | return undefined;
68 | }
69 |
70 | const div = this.document.createElement('div');
71 | div.innerHTML = content;
72 |
73 | return div.querySelector('svg') as SVGSVGElement;
74 | }
75 |
76 | private toElement(content: string): SVGElement {
77 | const div = this.document.createElement('div');
78 | div.innerHTML = content;
79 |
80 | return div.querySelector('svg') as SVGElement;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/svg-icon.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, ElementRef, Inject, Input } from '@angular/core';
2 |
3 | import { SvgIconRegistry } from './registry';
4 | import { SVG_CONFIG, SVG_ICONS_CONFIG } from './types';
5 |
6 | @Component({
7 | selector: 'svg-icon',
8 | standalone: true,
9 | template: '',
10 | host: {
11 | role: 'img',
12 | 'aria-hidden': 'true',
13 | },
14 | styles: [
15 | `
16 | :host {
17 | display: inline-block;
18 | fill: currentColor;
19 | font-size: 1rem;
20 |
21 | transform: rotate(var(--rotate, 0deg));
22 | transition: transform 0.2s ease-in-out;
23 | }
24 | `,
25 | ],
26 | changeDetection: ChangeDetectionStrategy.OnPush,
27 | })
28 | export class SvgIconComponent {
29 | @Input()
30 | set key(name: string) {
31 | const icon = this.registry.get(name) ?? this.registry.get(this.config.missingIconFallback?.name);
32 |
33 | if (icon) {
34 | this.element.setAttribute('aria-label', `${name}-icon`);
35 | this.element.classList.remove(`svg-icon-${this.lastKey}`);
36 | this.lastKey = name;
37 | this.element.classList.add(`svg-icon-${name}`);
38 | this.element.innerHTML = icon;
39 | }
40 | }
41 |
42 | @Input()
43 | set size(value: keyof SVG_CONFIG['sizes']) {
44 | this.element.style.fontSize = this.mergedConfig.sizes[value];
45 | }
46 |
47 | @Input() set width(value: number | string) {
48 | this.element.style.width = coerceCssPixelValue(value);
49 | }
50 |
51 | @Input() set height(value: number | string) {
52 | this.element.style.height = coerceCssPixelValue(value);
53 | }
54 |
55 | @Input()
56 | set fontSize(value: number | string) {
57 | this.element.style.fontSize = coerceCssPixelValue(value);
58 | }
59 |
60 | @Input()
61 | set color(color: string) {
62 | this.element.style.color = color;
63 | }
64 |
65 | private mergedConfig: SVG_CONFIG;
66 | private lastKey!: string;
67 |
68 | constructor(
69 | private host: ElementRef,
70 | private registry: SvgIconRegistry,
71 | @Inject(SVG_ICONS_CONFIG) private config: SVG_CONFIG
72 | ) {
73 | this.mergedConfig = this.createConfig();
74 | this.element.style.fontSize = this.mergedConfig.sizes[this.mergedConfig.defaultSize || 'md'];
75 | }
76 |
77 | get element() {
78 | return this.host.nativeElement;
79 | }
80 |
81 | private createConfig() {
82 | const defaults: SVG_CONFIG = {
83 | sizes: {
84 | xs: '0.5rem',
85 | sm: '0.75rem',
86 | md: '1rem',
87 | lg: '1.5rem',
88 | xl: '2rem',
89 | xxl: '2.5rem',
90 | },
91 | };
92 |
93 | return {
94 | ...defaults,
95 | ...this.config,
96 | };
97 | }
98 | }
99 |
100 | function coerceCssPixelValue(value: any): string {
101 | if (value == null) {
102 | return '';
103 | }
104 |
105 | return typeof value === 'string' ? value : `${value}px`;
106 | }
107 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/svg-icon.module.ts:
--------------------------------------------------------------------------------
1 | import { Inject, ModuleWithProviders, NgModule, Optional, Self } from '@angular/core';
2 |
3 | import { SvgIconRegistry } from './registry';
4 | import { SVG_CONFIG, SVG_ICONS, SVG_ICONS_CONFIG, SVG_MISSING_ICON_FALLBACK, SvgIconType } from './types';
5 |
6 | @NgModule()
7 | export class SvgIconsModule {
8 | static forRoot(config: Partial = {}): ModuleWithProviders {
9 | return {
10 | ngModule: SvgIconsModule,
11 | providers: [
12 | {
13 | provide: SVG_ICONS_CONFIG,
14 | useValue: config,
15 | },
16 | ],
17 | };
18 | }
19 |
20 | static forChild(icons: SvgIconType | SvgIconType[]): ModuleWithProviders {
21 | return {
22 | ngModule: SvgIconsModule,
23 | providers: [{ provide: SVG_ICONS, useValue: icons, multi: true }],
24 | };
25 | }
26 |
27 | constructor(
28 | @Optional() @Self() @Inject(SVG_ICONS) icons: SvgIconType[] | SvgIconType[][],
29 | @Optional() @Inject(SVG_MISSING_ICON_FALLBACK) missingIconFallback: SvgIconType,
30 | private service: SvgIconRegistry
31 | ) {
32 | if (icons) {
33 | this.service.register(([] as SvgIconType[]).concat(...icons));
34 | }
35 |
36 | if (missingIconFallback) {
37 | this.service.register(missingIconFallback);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/svg-icon/types.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 |
3 | export interface SvgIconType {
4 | name: string;
5 | data: string;
6 | }
7 |
8 | export interface SVG_CONFIG {
9 | icons?: SvgIconType | SvgIconType[];
10 | color?: string;
11 | defaultSize?: keyof SVG_CONFIG['sizes'];
12 | missingIconFallback?: SvgIconType;
13 | sizes: {
14 | xs?: string;
15 | sm?: string;
16 | md?: string;
17 | lg?: string;
18 | xl?: string;
19 | xxl?: string;
20 | };
21 | }
22 |
23 | export const SVG_ICONS_CONFIG = new InjectionToken('SVG_ICONS_CONFIG');
24 | export const SVG_ICONS = new InjectionToken('SVG_ICONS');
25 | export const SVG_MISSING_ICON_FALLBACK = new InjectionToken('SVG_MISSING_ICON_FALLBACK');
26 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/toast/toast.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ message.message }}
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/toast/toast.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, Component, ElementRef, Input, OnDestroy, ViewChild } from '@angular/core';
2 | import { Toast } from 'bootstrap';
3 | import { Subscription } from 'rxjs';
4 | import { ToastAction, ToastMessage, ToastService } from './toast.service';
5 | import { CommonModule } from '@angular/common';
6 |
7 | @Component({
8 | selector: 'uib-toast',
9 | standalone: true,
10 | imports: [CommonModule],
11 | templateUrl: './toast.component.html',
12 | styles: [`
13 | .alert {
14 | width: 350px;
15 | max-width: 100%;
16 | box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
17 | z-index: 1046;
18 | }
19 | `]
20 | })
21 | export class ToastComponent implements OnDestroy, AfterViewInit {
22 | @ViewChild('toastRef', {static: true}) el: ElementRef
23 |
24 | @Input() options: Toast.Options = {animation: true, autohide: true, delay: 5000};
25 |
26 | toast: Toast;
27 | message: ToastMessage|null;
28 |
29 | private sub: Subscription;
30 |
31 | constructor(private toastService: ToastService) {
32 | }
33 |
34 | ngAfterViewInit(): void {
35 | this.sub = this.toastService.onToastMessage.subscribe((message: ToastMessage|null) => {
36 | this.message = message;
37 | if (this.el) {
38 | if(message) {
39 | this.toast = Toast.getOrCreateInstance(this.el.nativeElement, message.options || this.options);
40 | this.toast.show();
41 | }
42 | else {
43 | this.toast.hide();
44 | }
45 | } else {
46 | console.warn('Toast not found!');
47 | }
48 | });
49 | }
50 |
51 | ngOnDestroy(): void {
52 | this.sub.unsubscribe();
53 | }
54 |
55 | onAction(action: ToastAction) {
56 | action.action();
57 | if(action.hideToast) {
58 | this.toast.hide();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/toast/toast.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Toast } from 'bootstrap';
3 | import { Subject } from 'rxjs';
4 |
5 | export type BsStyle = 'info' | 'success' | 'warning' | 'danger' | 'primary' | 'secondary' | 'light' | 'dark';
6 |
7 | export interface ToastAction {
8 | text?: string;
9 | style?: string | string;
10 | icon?: string;
11 | hideToast?: boolean;
12 | action: () => void;
13 | }
14 |
15 | export interface ToastMessage {
16 | icon?: string;
17 | message: string;
18 | style?: BsStyle | string;
19 | actions?: ToastAction[];
20 | options?: Toast.Options;
21 | }
22 |
23 | @Injectable({providedIn: 'root'})
24 | export class ToastService {
25 |
26 | onToastMessage = new Subject();
27 |
28 | show(
29 | message: string,
30 | style: BsStyle | string = 'info',
31 | actions?: ToastAction[],
32 | options?: Toast.Options
33 | ) {
34 | this.onToastMessage.next({message, style, actions, options});
35 | }
36 |
37 | hide() {
38 | this.onToastMessage.next(null);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/projects/lib/src/utils/typings.ts:
--------------------------------------------------------------------------------
1 | export type Mutable = {
2 | -readonly [k in keyof T]: T[k];
3 | }
--------------------------------------------------------------------------------
/projects/lib/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin white-stripes {
2 | --stripes-color: hsla(0, 0%, 100%, 0.4);
3 | background-color: #f2f2f2;
4 | background-image: linear-gradient(
5 | 45deg,
6 | var(--stripes-color) 12.5%,
7 | transparent 12.5%,
8 | transparent 50%,
9 | var(--stripes-color) 50%,
10 | var(--stripes-color) 62.5%,
11 | transparent 62.5%,
12 | transparent 100%
13 | );
14 | background-size: 5.66px 5.66px;
15 | }
16 |
17 | /* when an element have 'edited' state */
18 | @mixin edited {
19 | /* display current edited element */
20 | &.edited {
21 | @include white-stripes;
22 |
23 | background-color: lightblue;
24 | outline: 1px dashed lightskyblue;
25 |
26 | /* inset shadow acts as a border */
27 | box-shadow: inset 0px 0px 0px 2px #fff;
28 | }
29 | }
--------------------------------------------------------------------------------
/projects/lib/styles/_selected.scss:
--------------------------------------------------------------------------------
1 | /* when zone is selected apply this style */
2 | .uib-configurable[selected] {
3 | padding: var(--padding);
4 |
5 | .uib-configurable {
6 | margin: var(--margin);
7 | border-radius: 2px;
8 | }
9 |
10 | .uib-container {
11 | padding: var(--padding);
12 | }
13 | }
--------------------------------------------------------------------------------
/projects/lib/styles/_uib-bootstrap.scss:
--------------------------------------------------------------------------------
1 | .uib-bootstrap {
2 | @import "bootstrap/scss/mixins/banner";
3 | @include bsBanner("");
4 |
5 | // scss-docs-start import-stack
6 | // Configuration
7 | @import "bootstrap/scss/functions";
8 | @import "bootstrap/scss/variables";
9 | @import "bootstrap/scss/maps";
10 | @import "bootstrap/scss/mixins";
11 | @import "bootstrap/scss/utilities";
12 | // Note: Custom variable values only support SassScript inside `#{}`.
13 |
14 | // Colors
15 | //
16 | // Generate palettes for full colors, grays, and theme colors.
17 |
18 | @each $color, $value in $colors {
19 | --#{$prefix}#{$color}: #{$value};
20 | }
21 |
22 | @each $color, $value in $grays {
23 | --#{$prefix}gray-#{$color}: #{$value};
24 | }
25 |
26 | @each $color, $value in $theme-colors {
27 | --#{$prefix}#{$color}: #{$value};
28 | }
29 |
30 | @each $color, $value in $theme-colors-rgb {
31 | --#{$prefix}#{$color}-rgb: #{$value};
32 | }
33 |
34 | --#{$prefix}white-rgb: #{to-rgb($white)};
35 | --#{$prefix}black-rgb: #{to-rgb($black)};
36 | --#{$prefix}body-color-rgb: #{to-rgb($body-color)};
37 | --#{$prefix}body-bg-rgb: #{to-rgb($body-bg)};
38 |
39 | // Fonts
40 |
41 | // Note: Use `inspect` for lists so that quoted items keep the quotes.
42 | // See https://github.com/sass/sass/issues/2383#issuecomment-336349172
43 | --#{$prefix}font-sans-serif: #{inspect($font-family-sans-serif)};
44 | --#{$prefix}font-monospace: #{inspect($font-family-monospace)};
45 | --#{$prefix}gradient: #{$gradient};
46 |
47 | // Root and body
48 | // stylelint-disable custom-property-empty-line-before
49 | // scss-docs-start root-body-variables
50 | @if $font-size-root != null {
51 | --#{$prefix}root-font-size: #{$font-size-root};
52 | }
53 | --#{$prefix}body-font-family: #{$font-family-base};
54 | @include rfs($font-size-base, --#{$prefix}body-font-size);
55 | --#{$prefix}body-font-weight: #{$font-weight-base};
56 | --#{$prefix}body-line-height: #{$line-height-base};
57 | --#{$prefix}body-color: #{$body-color};
58 | @if $body-text-align != null {
59 | --#{$prefix}body-text-align: #{$body-text-align};
60 | }
61 | --#{$prefix}body-bg: #{$body-bg};
62 | // scss-docs-end root-body-variables
63 |
64 | // scss-docs-start root-border-var
65 | --#{$prefix}border-width: #{$border-width};
66 | --#{$prefix}border-style: #{$border-style};
67 | --#{$prefix}border-color: #{$border-color};
68 | --#{$prefix}border-color-translucent: #{$border-color-translucent};
69 |
70 | --#{$prefix}border-radius: #{$border-radius};
71 | --#{$prefix}border-radius-sm: #{$border-radius-sm};
72 | --#{$prefix}border-radius-lg: #{$border-radius-lg};
73 | --#{$prefix}border-radius-xl: #{$border-radius-xl};
74 | --#{$prefix}border-radius-2xl: #{$border-radius-2xl};
75 | --#{$prefix}border-radius-pill: #{$border-radius-pill};
76 | // scss-docs-end root-border-var
77 |
78 | --#{$prefix}link-color: #{$link-color};
79 | --#{$prefix}link-hover-color: #{$link-hover-color};
80 |
81 | --#{$prefix}code-color: #{$code-color};
82 |
83 | --#{$prefix}highlight-bg: #{$mark-bg};
84 |
85 | // Layout & components
86 | @import "bootstrap/scss/reboot";
87 | @import "bootstrap/scss/type";
88 | @import "bootstrap/scss/images";
89 | @import "bootstrap/scss/containers";
90 | @import "bootstrap/scss/grid";
91 | @import "bootstrap/scss/tables";
92 | @import "bootstrap/scss/forms";
93 | @import "bootstrap/scss/buttons";
94 | @import "bootstrap/scss/transitions";
95 | @import "bootstrap/scss/dropdown";
96 | @import "bootstrap/scss/button-group";
97 | @import "bootstrap/scss/nav";
98 | @import "bootstrap/scss/navbar";
99 | @import "bootstrap/scss/card";
100 | @import "bootstrap/scss/accordion";
101 | @import "bootstrap/scss/breadcrumb";
102 | @import "bootstrap/scss/pagination";
103 | @import "bootstrap/scss/badge";
104 | @import "bootstrap/scss/alert";
105 | @import "bootstrap/scss/progress";
106 | @import "bootstrap/scss/list-group";
107 | @import "bootstrap/scss/close";
108 | @import "bootstrap/scss/toasts";
109 | @import "bootstrap/scss/modal";
110 | @import "bootstrap/scss/tooltip";
111 | @import "bootstrap/scss/popover";
112 | @import "bootstrap/scss/carousel";
113 | @import "bootstrap/scss/spinners";
114 | @import "bootstrap/scss/offcanvas";
115 | @import "bootstrap/scss/placeholders";
116 |
117 | // Helpers
118 | @import "bootstrap/scss/helpers";
119 |
120 | // Utilities
121 | @import "bootstrap/scss/utilities/api";
122 |
123 |
124 |
125 | // Move reboot body rules in .uib-bootstrap:
126 |
127 | // Body
128 | //
129 | // 1. Remove the margin in all browsers.
130 | // 2. As a best practice, apply a default `background-color`.
131 | // 3. Prevent adjustments of font size after orientation changes in iOS.
132 | // 4. Change the default tap highlight to be completely transparent in iOS.
133 |
134 | // scss-docs-start reboot-body-rules
135 | margin: 0; // 1
136 | font-family: var(--#{$prefix}body-font-family);
137 | @include font-size(var(--#{$prefix}body-font-size));
138 | font-weight: var(--#{$prefix}body-font-weight);
139 | line-height: var(--#{$prefix}body-line-height);
140 | color: var(--#{$prefix}body-color);
141 | text-align: var(--#{$prefix}body-text-align);
142 | background-color: var(--#{$prefix}body-bg); // 2
143 | -webkit-text-size-adjust: 100%; // 3
144 | -webkit-tap-highlight-color: rgba($black, 0); // 4
145 | // scss-docs-end reboot-body-rules
146 | }
147 |
--------------------------------------------------------------------------------
/projects/lib/styles/_zone.scss:
--------------------------------------------------------------------------------
1 | @import "_mixins";
2 |
3 | /* default zone styles */
4 | .uib-zone {
5 | position: relative;
6 |
7 | /* box-model */
8 | /* visual */
9 | border-radius: 4px;
10 |
11 | &:not([selected]) {
12 | outline: 1px dashed lightsteelblue;
13 | }
14 |
15 | /* elements text are not selectable */
16 | &,
17 | * {
18 | user-select: none;
19 | }
20 |
21 | /* elements within zone are clickable */
22 | &,
23 | .cursor-default {
24 | cursor: pointer;
25 | }
26 |
27 | /* edited styles */
28 | &.edited,
29 | .edited {
30 | @include edited;
31 | }
32 |
33 | /* highlight styles sync with tree */
34 | &.highlight,
35 | .uib-dropzone-content.highlight,
36 | .highlight:not(.uib-dropzone-content) {
37 | outline: 2px solid red;
38 | z-index: 99;
39 | }
40 |
41 | .uib-container {
42 | display: flex;
43 | min-width: 32px;
44 | min-height: 32px;
45 | }
46 |
47 |
48 | /* when hovering a zone or a zone's element */
49 | &:hover {
50 | outline: 1px dashed lightsteelblue;
51 | // margin: var(--margin);
52 |
53 | [uib-configurable] {
54 | /* box-model */
55 |
56 | /* visual */
57 | border-radius: 2px;
58 |
59 | &:not(.uib-container, .highlight, .dndDragging, .uib-dropzone-content) {
60 | outline: 1px dashed yellowgreen;
61 | outline-offset: -3px;
62 | }
63 |
64 | /* Deactivate clicks within templates in edit mode */
65 | & > *:not(.uib-dropzone) {
66 | pointer-events: none;
67 | }
68 | }
69 |
70 | .uib-dropzone-content {
71 | outline: 1px dashed steelblue;
72 | }
73 |
74 | /* highlight styles */
75 | &.highlight,
76 | .uib-dropzone-content.highlight {
77 | outline: 2px solid red;
78 | z-index: 99;
79 | }
80 | }
81 | }
82 |
83 | /* while dragging remove outline and add a slight border */
84 | [uib-configurable].dndDragging {
85 | background-color: lightgray;
86 | outline: none !important;
87 | border: 1px solid gray !important;
88 | }
--------------------------------------------------------------------------------
/projects/lib/styles/ui-builder.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --padding: 0;
3 | --margin: 0;
4 | --menu-top: -18px;
5 | --menu-left: 0px;
6 | }
7 |
8 | /* bootstrap encapsulated inside "uib-bootstrap" namespace */
9 | @import "_uib-bootstrap";
10 |
11 | uib-toolbar {
12 | position: fixed;
13 | bottom: 50px;
14 | right: 50px;
15 | z-index: 1046; // just above the configurator
16 | }
17 |
18 | /* zone's styles */
19 | @import "_zone";
20 |
21 | .uib-dropzone {
22 | display: flex;
23 | flex-grow: 1;
24 | }
25 |
26 | [uib-configurable] {
27 | min-height: 8px;
28 | min-width: 8px;
29 | }
30 |
31 | .dragPlaceholder {
32 | @include white-stripes;
33 | outline: 2px dashed orange;
34 |
35 | flex-grow: 1;
36 |
37 | min-height: 16px;
38 | min-width: 16px;
39 |
40 | z-index: 99;
41 | }
42 |
--------------------------------------------------------------------------------
/projects/lib/tsconfig.doc.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/**/*.ts"],
3 | "exclude": ["src/test.ts", "src/**/*.spec.ts"]
4 | }
--------------------------------------------------------------------------------
/projects/lib/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/lib",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "inlineSources": true,
9 | "types": [],
10 | "lib": [
11 | "dom",
12 | "es2022"
13 | ]
14 | },
15 | "exclude": [
16 | "src/test.ts",
17 | "**/*.spec.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/projects/lib/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.lib.json",
4 | "compilerOptions": {
5 | "declarationMap": false
6 | },
7 | "angularCompilerOptions": {
8 | "compilationMode": "partial"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/projects/lib/tsconfig.schematics.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "lib": [
5 | "es2022",
6 | "dom"
7 | ],
8 | "declaration": true,
9 | "esModuleInterop": true,
10 | "module": "commonjs",
11 | "moduleResolution": "node",
12 | "noEmitOnError": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": false,
15 | "noImplicitThis": true,
16 | "noUnusedParameters": true,
17 | "noUnusedLocals": true,
18 | "rootDir": "./schematics",
19 | "outDir": "../../dist/lib/schematics",
20 | "skipDefaultLibCheck": true,
21 | "skipLibCheck": true,
22 | "sourceMap": true,
23 | "strictNullChecks": true,
24 | "target": "ES2022",
25 | "types": [
26 | "node"
27 | ]
28 | },
29 | "include": [
30 | "schematics/**/*"
31 | ],
32 | "exclude": [
33 | "schematics/*/files/**/*"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/projects/lib/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../../out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts"
12 | ],
13 | "include": [
14 | "**/*.spec.ts",
15 | "**/*.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "noImplicitReturns": true,
9 | "esModuleInterop": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "sourceMap": true,
12 | "paths": {
13 | "@sinequa/ngx-ui-builder": [
14 | "dist/lib"
15 | ]
16 | },
17 | "declaration": false,
18 | "experimentalDecorators": true,
19 | "moduleResolution": "node",
20 | "importHelpers": true,
21 | "preserveSymlinks": true,
22 | "noImplicitThis": true,
23 | "noUnusedLocals": true,
24 | "strictFunctionTypes": true,
25 | "strictNullChecks": true,
26 | "target": "ES2022",
27 | "module": "ESNext",
28 | "lib": [
29 | "ES2022",
30 | "dom"
31 | ]
32 | },
33 | "angularCompilerOptions": {
34 | "fullTemplateTypeCheck": true,
35 | "strictInjectionParameters": true
36 | }
37 | }
38 |
--------------------------------------------------------------------------------