diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index e37c07882d..6b1ea80d08 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,14 +1,9 @@ categories: - - title: "Breaking Changes" - labels: - - "BC-break" - - title: "Major Features" + - title: "Major features" labels: - "MAJOR" - - title: "Documentation enhancements" + - title: "Breaking changes" labels: - - "Documentation :books:" -template: | - ## What’s Changed - - $CHANGES + - "BC-break" + - title: "Other changes" +template: $CHANGES diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-changelog.yml similarity index 72% rename from .github/workflows/build-docs.yml rename to .github/workflows/build-changelog.yml index ab60038a44..65b915189e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-changelog.yml @@ -1,4 +1,4 @@ -name: Build Docs +name: Build changelog on: push: @@ -6,10 +6,11 @@ on: - develop jobs: - update_release_draft: + update: + name: Update runs-on: ubuntu-latest steps: - - name: Run Release Drafter + - name: Run uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 88865f78c6..ac84495540 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,4 +1,4 @@ -name: Build Release +name: Build release on: push: diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 410576ec06..0e9e3debee 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -34,10 +34,10 @@ jobs: - name: Setup cache 1/2 id: composer-cache run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Setup cache 2/2 - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-smoke-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} @@ -82,7 +82,7 @@ jobs: php: ['7.4', '8.0', '8.1', '8.2'] type: ['Phpunit', 'Phpunit Lowest'] include: - - php: '8.1' # TODO replace with 'latest' once it represents at least PHP 8.1 + - php: 'latest' type: 'Phpunit Burn' env: LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.0' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" @@ -92,7 +92,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test --entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password" mariadb: image: mariadb - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test postgres: image: postgres:12-alpine env: @@ -121,10 +121,10 @@ jobs: - name: Setup cache 1/2 id: composer-cache run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Setup cache 2/2 - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} @@ -224,10 +224,11 @@ jobs: - name: Upload coverage logs 2/2 (only for latest Phpunit) if: env.LOG_COVERAGE - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - file: coverage/merged.xml + fail_ci_if_error: true + files: coverage/merged.xml behat-test: name: Behat @@ -252,7 +253,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test --entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password" mariadb: image: mariadb - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test + options: --health-cmd="mariadb-admin ping" --health-interval=10s --health-timeout=5s --health-retries=5 -e MYSQL_ROOT_PASSWORD=atk4_pass_root -e MYSQL_USER=atk4_test_user -e MYSQL_PASSWORD=atk4_pass -e MYSQL_DATABASE=atk4_test postgres: image: postgres:12-alpine env: @@ -281,54 +282,64 @@ jobs: - name: Setup cache 1/2 id: composer-cache run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Setup cache 2/2 - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-behat-${{ matrix.php }}-${{ matrix.type }}-${{ hashFiles('composer.json') }} restore-keys: | ${{ runner.os }}-composer- - - name: Install JS dependencies (only for Slow) - if: matrix.type == 'Chrome Slow' - run: | - mv public public.orig - mkdir public public/external - cp public.orig/.gitignore public - cp public.orig/agileui.less public - cp public.orig/logo.png public - cp public.orig/external/.gitignore public/external - cp public.orig/external/package.json public/external - cp public.orig/external/package-lock.json public/external - cp public.orig/external/postinstall.js public/external - npm install --loglevel=error -g pug-cli less less-plugin-clean-css uglify-js - (cd js && npm ci --loglevel=error) - # uncomment and remove line below once Fomantic-UI 2.9.0 is released (cd public/external && npm ci --loglevel=error && git clean -dxfq .) - (cd public/external && npm ci --ignore-scripts --loglevel=error && npm run postinstall && git clean -dxfq .) + - name: Install JS dependencies (only for coverage or Slow) + if: env.LOG_COVERAGE || matrix.type == 'Chrome Slow' + run: | + if [ -n "$LOG_COVERAGE" ]; then + (cd js && npm install --package-lock-only --save-dev babel-plugin-istanbul nyc && npm ci --loglevel=error) + else + mv public public.orig + mkdir public public/css public/external + cp public.orig/.gitattributes public + cp public.orig/.gitignore public + cp public.orig/logo.png public + cp public.orig/css/agileui.less public/css + cp public.orig/external/.gitignore public/external + cp public.orig/external/package.json public/external + cp public.orig/external/package-lock.json public/external + cp public.orig/external/postinstall.js public/external + npm install --loglevel=error -g pug-cli less less-plugin-clean-css uglify-js + (cd js && npm ci --loglevel=error) + (cd public/external && npm ci --loglevel=error && git clean -dxfq .) + fi - name: Lint JS files (only for Slow) if: matrix.type == 'Chrome Slow' run: | - (cd js && npm run lint ../public/external/\*.js) + cp public/external/postinstall.js js + (cd js && npm run lint) - name: Compile HTML files (only for Slow) if: matrix.type == 'Chrome Slow' run: | cp -r template template.orig find template -not -type d -not -name '*.pug' -delete - (cd template && pug --silent --pretty .) + (cd template && pug --doctype html --pretty --silent .) - name: Compile CSS files (only for Slow) if: matrix.type == 'Chrome Slow' run: | - lessc public/agileui.less public/agileui.min.css --clean-css="--s1 --advanced" --source-map + lessc public/css/agileui.less public/css/agileui.min.css --clean-css="--s1 --advanced" --source-map - - name: Compile JS files (only for Slow) - if: matrix.type == 'Chrome Slow' + - name: Compile JS files (only for coverage or Slow) + if: env.LOG_COVERAGE || matrix.type == 'Chrome Slow' run: | - (cd js && npm run build) + if [ -n "$LOG_COVERAGE" ]; then + rm -r public/js + (cd js && ISTANBUL_COVERAGE=1 npm run build) + else + (cd js && npm run build) + fi - name: Diff compiled files (only for Slow) if: matrix.type == 'Chrome Slow' @@ -352,7 +363,7 @@ jobs: php -r '(new PDO("mysql:host=mariadb", "root", "atk4_pass_root"))->exec("ALTER USER '"'"'atk4_test_user'"'"'@'"'"'%'"'"' WITH MAX_USER_CONNECTIONS 5");' php -r '(new PDO("pgsql:host=postgres;dbname=atk4_test", "atk4_test_user", "atk4_pass"))->exec("ALTER ROLE atk4_test_user CONNECTION LIMIT 1");' /usr/lib/oracle/setup.sh - if [ -n "$LOG_COVERAGE" ]; then mkdir coverage; fi + if [ -n "$LOG_COVERAGE" ]; then mkdir coverage coverage/js; fi ci_wait_until () { timeout 30 sh -c "until { $1 2> /dev/null; }; do sleep 0.02; done" || timeout 15 sh -c "$1" || { echo "health timeout: $1"; exit 1; }; } php -d opcache.enable_cli=1 -S 127.0.0.1:8888 > /dev/null 2>&1 & ci_wait_until 'nc -w 1 127.0.0.1 8888' @@ -425,15 +436,47 @@ jobs: php demos/_demo-data/create-db.php vendor/bin/behat -vv --config behat.yml.dist - - name: Upload coverage logs 1/2 (only for latest Chrome) + - name: Upload coverage logs 1/2 (only for coverage) if: env.LOG_COVERAGE run: | ls -l coverage | wc -l php -d memory_limit=2G vendor/bin/phpcov merge coverage/ --clover coverage/merged.xml - - - name: Upload coverage logs 2/2 (only for latest Chrome) + ls -l coverage/js | wc -l + (cd js && npx nyc report --temp-dir ../coverage/js --report-dir ../coverage/js -e vue --reporter=clover) + # fix never reached condition is rendered to clover with falsecount > 0 + # https://github.com/istanbuljs/istanbuljs/issues/695 + sed -i -E 's~count="0" type="cond" truecount="0" falsecount="[1-9]+[0-9]*"~count="0" type="cond" truecount="0" falsecount="0"~' coverage/js/clover.xml + sed -i -E 's~count="[0-9]+" type="cond" truecount="[1-9]+[0-9]*" falsecount="[0-9]+"~count="1" type="cond" truecount="1" falsecount="1"~' coverage/js/clover.xml + + - name: Upload coverage logs 2/2 (only for coverage) if: env.LOG_COVERAGE - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - file: coverage/merged.xml + fail_ci_if_error: true + files: coverage/merged.xml,coverage/js/clover.xml + + docs-test: + name: Docs + runs-on: ubuntu-latest + container: + image: ghcr.io/mvorisek/image-php:latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Python and dependencies + run: | + apk add python3 py3-pip + python --version + (cd docs && pip install -r requirements.txt) + + - name: Build + run: | + mv docs/baseline.txt docs/baseline.orig.txt + (cd docs && python -m sphinx -T -b html . out 2>&1 | tee baseline.txt) + sed -i -r 's~[^:]*/docs/([^:]*:)([0-9]+:)?~\1~;t;d' docs/baseline.txt + + - name: Diff build baseline + run: | + diff -u docs/baseline.orig.txt docs/baseline.txt diff --git a/.gitignore b/.gitignore index f91a9d78d2..6dfde935bf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ cache *.cache.* /demos/db.php +/demos/db-behat-rw.txt /demos/_demo-data/db.sqlite /demos/_demo-data/db.sqlite-journal /phpunit.xml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 5797add99b..97a57e9e77 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -9,8 +9,8 @@ ->setRules([ '@PhpCsFixer' => true, '@PhpCsFixer:risky' => true, - '@PHP74Migration:risky' => true, '@PHP74Migration' => true, + '@PHP74Migration:risky' => true, // required by PSR-12 'concat_space' => [ @@ -18,11 +18,6 @@ ], // disable some too strict rules - 'phpdoc_types' => [ - // keep enabled, but without "alias" group to not fix - // "Callback" to "callback" in phpdoc - 'groups' => ['simple', 'meta'], - ], 'phpdoc_types_order' => [ 'null_adjustment' => 'always_last', 'sort_algorithm' => 'none', diff --git a/README.md b/README.md index f129c86712..6535f8cd25 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Agile UI - User Interface framework for Agile Toolkit +# Agile UI - Robust and easy to use PHP Framework for Web Apps -[Agile Toolkit](https://agiletoolkit.org/) is a Low Code framework written in PHP. Agile UI implement server side rendering engine and over 50 UI generic components for interacting with your Data Model. +Agile UI implement server side rendering engine and over 50 UI generic components for interacting with your data. -Agile UI is quickest way for building back-end UI, admin interfaces, data management systems for medium and large projects designed around roles, complex logic, formulas. +Agile UI is the quickest way for building back-end UI, admin interfaces, data management systems for medium and large projects designed around roles, complex logic, formulas... - Agile UI relies on abstract data. It could be stored in SQL, NoSQL or in external API. - Agile UI adjusts to your data model. If you change your model structure, UI will reflect that. @@ -11,12 +11,12 @@ Agile UI is quickest way for building back-end UI, admin interfaces, data manage - Agile UI is compact - single file, several lines of code - that's all it takes. - Agile UI is extensible - integrates VueJS for custom components and interactive behaviours. -![Build](https://github.com/atk4/ui/workflows/Unit%20Testing/badge.svg) +[![Build](https://github.com/atk4/ui/workflows/Unit/badge.svg)](https://github.com/atk4/ui/actions?query=workflow%3AUnit+branch%3Adevelop) [![CodeCov](https://codecov.io/gh/atk4/ui/branch/develop/graph/badge.svg)](https://codecov.io/gh/atk4/ui) [![GitHub release](https://img.shields.io/github/release/atk4/ui.svg)](CHANGELOG.md) [![Code Climate](https://codeclimate.com/github/atk4/ui/badges/gpa.svg)](https://codeclimate.com/github/atk4/ui) -Quick-Links: [Documentation](https://agile-ui.readthedocs.io). [Demo-site](https://ui.agiletoolkit.org). [ATK Data](https://github.com/atk4/data). [Forum](https://forum.agiletoolkit.org/). [Chat](https://gitter.im/atk4/atk4). [Commercial support](https://www.agiletoolkit.org/contact). +Quick-Links: [Documentation](https://atk4-ui.readthedocs.io/). [Demo-site](https://ui.atk4.org/). [ATK Data](https://github.com/atk4/data). [Forum](https://forum.agiletoolkit.org/). [Chat](https://gitter.im/atk4/atk4). [Commercial support](https://www.agiletoolkit.org/contact). ## How does Agile Toolkit work? @@ -37,19 +37,15 @@ high-level projects developed entirely on Agile Toolkit. ### Who uses Agile Toolkit? -Companies use Agile Toolkit to implement admin interface and in some cases even user-facing interface. - - - www.linkedfinance.com - - www.sortmybooks.com - - If you have a project built with Agile Toolkit - add it here! +Many companies use Agile Toolkit to implement admin interface and in some cases even user-facing interface. ### How does it work? -Download from www.agiletoolkit.org or Install ATK UI with `composer require atk4/ui` +Download from https://ui.atk4.org/ or install ATK UI with `composer require atk4/ui` Create "index.php" file with: -``` php +```php addField('email'); $form->onSubmit(function (Form $form) { // implement subscribe here - return $form->success('Subscribed ' . $form->model->get('email') . ' to newsletter.'); + return $form->jsSuccess('Subscribed ' . $form->model->get('email') . ' to newsletter.'); }); // decorate anything @@ -76,16 +72,16 @@ Open PHP in the browser and observe a fully working and good looking form: ![subscribe](docs/images/subscribe.png) -ATK UI relies on https://fomantic-ui.com CSS framework to render the form beautifully. It also implements submission call-back in a very straightforward way. The demo also demonstrates use of JavaScript action, which can make objects interract with each-other (e.g. Form submit reloads Table). +ATK UI relies on https://fomantic-ui.com CSS framework to render the form beautifully. It also implements submission callback in a very straightforward way. The demo also demonstrates use of JavaScript action, which can make objects interact with each-other (e.g. Form submit reloads Table). ### Database Integration with ATK Data To get most of ATK UI, use [ATK Data](https://github.com/atk4/data) to describe your business models such as "User" or "Purchase". When you define models, you can start using some more advanced components: -[Crud](https://ui.agiletoolkit.org/demos/crud.php) is a fully-interractive component that supports pagination, reloading, conditions, data formatting, sorting, quick-search, ordering, custom actions and modals, but at the same time is very easy to use: +[Crud](https://ui.atk4.org/demos/crud.php) is a fully-interactive component that supports pagination, reloading, conditions, data formatting, sorting, quick-search, ordering, custom actions and modals, but at the same time is very easy to use: -``` php -$app = new \Atk4\Ui\App('hello world'); +```php +$app = new \Atk4\Ui\App(['title' => 'hello world']); $app->initLayout([\Atk4\Ui\Layout\Admin::class]); $app->db = \Atk4\Data\Persistence::connect('mysql://user:pass@localhost/atk'); @@ -95,7 +91,7 @@ $app->db = \Atk4\Data\Persistence::connect('mysql://user:pass@localhost/atk'); ATK Data allows you to set up relations between models: -``` php +```php class User extends Model { protected function init(): void @@ -111,16 +107,15 @@ class User extends Model Conventional Crud works only with a single model, but with add-on you can take advantage this relationship information: https://github.com/atk4/mastercrud -``` php +```php use \Atk4\Mastercrud\MasterCrud; // set up $app here -$master_crud = MasterCrud::addTo($app) +$masterCrud = MasterCrud::addTo($app) ->setModel(new User($app->db), [ 'Purchases' => [], ]); - ``` ### Agile UI can be styled @@ -133,7 +128,7 @@ It's easy to create your own application styling. Here are some example UI: As of version 2.0 - Agile Toolkit offers support for User Actions. Those are easy to define in your Data Model declaration: -``` php +```php $this->addUserAction('archive', function (Model $m) { $m->set('is_archived', true); $this->saveAndUnload(); @@ -151,7 +146,7 @@ Agile UI has some unique features: One of the fundamental features of ATK is Callback - ability to dynamically generate a route then have JS part of the component invoke it. Thanks to this approach, code can be fluid, simple and readable: -``` php +```php $tabs = \Atk4\Ui\Tabs::addTo($app); \Atk4\Ui\Message::addTo($tabs->addTab('Intro'), ['Other tabs are loaded dynamically!']); @@ -178,13 +173,13 @@ Another component implementation using a very friendly PHP syntax: ![wizard](docs/images/wizard.png) -You get most benefit when you use various ATK UI Components together. Try the following demo: https://ui.agiletoolkit.org/demos/interactive/wizard.php. The demo implements: +You get most benefit when you use various ATK UI Components together. Try the following demo: https://ui.atk4.org/demos/interactive/wizard.php. The demo implements: -- Multi-step wizard with ability to navigate forward and backward -- Form with validation -- Data memorization in the session -- Table with column formatter, Messages -- Real-time output console +- Multi-step wizard with ability to navigate forward and backward +- Form with validation +- Data memorization in the session +- Table with column formatter, Messages +- Real-time output console With ATK it [takes about 50 lines of PHP code only](https://github.com/atk4/ui/blob/develop/demos/interactive/wizard.php) to build it all. @@ -192,7 +187,7 @@ With ATK it [takes about 50 lines of PHP code only](https://github.com/atk4/ui/b It's really easy to put together a complex Admin system. Add this code to a new PHP file (tweak it with your database details, table and fields): -``` php +```php =7.4 <8.3", "atk4/data": "dev-develop", - "symfony/filesystem": "^4.4 || ^5.3", - "symfony/http-foundation": "^4.4 || ^5.3" + "nyholm/psr7": "^1.4", + "nyholm/psr7-server": "^1.0", + "symfony/filesystem": "^4.4 || ^5.3 || ^6.0", + "symfony/http-foundation": "^4.4 || ^5.3 || ^6.0" }, "require-release": { "php": ">=7.4 <8.3", - "atk4/data": "~4.0.0", - "symfony/filesystem": "^4.4 || ^5.3", - "symfony/http-foundation": "^4.4 || ^5.3" + "atk4/data": "~5.0.0", + "nyholm/psr7": "^1.4", + "nyholm/psr7-server": "^1.0", + "symfony/filesystem": "^4.4 || ^5.3 || ^6.0", + "symfony/http-foundation": "^4.4 || ^5.3 || ^6.0" }, "require-dev": { + "atk4/behat-mink-selenium2-driver": "^1.6.1", "behat/mink-extension": "^2.3.1", - "behat/mink-selenium2-driver": "^1.5", "ergebnis/composer-normalize": "^2.13", "friendsofphp/php-cs-fixer": "^3.0", "fzaninotto/faker": "^1.6", - "guzzlehttp/guzzle": "^6.3", + "guzzlehttp/guzzle": "^7.3", "johnkary/phpunit-speedtrap": "^3.3", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.3", "phpunit/phpunit": "^9.5.5", - "symfony/process": "^4.4.30 || ^5.3.7" + "symfony/process": "^4.4.30 || ^5.3.7 || ^6.0" }, "conflict": { "behat/behat": "<3.9", "behat/mink": "<1.9", - "instaclick/php-webdriver": "<1.4.13", + "guzzlehttp/psr7": "<2.4", "symfony/console": "<4.4.30 || >=5 <5.3.7", "symfony/css-selector": "<4.4.24 || >=5 <5.2.9", "symfony/filesystem": "<4.4.30 || >=5 <5.3.7", diff --git a/demos/Dockerfile b/demos/Dockerfile index ac7b982eca..aeb0d1b13e 100644 --- a/demos/Dockerfile +++ b/demos/Dockerfile @@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y \ && docker-php-ext-install intl \ && docker-php-ext-install pdo pdo_mysql -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ +RUN curl -sL https://deb.nodesource.com/setup_current.x | bash - \ && apt-get update && apt-get -y install nodejs \ && npm install -g npm @@ -27,7 +27,7 @@ COPY js js COPY public public RUN cd js && npm ci && npm run build -RUN cd public && lessc agileui.less agileui.css +RUN cd public/css && lessc agileui.less agileui.css ADD composer.json . RUN jq 'del(."require-release")|del(."require-dev")' < composer.json > tmp && mv tmp composer.json \ @@ -40,6 +40,8 @@ COPY src src COPY template template COPY demos demos RUN echo " index.php +RUN sed -E "s/(\\\$minified = )true;/\\1false;/g" -i src/App.php RUN php demos/_demo-data/create-db.php +RUN chown -R www-data:www-data demos/_demo-data RUN sed -E "s/\(('sqlite:.+)\);/(\$_ENV['DB_DSN'] ?? \\1, \$_ENV['DB_USER'] ?? null, \$_ENV['DB_PASSWORD'] ?? null);/g" -i demos/db.default.php diff --git a/demos/_demo-data/create-db.php b/demos/_demo-data/create-db.php index 02904219f2..c91c58aaa9 100644 --- a/demos/_demo-data/create-db.php +++ b/demos/_demo-data/create-db.php @@ -62,7 +62,12 @@ public function import(array $rowsMulti) return parent::import(array_map(function (array $rows): array { $rowsPrefixed = []; foreach ($rows as $k => $v) { - $rowsPrefixed[$this->prefixFieldName($k)] = $v; + $field = $this->getField($this->prefixFieldName($k)); + if (in_array($field->type, ['date', 'time', 'datetime'], true)) { + $v = new \DateTime($v . ' GMT'); + } + + $rowsPrefixed[$field->shortName] = $v; } return $rowsPrefixed; @@ -1100,11 +1105,6 @@ public function import(array $rowsMulti) ['id' => 3, 'project_name' => 'Agile Data', 'project_code' => 'at03', 'description' => 'Agile Data implements an entirely new pattern for data abstraction, that is specifically designed for remote databases such as RDS, Cloud SQL, BigQuery and other distributed data storage architectures. It focuses on reducing number of requests your App have to send to the Database by using more sophisticated queries while also offering full Domain Model mapping and Database vendor abstraction.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 1, 'project_budget' => 12000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 300, 'project_hours_reported' => 394, 'project_expenses_est' => 600, 'project_expenses' => 430, 'project_mgmt_cost_pct' => 0.2, 'project_qa_cost_pct' => 0.3, 'start_date' => '2016-04-17', 'finish_date' => '2016-06-20', 'finish_time' => '03:04:00', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'], ['id' => 4, 'project_name' => 'Agile UI', 'project_code' => 'at04', 'description' => 'Web UI Component library.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 0, 'project_budget' => 20000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 600, 'project_hours_reported' => 368, 'project_expenses_est' => 1200, 'project_expenses' => 0, 'project_mgmt_cost_pct' => 0.3, 'project_qa_cost_pct' => 0.4, 'start_date' => '2016-09-17', 'finish_date' => '', 'finish_time' => '', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'], ]; -foreach ($data as $rowIndex => $row) { - foreach (['start_date', 'finish_date', 'finish_time', 'created', 'updated'] as $k) { - $data[$rowIndex][$k] = new \DateTime($row[$k] . ' GMT'); - } -} $model->import($data); $model = new ImportModelWithPrefixedFields($db, ['table' => 'product_category']); @@ -1148,4 +1148,27 @@ public function import(array $rowsMulti) ['id' => 7, 'name' => 'Ice Cream', 'brand' => 'Milk Corp.', 'product_category_id' => 3, 'product_sub_category_id' => 8], ]); +$model = new ImportModelWithPrefixedFields($db, ['table' => 'multiline_item']); +$model->addField('item', ['type' => 'string']); +$model->addField('inv_date', ['type' => 'date']); +$model->addField('inv_time', ['type' => 'time']); +$model->addField('country_id', ['type' => 'bigint']); +$model->addField('qty', ['type' => 'integer']); +$model->addField('box', ['type' => 'integer']); +(new Migrator($model))->create(); +$model->import([ + ['id' => 1, 'item' => 'Chocolate', 'inv_date' => '2020-02-20', 'inv_time' => '7:20', 'country_id' => 80, 'qty' => 7, 'box' => 5], + ['id' => 2, 'item' => 'DAP delivery', 'inv_date' => '2020-02-01', 'inv_time' => '8:33', 'country_id' => 223, 'qty' => 2, 'box' => 100], +]); + +$model = new ImportModelWithPrefixedFields($db, ['table' => 'multiline_delivery']); +$model->addField('name', ['type' => 'string']); +$model->addField('country', ['type' => 'json']); +$model->addField('items', ['type' => 'json']); +(new Migrator($model))->create(); +$model->import([ + // TODO Model::containsXxx support + // https://github.com/atk4/ui/issues/1860 +]); + echo 'import complete!' . "\n\n"; diff --git a/demos/_includes/Counter.php b/demos/_includes/Counter.php index c08657b41f..3d65543429 100644 --- a/demos/_includes/Counter.php +++ b/demos/_includes/Counter.php @@ -6,7 +6,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; -use Atk4\Ui\JsExpression; +use Atk4\Ui\Js\JsExpression; class Counter extends Form\Control\Line { @@ -19,7 +19,7 @@ protected function init(): void $this->actionLeft = new Button(['icon' => 'minus']); $this->action = new Button(['icon' => 'plus']); - $this->actionLeft->js('click', $this->jsInput()->val(new JsExpression('parseInt([]) - 1', [$this->jsInput()->val()]))); - $this->action->js('click', $this->jsInput()->val(new JsExpression('parseInt([]) + 1', [$this->jsInput()->val()]))); + $this->actionLeft->on('click', $this->jsInput()->val(new JsExpression('parseInt([]) - 1', [$this->jsInput()->val()]))); + $this->action->on('click', $this->jsInput()->val(new JsExpression('parseInt([]) + 1', [$this->jsInput()->val()]))); } } diff --git a/demos/_includes/Demo.php b/demos/_includes/Demo.php index 7c8658d4d3..384d6f807b 100644 --- a/demos/_includes/Demo.php +++ b/demos/_includes/Demo.php @@ -6,7 +6,8 @@ use Atk4\Ui\Columns; use Atk4\Ui\Exception; -use Atk4\Ui\JsChain; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsFunction; use Atk4\Ui\View; class Demo extends Columns @@ -19,9 +20,6 @@ class Demo extends Columns /** @var bool */ public static $isInitialized = false; - /** @var string */ - public $highlightDefaultStyle = 'dark'; - /** @var int */ public $leftWidth = 8; /** @var int */ @@ -59,12 +57,18 @@ protected function extractCodeFromClosure(\Closure $fx): string }, $codeArr)); } + /** + * @param \Closure(View): void $fx + */ public function setCodeAndCall(\Closure $fx, string $lang = 'php'): void { $code = $this->extractCodeFromClosure($fx); $this->highLightCode(); - View::addTo(View::addTo($this->left, ['element' => 'pre']), ['element' => 'code'])->addClass($lang)->set($code); + View::addTo(View::addTo($this->left, ['element' => 'pre']), ['element' => 'code']) + ->addClass('language-' . $lang) + ->set($code) + ->js(true)->each(new JsFunction(['i, el'], [new JsExpression('hljs.highlightElement(el)')])); $fx($this->right); } @@ -72,9 +76,8 @@ public function setCodeAndCall(\Closure $fx, string $lang = 'php'): void public function highLightCode(): void { if (!self::$isInitialized) { - $this->getApp()->requireCss('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.16.2/styles/' . $this->highlightDefaultStyle . '.min.css'); - $this->getApp()->requireJs('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.16.2/highlight.min.js'); - $this->js(true, (new JsChain('hljs'))->initHighlighting()); + $this->getApp()->requireCss($this->getApp()->cdn['highlight.js'] . '/styles/github-dark-dimmed.min.css'); + $this->getApp()->requireJs($this->getApp()->cdn['highlight.js'] . '/highlight.min.js'); self::$isInitialized = true; } } diff --git a/demos/_includes/DemoActionsUtil.php b/demos/_includes/DemoActionsUtil.php index b19fa332a0..4617cb3e6d 100644 --- a/demos/_includes/DemoActionsUtil.php +++ b/demos/_includes/DemoActionsUtil.php @@ -56,7 +56,7 @@ public static function setupDemoActions(Country $country): void }, ]); - $country->addUserAction('edit_argument_prev', [ + $country->addUserAction('edit_argument_preview', [ 'caption' => 'Argument/Preview', 'description' => 'Ask for argument "Age" and display preview prior to execute', 'args' => [ @@ -99,7 +99,7 @@ public static function setupDemoActions(Country $country): void 'caption' => 'User Confirmation', 'description' => 'Confirm the action using a ConfirmationExecutor', 'confirmation' => function (UserAction $a) { - $iso3 = $a->getEntity()->get(Country::hinting()->fieldName()->iso3); + $iso3 = Country::assertInstanceOf($a->getEntity())->iso3; return 'Are you sure you want to perform this action on: ' . $a->getEntity()->getTitle() . ' (' . $iso3 . ')'; }, diff --git a/demos/_includes/DemoLookup.php b/demos/_includes/DemoLookup.php deleted file mode 100644 index 6422dc8e87..0000000000 --- a/demos/_includes/DemoLookup.php +++ /dev/null @@ -1,66 +0,0 @@ -plus) { - return; - } - - if ($this->plus === true) { - $this->plus = 'Add New'; - } - - if (is_string($this->plus)) { - $this->plus = ['button' => $this->plus]; - } - - $buttonSeed = $this->plus['button'] ?? []; - if (is_string($buttonSeed)) { - $buttonSeed = ['content' => $buttonSeed]; - } - - $defaultSeed = [Button::class, 'class.disabled' => $this->disabled || $this->readOnly]; - $this->action = Factory::factory(array_merge($defaultSeed, $buttonSeed)); - - $vp = VirtualPage::addTo($this->form ?? $this->getOwner()); - $vp->set(function (VirtualPage $vp) { - $form = Form::addTo($vp); - - $entity = $this->model->createEntity(); - $form->setModel($entity, $this->plus['fields'] ?? null); - - $form->onSubmit(function (Form $form) { - $form->model->save(); - - $ret = [ - new JsToast('Form submit!. Data are not save in demo mode.'), - (new Jquery('.atk-modal'))->modal('hide'), - ]; - - $row = $this->renderRow($form->model); - $chain = new Jquery('#' . $this->name . '-ac'); - $chain->dropdown('set value', $row['value'])->dropdown('set text', $row['title']); - $ret[] = $chain; - - return $ret; - }); - }); - - $caption = $this->plus['caption'] ?? 'Add New ' . $this->model->getModelCaption(); - $this->action->js('click', new JsModal($caption, $vp)); - } -} diff --git a/demos/_includes/FlyersForm.php b/demos/_includes/FlyersForm.php index 65f9ba5851..64698313e1 100644 --- a/demos/_includes/FlyersForm.php +++ b/demos/_includes/FlyersForm.php @@ -7,7 +7,7 @@ use Atk4\Data\Model; use Atk4\Data\Persistence; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; class FlyersForm extends Form { diff --git a/demos/_includes/PromotionText.php b/demos/_includes/PromotionText.php index 3eca0128d7..084938a149 100644 --- a/demos/_includes/PromotionText.php +++ b/demos/_includes/PromotionText.php @@ -17,22 +17,18 @@ protected function init(): void parent::init(); $t = Text::addTo($this); - $t->addParagraph( - <<< 'EOF' - Agile Toolkit base package includes: - EOF - ); - - $t->addHtml( - <<< 'HTML' - - HTML - ); + $t->addParagraph(<<<'EOF' + Agile Toolkit base package includes: + EOF); + + $t->addHtml(<<<'EOF' + + EOF); $gl = GridLayout::addTo($this, ['class.stackable divided' => true, 'columns' => 4]); Button::addTo($gl, ['Explore UI components', 'class.primary basic fluid' => true, 'iconRight' => 'right arrow'], ['r1c1']) diff --git a/demos/_includes/ReloadTest.php b/demos/_includes/ReloadTest.php index 830e3eafcd..2688854f4a 100644 --- a/demos/_includes/ReloadTest.php +++ b/demos/_includes/ReloadTest.php @@ -4,7 +4,7 @@ namespace Atk4\Ui\Demos; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Label; use Atk4\Ui\View; diff --git a/demos/_includes/ViewTester.php b/demos/_includes/ViewTester.php index 3f9b62f112..237577aec0 100644 --- a/demos/_includes/ViewTester.php +++ b/demos/_includes/ViewTester.php @@ -4,8 +4,8 @@ namespace Atk4\Ui\Demos; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Label; use Atk4\Ui\View; @@ -27,7 +27,7 @@ protected function init(): void $label->detail = 'success'; } else { $this->js(true, $reload); - $this->js(true, new JsExpression('var s = Date.now(); var i=setInterval(function() { var p = Date.now()-s; var el=$[]; el.find(".detail").text(p+"ms"); if(el.is(".green")) { clearInterval(i); }}, 100)', [$label])); + $this->js(true, new JsExpression('var s = Date.now(); var i = setInterval(function () { var p = Date.now() - s; var el = $([]); el.find(\'.detail\').text(p + \'ms\'); if (el.is(\'.green\')) { clearInterval(i); }}, 100)', [$label])); } } } diff --git a/demos/_unit-test/calendar-input.php b/demos/_unit-test/calendar-input.php deleted file mode 100644 index c57c144b71..0000000000 --- a/demos/_unit-test/calendar-input.php +++ /dev/null @@ -1,70 +0,0 @@ -invokeInit(); - $view->text->addHtml($dt === null ? 'empty' : $dt->format($format)); - - return $view; -}; - -Header::addTo($app, ['Testing flatpickr using Behat']); -$form = Form::addTo($app); -$c = $form->addControl('field', [], ['type' => 'date']); -$form->buttonSave->set($c->shortName); - -$form->onSubmit(function (Form $form) use ($output, $c, $app) { - return $output($form->model->get($c->shortName), $app->uiPersistence->dateFormat); -}); - -View::addTo($app, ['ui' => 'hidden divider']); -$app->uiPersistence->dateFormat = 'Y-m-d'; -$form = Form::addTo($app); -$c = $form->addControl('date_ymd', [Form\Control\Calendar::class, 'type' => 'date']); -$form->buttonSave->set($c->shortName); - -$form->onSubmit(function (Form $form) use ($output, $c, $app) { - return $output($form->model->get($c->shortName), $app->uiPersistence->dateFormat); -}); - -View::addTo($app, ['ui' => 'hidden divider']); -$app->uiPersistence->timeFormat = 'H:i:s'; -$form = Form::addTo($app); -$c = $form->addControl('time_24hr', [Form\Control\Calendar::class, 'type' => 'time']); -$form->buttonSave->set($c->shortName); - -$form->onSubmit(function (Form $form) use ($output, $c, $app) { - return $output($form->model->get($c->shortName), $app->uiPersistence->timeFormat); -}); - -View::addTo($app, ['ui' => 'hidden divider']); -$app->uiPersistence->timeFormat = 'G:i A'; -$form = Form::addTo($app); -$c = $form->addControl('time_am', [Form\Control\Calendar::class, 'type' => 'time']); -$form->buttonSave->set($c->shortName); - -$form->onSubmit(function (Form $form) use ($output, $c, $app) { - return $output($form->model->get($c->shortName), $app->uiPersistence->timeFormat); -}); - -View::addTo($app, ['ui' => 'hidden divider']); -$app->uiPersistence->datetimeFormat = 'Y-m-d (H:i:s)'; -$form = Form::addTo($app); -$c = $form->addControl('datetime', [Form\Control\Calendar::class, 'type' => 'datetime']); -$form->buttonSave->set($c->shortName); - -$form->onSubmit(function (Form $form) use ($output, $c, $app) { - return $output($form->model->get($c->shortName), $app->uiPersistence->datetimeFormat); -}); diff --git a/demos/_unit-test/callback-nested.php b/demos/_unit-test/callback-nested.php index 9e99c758fa..e9c8a8720a 100644 --- a/demos/_unit-test/callback-nested.php +++ b/demos/_unit-test/callback-nested.php @@ -54,7 +54,9 @@ $c->setModel($m, [$m->fieldName()->name]); }); }); - Button::addTo($p, ['Load2'])->js('click', $loaderSub->jsLoad()); + Button::addTo($p, ['Load2']) + ->on('click', $loaderSub->jsLoad()); }); -Button::addTo($app, ['Load1'])->js('click', $loader->jsLoad()); +Button::addTo($app, ['Load1']) + ->on('click', $loader->jsLoad()); diff --git a/demos/_unit-test/callback.php b/demos/_unit-test/callback.php index cf2a9d3dcc..0aa398033a 100644 --- a/demos/_unit-test/callback.php +++ b/demos/_unit-test/callback.php @@ -6,9 +6,10 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsModal; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsModal; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Table; use Atk4\Ui\VirtualPage; @@ -33,9 +34,9 @@ $form->onSubmit(function (Form $form) use ($table) { $form->model->save(); - return [ + return new JsBlock([ $table->jsReload(), new JsToast('Save'), - (new Jquery('.ui.modal.visible.active.front'))->modal('hide'), - ]; + (new Jquery())->closest('.ui.modal')->modal('hide'), + ]); }); diff --git a/demos/_unit-test/console_run.php b/demos/_unit-test/console_run.php index 35a993ca91..db084d1488 100644 --- a/demos/_unit-test/console_run.php +++ b/demos/_unit-test/console_run.php @@ -12,7 +12,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/** @var View $testRunClass */ $testRunClass = AnonymousClassNameCache::get_class(fn () => new class() extends View { use DebugTrait; diff --git a/demos/_unit-test/crud-nested.php b/demos/_unit-test/crud-nested.php index d97e29d2a7..25e0511e9f 100644 --- a/demos/_unit-test/crud-nested.php +++ b/demos/_unit-test/crud-nested.php @@ -16,7 +16,7 @@ $crud->setModel($model); $crud->addModalAction(['icon' => 'book'], 'Edit product category', function (View $v, $id) use ($model) { - $entity = (clone $model)->load($id); + $entity = $model->load($id); $innerCrud = Crud::addTo($v); $innerCrud->setModel($entity->Products); diff --git a/demos/_unit-test/crud.php b/demos/_unit-test/crud.php index da2b3bc4cf..f067b085f0 100644 --- a/demos/_unit-test/crud.php +++ b/demos/_unit-test/crud.php @@ -10,11 +10,10 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// reset to default button $app->getExecutorFactory()->useTriggerDefault(ExecutorFactory::TABLE_BUTTON); $model = new Country($app->db); $crud = Crud::addTo($app, ['ipp' => 10, 'menu' => ['class' => ['atk-grid-menu']]]); $crud->setModel($model); -$crud->addQuickSearch([$model->fieldName()->name], true); +$crud->addQuickSearch([$model->fieldName()->name, $model->fieldName()->phonecode], true); diff --git a/demos/_unit-test/exception.php b/demos/_unit-test/exception.php index 7275833b00..0c1622680c 100644 --- a/demos/_unit-test/exception.php +++ b/demos/_unit-test/exception.php @@ -8,11 +8,11 @@ use Atk4\Ui\CallbackLater; use Atk4\Ui\Modal; +// test Exception and Error throw + /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// JUST TO TEST Exceptions and Error throws - $cb = CallbackLater::addTo($app); $cb->setUrlTrigger('m_cb'); @@ -24,7 +24,7 @@ }); $button = Button::addTo($app, ['Test modal exception']); -$button->on('click', $modal->show()); +$button->on('click', $modal->jsShow()); $cb1 = CallbackLater::addTo($app, ['urlTrigger' => 'm2_cb']); $modal2 = Modal::addTo($app, ['cb' => $cb1]); @@ -34,4 +34,4 @@ }); $button2 = Button::addTo($app, ['Test modal error']); -$button2->on('click', $modal2->show()); +$button2->on('click', $modal2->jsShow()); diff --git a/demos/_unit-test/grid-rowclick.php b/demos/_unit-test/grid-rowclick.php index 13cf1561c4..6ce555a1fd 100644 --- a/demos/_unit-test/grid-rowclick.php +++ b/demos/_unit-test/grid-rowclick.php @@ -5,9 +5,9 @@ namespace Atk4\Ui\Demos; use Atk4\Ui\Grid; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsFunction; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsFunction; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Message; use Atk4\Ui\Table; use Atk4\Ui\View; diff --git a/demos/_unit-test/late-output-error.php b/demos/_unit-test/late-output-error.php index f9628205e6..c507f66d03 100644 --- a/demos/_unit-test/late-output-error.php +++ b/demos/_unit-test/late-output-error.php @@ -45,15 +45,15 @@ Header::addTo($app, ['content' => 'Before render (/w Callback)']); $buttonH1 = Button::addTo($app, ['Test LateOutputError I: Headers already sent']); -$buttonH1->on('click', $modalH1->show()); +$buttonH1->on('click', $modalH1->jsShow()); $buttonO1 = Button::addTo($app, ['Test LateOutputError I: Unexpected output detected']); -$buttonO1->on('click', $modalO1->show()); +$buttonO1->on('click', $modalO1->jsShow()); Header::addTo($app, ['content' => 'After render (/w CallbackLater)']); $buttonH2 = Button::addTo($app, ['Test LateOutputError II: Headers already sent']); -$buttonH2->on('click', $modalH2->show()); +$buttonH2->on('click', $modalH2->jsShow()); $buttonO2 = Button::addTo($app, ['Test LateOutputError II: Unexpected output detected']); -$buttonO2->on('click', $modalO2->show()); +$buttonO2->on('click', $modalO2->jsShow()); diff --git a/demos/_unit-test/lookup-virtual-page.php b/demos/_unit-test/lookup-virtual-page.php index 7933f61b30..b7a6c5ac6d 100644 --- a/demos/_unit-test/lookup-virtual-page.php +++ b/demos/_unit-test/lookup-virtual-page.php @@ -6,8 +6,8 @@ use Atk4\Ui\Form; use Atk4\Ui\Grid; -use Atk4\Ui\JsModal; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsModal; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\VirtualPage; /** @var \Atk4\Ui\App $app */ diff --git a/demos/_unit-test/lookup.php b/demos/_unit-test/lookup.php index ce1115b1c7..b24c8ebeb1 100644 --- a/demos/_unit-test/lookup.php +++ b/demos/_unit-test/lookup.php @@ -7,7 +7,7 @@ use Atk4\Ui\Crud; use Atk4\Ui\UserAction\ExecutorFactory; -// Test for hasOne Lookup as dropdown control. +// test hasOne Lookup as dropdown control /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; @@ -15,7 +15,6 @@ $model = new Product($app->db); $model->addCondition($model->fieldName()->name, '=', 'Mustard'); -// use default. $app->getExecutorFactory()->useTriggerDefault(ExecutorFactory::TABLE_BUTTON); $edit = $model->getUserAction('edit'); diff --git a/demos/_unit-test/modal-error.php b/demos/_unit-test/modal-error.php new file mode 100644 index 0000000000..0aeacac811 --- /dev/null +++ b/demos/_unit-test/modal-error.php @@ -0,0 +1,41 @@ +set(function (View $p) { + $modal = Modal::addTo($p); + $modal->set(function (View $p) { + throw new \Error('Exception from Modal'); + }); + $button = Button::addTo($p)->set('Test Modal load PHP error'); + $button->on('click', $modal->jsShow()); + + $modal = Modal::addTo($p); + $modal->set(function (View $p) { + $p->js(true, new JsExpression('$(\'
\').modal({onShow: () => true})')); + }); + $button = Button::addTo($p)->set('Test Modal load JS error'); + $button->on('click', $modal->jsShow()); + + $country = new Country($p->getApp()->db); + $button = Button::addTo($p)->set('Test ModalExecutor load PHP error'); + $executor = ModalExecutor::assertInstanceOf($p->getExecutorFactory()->createExecutor($country->getUserAction('edit'), $button)); + if (\Closure::bind(fn () => $executor->loader, null, ModalExecutor::class)()->cb->isTriggered()) { + $executor->stickyGet($executor->name, '-1'); + } + $button->on('click', $executor); +}); +$button = Button::addTo($app)->set('Test'); +$button->on('click', $modal->jsShow()); diff --git a/demos/_unit-test/modal-reload.php b/demos/_unit-test/modal-reload.php index eaaded947a..3c841fe03c 100644 --- a/demos/_unit-test/modal-reload.php +++ b/demos/_unit-test/modal-reload.php @@ -12,8 +12,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// simulating ModalExecutor reload for Behat test - Header::addTo($app, ['Testing ModalExecutor reload']); $modal = Modal::addTo($app, ['title' => 'Modal Executor']); @@ -23,4 +21,4 @@ }); $button = Button::addTo($app)->set('Test'); -$button->on('click', $modal->show()); +$button->on('click', $modal->jsShow()); diff --git a/demos/_unit-test/post.php b/demos/_unit-test/post.php index b2c43e591d..c097c78a3e 100644 --- a/demos/_unit-test/post.php +++ b/demos/_unit-test/post.php @@ -5,7 +5,7 @@ namespace Atk4\Ui\Demos; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; diff --git a/demos/_unit-test/reload.php b/demos/_unit-test/reload.php index 3bcfa397a0..da111102a6 100644 --- a/demos/_unit-test/reload.php +++ b/demos/_unit-test/reload.php @@ -6,7 +6,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Callback; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Loader; use Atk4\Ui\View; diff --git a/demos/_unit-test/scope-builder-to-query.php b/demos/_unit-test/scope-builder-to-query.php index a7e21366b5..bf1d0eb26e 100644 --- a/demos/_unit-test/scope-builder-to-query.php +++ b/demos/_unit-test/scope-builder-to-query.php @@ -33,7 +33,7 @@ ], ], ]; -$scope = Form\Control\ScopeBuilder::queryToScope($q); +$scope = (new Form\Control\ScopeBuilder())->queryToScope($q); $product = new Product($app->db); diff --git a/demos/_unit-test/scope-builder.php b/demos/_unit-test/scope-builder.php index f6073aa055..fb554ba2d8 100644 --- a/demos/_unit-test/scope-builder.php +++ b/demos/_unit-test/scope-builder.php @@ -17,10 +17,10 @@ $project = new Condition($model->fieldName()->project_name, Condition::OPERATOR_REGEXP, '[a-zA-Z]'); $brazil = new Condition($model->fieldName()->client_country_iso, '=', 'BR'); -$start = new Condition($model->fieldName()->start_date, '=', '2020-10-22'); -$finish = new Condition($model->fieldName()->finish_time, '!=', '22:22'); -$isCommercial = new Condition($model->fieldName()->is_commercial, '0'); -$budget = new Condition($model->fieldName()->project_budget, '>=', '1000'); +$start = new Condition($model->fieldName()->start_date, '=', new \DateTime('2020-10-22')); +$finish = new Condition($model->fieldName()->finish_time, '!=', new \DateTime('22:22')); +$isCommercial = new Condition($model->fieldName()->is_commercial, true); +$budget = new Condition($model->fieldName()->project_budget, '>=', 1000); $currency = new Condition($model->fieldName()->currency, 'USD'); $scope = Scope::createAnd($project, $brazil, $start); @@ -37,6 +37,7 @@ $form->onSubmit(function (Form $form) use ($model) { $message = $form->model->get('qb')->toWords($model); $view = (new View(['name' => false]))->addClass('atk-scope-builder-response'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->set($message); @@ -44,14 +45,8 @@ return $view; }); -$expectedWord = <<<'EOF' - Project Budget is greater or equal to '1000' - and (Project Name is regular expression '[a-zA-Z]' - and Client Country Iso is equal to 'BR' ('Brazil') and Start Date is equal to '2020-10-22') - and (Finish Time is not equal to '22:22' or Is Commercial is equal to '0' or Currency is equal to 'USD') - EOF; - $statModelForHinting = new Stat($app->db); +$budget1000Eur = "€\u{00a0}1\u{00a0}000.00"; $expectedInput = json_encode(json_decode(<<<"EOF" { "logicalOperator": "AND", @@ -61,7 +56,7 @@ "query": { "rule": "{$statModelForHinting->fieldName()->project_budget}", "operator": ">=", - "value": "1000", + "value": "{$budget1000Eur}", "option": null } }, @@ -97,7 +92,7 @@ "query": { "rule": "{$statModelForHinting->fieldName()->start_date}", "operator": "is on", - "value": "2020-10-22", + "value": "Oct 22, 2020", "option": null } } @@ -123,7 +118,7 @@ "query": { "rule": "{$statModelForHinting->fieldName()->is_commercial}", "operator": "equals", - "value": "0", + "value": "Yes", "option": null } }, @@ -141,10 +136,17 @@ } ] } - EOF, true)); - -Header::addTo($app, ['Word:']); -View::addTo($app, ['element' => 'p', 'content' => $expectedWord])->addClass('atk-expected-word-result'); + EOF, true), \JSON_UNESCAPED_UNICODE); Header::addTo($app, ['Input:']); View::addTo($app, ['element' => 'p', 'content' => $expectedInput])->addClass('atk-expected-input-result'); + +$expectedWord = <<<'EOF' + Project Budget is greater or equal to '{$budget1000Eur}' + and (Project Name is regular expression '[a-zA-Z]' + and Client Country Iso is equal to 'BR' ('Brazil') and Start Date is equal to 'Oct 22, 2020') + and (Finish Time is not equal to '22:22' or Is Commercial is equal to 'Yes' or Currency is equal to 'USD') + EOF; + +Header::addTo($app, ['Word:']); +View::addTo($app, ['element' => 'p', 'content' => $expectedWord])->addClass('atk-expected-word-result'); diff --git a/demos/_unit-test/scroll.php b/demos/_unit-test/scroll.php new file mode 100644 index 0000000000..370e01a6b9 --- /dev/null +++ b/demos/_unit-test/scroll.php @@ -0,0 +1,27 @@ +db); +$grid = Grid::addTo($app); +$grid->setModel($model); + +$makeClickJsToastFx = function (string $source) use ($grid) { + return new JsToast(['message' => new JsExpression('[] + [] + []', [$source, ' clicked: ', $grid->jsRow()->data('id')])]); +}; + +$grid->addActionButton(['icon' => 'bell'], $makeClickJsToastFx('action')); +$grid->table->onRowClick($makeClickJsToastFx('row')); + +// TODO JsPaginator should be possible to be added no later than setModel call +// https://github.com/atk4/ui/issues/1934 +$grid->addJsPaginator(30); diff --git a/demos/_unit-test/sse.php b/demos/_unit-test/sse.php index 3c06c77853..785b12b80c 100644 --- a/demos/_unit-test/sse.php +++ b/demos/_unit-test/sse.php @@ -4,9 +4,9 @@ namespace Atk4\Ui\Demos; -use Atk4\Ui\JsExpression; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\JsSse; -use Atk4\Ui\JsToast; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -15,15 +15,14 @@ $v = View::addTo($app)->set('This will trigger a network request for testing SSE...'); $sse = JsSse::addTo($app); -// url trigger must match php_unit test in sse provider. +// URL trigger must match php_unit test in sse provider. $sse->setUrlTrigger('see_test'); $v->js(true, $sse->set(function () use ($sse) { - $sse->send(new JsExpression('console.log("test")')); - $sse->send(new JsExpression('console.log("test")')); - $sse->send(new JsExpression('console.log("test")')); - $sse->send(new JsExpression('console.log("test")')); + for ($i = 0; $i < 4; ++$i) { + $sse->send(new JsExpression('console.log([])', ['test ' . $i])); + } // non-SSE way return new JsToast('SSE sent, see browser console log'); -})); +})->jsExecute()); diff --git a/demos/_unit-test/stream.php b/demos/_unit-test/stream.php new file mode 100644 index 0000000000..53682cce1f --- /dev/null +++ b/demos/_unit-test/stream.php @@ -0,0 +1,140 @@ + new class(fn (int $pos) => '', -1) implements StreamInterface { + /** @var \Closure(int): string */ + private \Closure $fx; + + private int $size; + + private ?int $pos = 0; + + private string $buffer = ''; + + /** + * @param \Closure(int): string $fx + */ + public function __construct(\Closure $fx, int $size) + { + $this->fx = $fx; + $this->size = $size; + } + + /** + * @return never + */ + public function throwNotSupported(): void + { + throw new Exception('Not implemented/supported'); + } + + public function __toString() + { + $this->throwNotSupported(); + } + + public function close(): void + { + $this->pos = null; + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize(): int + { + return $this->size; + } + + public function tell(): int + { + return $this->pos; + } + + public function eof(): bool + { + return $this->pos === $this->size; + } + + public function isSeekable(): bool + { + return false; + } + + public function seek($offset, $whence = \SEEK_SET): void + { + $this->throwNotSupported(); + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + return false; + } + + public function write($string): int + { + $this->throwNotSupported(); + } + + public function isReadable(): bool + { + return true; + } + + public function read($length): string + { + if ($this->pos + $length > $this->size) { + $length = $this->size - $this->pos; + } + + while (strlen($this->buffer) < $length) { + $this->buffer .= ($this->fx)($this->pos + strlen($this->buffer)); + } + + $res = substr($this->buffer, 0, $length); + $this->pos += $length; + $this->buffer = substr($this->buffer, $length); + + return $res; + } + + public function getContents(): string + { + $this->throwNotSupported(); + } + + public function getMetadata($key = null) + { + $this->throwNotSupported(); + } +}); + +$sizeBytes = $_GET['size_mb'] * 1024 * 1024; + +$stream = new $hugePseudoStreamClass(function (int $pos) { + return "\n\0" . str_repeat($pos . ',', 1024); +}, $sizeBytes); + +$app->setResponseHeader('Content-Type', 'application/octet-stream'); +$app->setResponseHeader('Content-Length', (string) $sizeBytes); +$app->setResponseHeader('Content-Disposition', 'attachment; filename="test.bin"'); +$app->terminate($stream); diff --git a/demos/_unit-test/virtual-page.php b/demos/_unit-test/virtual-page.php index 93949c9f5d..b53925e148 100644 --- a/demos/_unit-test/virtual-page.php +++ b/demos/_unit-test/virtual-page.php @@ -6,7 +6,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\View; use Atk4\Ui\VirtualPage; diff --git a/demos/basic/breadcrumb.php b/demos/basic/breadcrumb.php index abadaddfa3..36c03f693e 100644 --- a/demos/basic/breadcrumb.php +++ b/demos/basic/breadcrumb.php @@ -6,7 +6,7 @@ use Atk4\Ui\Breadcrumb; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Table; use Atk4\Ui\View; @@ -14,7 +14,8 @@ require_once __DIR__ . '/../init-app.php'; $crumb = Breadcrumb::addTo($app); -$crumb->addCrumb('UI Demo', ['index']); +$crumb->addCrumb('UI Demo', '..'); +$crumb->addCrumb('Basics', '.'); $crumb->addCrumb('Breadcrumb Demo', ['breadcrumb']); View::addTo($app, ['ui' => 'divider']); @@ -24,7 +25,8 @@ $model = new Country($app->db); $model->setLimit(15); -if ($id = $crumb->stickyGet('country_id')) { +$id = $crumb->stickyGet('country_id'); +if ($id) { // perhaps we edit individual country? $model = $model->load($id); $crumb->addCrumb($model->name, []); diff --git a/demos/basic/button.php b/demos/basic/button.php index 299704da85..8fd711822d 100644 --- a/demos/basic/button.php +++ b/demos/basic/button.php @@ -30,7 +30,7 @@ Header::addTo($app, ['Properties', 'size' => 2]); Button::addTo($app, ['Primary button', 'class.primary' => true]); -Button::addTo($app, ['Load', 'class.labeled' => true, 'icon' => 'pause']); +Button::addTo($app, ['Load', 'icon' => 'pause']); Button::addTo($app, ['Next', 'iconRight' => 'right arrow']); Button::addTo($app, ['class.circular' => true, 'icon' => 'settings']); @@ -58,7 +58,6 @@ // Creating your own button component example -/** @var Button $forkButtonClass */ $forkButtonClass = AnonymousClassNameCache::get_class(fn () => new class(0) /* need 0 argument here for constructor */ extends Button { public function __construct(int $n) { diff --git a/demos/basic/columns.php b/demos/basic/columns.php index 0c0901937b..a4612972a1 100644 --- a/demos/basic/columns.php +++ b/demos/basic/columns.php @@ -71,10 +71,8 @@ // Example box component with some content, good for putting into columns. -/** @var View $boxClass */ $boxClass = AnonymousClassNameCache::get_class(fn () => new class() extends View { public $ui = 'segment'; - public $content = false; protected function init(): void { diff --git a/demos/basic/label.php b/demos/basic/label.php index 1c89b9ba80..ce9ccd518e 100644 --- a/demos/basic/label.php +++ b/demos/basic/label.php @@ -6,7 +6,7 @@ use Atk4\Ui\Columns; use Atk4\Ui\Header; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Label; use Atk4\Ui\LoremIpsum; use Atk4\Ui\Menu; @@ -39,7 +39,7 @@ $seg = View::addTo($app, ['ui' => 'segment']); Header::addTo($seg, ['Label Group']); -$labels = View::addTo($seg, [false, 'class.tag' => true, 'ui' => 'labels']); +$labels = View::addTo($seg, ['class.tag' => true, 'ui' => 'labels']); Label::addTo($seg, ['$9.99']); Label::addTo($seg, ['$19.99']); Label::addTo($seg, ['$24.99']); diff --git a/demos/basic/menu.php b/demos/basic/menu.php index 5c1d79a75e..eecfa003aa 100644 --- a/demos/basic/menu.php +++ b/demos/basic/menu.php @@ -20,7 +20,7 @@ $dropdown = UiDropdown::addTo($menu, ['With Callback', 'dropdownOptions' => ['on' => 'hover']]); $dropdown->setSource(['a', 'b', 'c']); $dropdown->onChange(function (string $itemId) { - return 'New seleced item id: ' . $itemId; + return 'New selected item ID: ' . $itemId; }); $submenu = $menu->addMenu('Sub-menu'); @@ -34,12 +34,12 @@ $menu = Menu::addTo($app, ['vertical pointing']); $menu->addItem(['Inbox', 'label' => ['123', 'class.teal left pointing' => true]]); $menu->addItem('Spam'); -Form\Control\Input::addTo($menu->addItem(), ['placeholder' => 'Search', 'icon' => 'search'])->addClass('transparent'); +Form\Control\Line::addTo($menu->addItem(), ['placeholder' => 'Search', 'icon' => 'search'])->addClass('transparent'); $menu = Menu::addTo($app, ['secondary vertical pointing']); $menu->addItem(['Inbox', 'label' => ['123', 'class.teal left pointing' => true]]); $menu->addItem('Spam'); -Form\Control\Input::addTo($menu->addItem(), ['placeholder' => 'Search', 'icon' => 'search'])->addClass('transparent'); +Form\Control\Line::addTo($menu->addItem(), ['placeholder' => 'Search', 'icon' => 'search'])->addClass('transparent'); $menu = Menu::addTo($app, ['vertical']); $group = $menu->addGroup('Products'); $group->addItem('Enterprise'); diff --git a/demos/basic/message.php b/demos/basic/message.php index 4a069c0d5a..baa9028b7c 100644 --- a/demos/basic/message.php +++ b/demos/basic/message.php @@ -6,8 +6,8 @@ use Atk4\Ui\Button; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Message; use Atk4\Ui\View; diff --git a/demos/basic/view.php b/demos/basic/view.php index 5b02b8d71f..c77d20e5c4 100644 --- a/demos/basic/view.php +++ b/demos/basic/view.php @@ -8,8 +8,8 @@ use Atk4\Ui\Columns; use Atk4\Ui\Header; use Atk4\Ui\HtmlTemplate; -use Atk4\Ui\JsModal; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsModal; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Label; use Atk4\Ui\Message; use Atk4\Ui\Paginator; @@ -45,7 +45,7 @@ } Header::addTo($app, ['View load HTML from string or file']); -$planeTemplate = new HtmlTemplate('
+$planeTemplate = new HtmlTemplate('
{$num}
@@ -61,21 +61,26 @@ View::addTo($app, ['ui' => 'segment', 'class.raised' => true, 'element' => 'pre'])->set($plane->render()); Header::addTo($app, ['Has a unique global identifier']); -Label::addTo($app, ['Plane ID: ', 'detail' => $plane->name]); - -Header::addTo($app, ['Can interract with JavaScript actions']); -Button::addTo($app, ['Hide plane', 'icon' => 'down arrow'])->on('click', $plane->js()->hide()); -Button::addTo($app, ['Show plane', 'icon' => 'up arrow'])->on('click', $plane->js()->show()); -Button::addTo($app, ['Jiggle plane', 'icon' => 'expand'])->on('click', $plane->js()->transition('jiggle')); -Button::addTo($app, ['Reload plane', 'icon' => 'refresh'])->on('click', new JsReload($plane)); +Label::addTo($app, ['Plane ID:', 'detail' => $plane->name]); + +Header::addTo($app, ['Can interact with JavaScript actions']); +Button::addTo($app, ['Hide plane', 'icon' => 'down arrow']) + ->on('click', $plane->js()->hide()); +Button::addTo($app, ['Show plane', 'icon' => 'up arrow']) + ->on('click', $plane->js()->show()); +Button::addTo($app, ['Jiggle plane', 'icon' => 'expand']) + ->on('click', $plane->js()->transition('jiggle')); +Button::addTo($app, ['Reload plane', 'icon' => 'refresh']) + ->on('click', new JsReload($plane)); Header::addTo($app, ['Can be on a Virtual Page']); $vp = VirtualPage::addTo($app)->set(function (VirtualPage $vp) use ($planeTemplate) { $plane = View::addTo($vp, ['template' => $planeTemplate]); - Label::addTo($vp, ['Plane ID: ', 'class.bottom attached' => true, 'detail' => $plane->name]); + Label::addTo($vp, ['Plane ID:', 'class.bottom attached' => true, 'detail' => $plane->name]); }); -Button::addTo($app, ['Show $plane in a dialog', 'icon' => 'clone'])->on('click', new JsModal('Plane Box', $vp)); +Button::addTo($app, ['Show $plane in a dialog', 'icon' => 'clone']) + ->on('click', new JsModal('Plane Box', $vp)); Header::addTo($app, ['All components extend View (even paginator)']); $columns = Columns::addTo($app); diff --git a/demos/collection/crud.php b/demos/collection/crud.php index f8d5ae0efa..6115c33e92 100644 --- a/demos/collection/crud.php +++ b/demos/collection/crud.php @@ -23,7 +23,7 @@ $crud = Crud::addTo($app, ['ipp' => 10]); // callback for model action add form. -$crud->onFormAdd(function (Form $form, $t) use ($model) { +$crud->onFormAdd(function (Form $form, ModalExecutor $ex) use ($model) { $form->js(true, $form->getControl($model->fieldName()->name)->jsInput()->val('Entering value via javascript')); }); @@ -73,7 +73,6 @@ $column = $columns->addColumn(); Header::addTo($column, ['Customizations']); -/** @var ModalExecutor $myExecutorClass */ $myExecutorClass = AnonymousClassNameCache::get_class(fn () => new class() extends ModalExecutor { public function addFormTo(View $view): Form { @@ -83,7 +82,7 @@ public function addFormTo(View $view): Form $result = parent::addFormTo($left); - if ($this->action->getEntity()->get(File::hinting()->fieldName()->is_folder)) { + if (File::assertInstanceOf($this->action->getEntity())->is_folder) { Grid::addTo($right, ['menu' => false, 'ipp' => 5]) ->setModel(File::assertInstanceOf($this->getAction()->getModel())->SubFolder); } else { diff --git a/demos/collection/crud3.php b/demos/collection/crud3.php index 7479986133..ac7f46953c 100644 --- a/demos/collection/crud3.php +++ b/demos/collection/crud3.php @@ -12,7 +12,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/** @var Model $modelClass */ $modelClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model { public $table = 'test'; diff --git a/demos/collection/grid.php b/demos/collection/grid.php index e01da8962f..f5a4a6e073 100644 --- a/demos/collection/grid.php +++ b/demos/collection/grid.php @@ -7,10 +7,10 @@ use Atk4\Data\Model; use Atk4\Ui\Button; use Atk4\Ui\Grid; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsReload; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsReload; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Message; use Atk4\Ui\Table; use Atk4\Ui\UserAction\BasicExecutor; @@ -27,6 +27,13 @@ $grid->setModel($model); +// add country flag column +$grid->addColumn('flag', [ + Table\Column\CountryFlag::class, + 'codeField' => $model->fieldName()->iso, + 'nameField' => $model->fieldName()->name, +]); + // Adding Quicksearch on Name field using auto query. $grid->addQuickSearch([$model->fieldName()->name], true); @@ -41,7 +48,7 @@ $grid->addColumn(null, [Table\Column\Template::class, 'helloworld']); // Creating a button for executing model test user action. -$grid->addExecutorButton($grid->getExecutorFactory()->create($model->getUserAction('test'), $grid)); +$grid->addExecutorButton($grid->getExecutorFactory()->createExecutor($model->getUserAction('test'), $grid)); $grid->addActionButton('Say HI', function (Jquery $j, $id) use ($grid) { $model = Country::assertInstanceOf($grid->model); @@ -54,7 +61,7 @@ }); // Creating an executor for delete action. -$deleteExecutor = $grid->getExecutorFactory()->create($model->getUserAction('delete'), $grid); +$deleteExecutor = $grid->getExecutorFactory()->createExecutor($model->getUserAction('delete'), $grid); $deleteExecutor->onHook(BasicExecutor::HOOK_AFTER_EXECUTE, function () { return [ (new Jquery())->closest('tr')->transition('fade left'), @@ -65,10 +72,11 @@ // $grid->addExecutorButton($deleteExecutor, new Button(['icon' => 'times circle outline'])); $sel = $grid->addSelection(); -$grid->menu->addItem('show selection')->on('click', new JsExpression( - 'alert("Selected: "+[])', - [$sel->jsChecked()] -)); +$grid->menu->addItem('show selection') + ->on('click', new JsExpression( + 'alert(\'Selected: \' + [])', + [$sel->jsChecked()] + )); // Setting ipp with an array will add an ItemPerPageSelector to paginator. -$grid->setIpp([10, 25, 50, 100]); +$grid->setIpp([10, 100, 1000]); diff --git a/demos/collection/multitable.php b/demos/collection/multitable.php index 92c0eb63ad..cc75eafdff 100644 --- a/demos/collection/multitable.php +++ b/demos/collection/multitable.php @@ -8,9 +8,9 @@ use Atk4\Ui\Button; use Atk4\Ui\Columns; use Atk4\Ui\Header; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsModal; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsModal; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\Table; use Atk4\Ui\VirtualPage; @@ -29,7 +29,7 @@ public function setModel(Model $model, array $route = []): void $this->addClass('internally celled'); // lets add our first table here - $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->addStyle('cursor', 'pointer'); + $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->setStyle('cursor', 'pointer'); $table->setModel($model, [$model->titleField]); $selections = explode(',', $_GET[$this->name] ?? ''); @@ -41,7 +41,7 @@ public function setModel(Model $model, array $route = []): void $makeJsReloadFx = function (array $path): JsReload { return new JsReload($this, [$this->name => new JsExpression('[] + []', [ count($path) > 0 ? implode(',', $path) . ',' : '', - new JsExpression('$(this).data("id")'), + new JsExpression('$(this).data(\'id\')'), ])]); }; @@ -68,7 +68,7 @@ public function setModel(Model $model, array $route = []): void $pushModel = $pushModel->ref($ref); $pushModel->setOrder([File::hinting()->fieldName()->is_folder => 'desc', File::hinting()->fieldName()->name]); - $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->addStyle('cursor', 'pointer'); + $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->setStyle('cursor', 'pointer'); $table->setModel($pushModel->setLimit(10), [$pushModel->titleField]); if ($selections) { @@ -93,8 +93,8 @@ public function setModel(Model $model, array $route = []): void $vp->js(true)->closest('.modal')->find('.header')->remove(); }); -Button::addTo($app, ['Re-Import From Filesystem', 'class.top attached' => true])->on('click', new JsModal('Now importing ... ', $vp)); +Button::addTo($app, ['Re-Import From Filesystem', 'class.top attached' => true]) + ->on('click', new JsModal('Now importing ... ', $vp)); -$finderClass::addTo($app, ['bottom attached']) - ->addClass('top attached segment') +$finderClass::addTo($app, ['bottom attached segment']) ->setModel($model->setLimit(10), [$model->fieldName()->SubFolder]); diff --git a/demos/collection/table.php b/demos/collection/table.php index 54e9904c37..e4d4346068 100644 --- a/demos/collection/table.php +++ b/demos/collection/table.php @@ -6,15 +6,15 @@ use Atk4\Data\Model; use Atk4\Ui\Button; -use Atk4\Ui\JsReload; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsReload; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Table; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -if ($id = $_GET['id'] ?? null) { +if ($_GET['id'] ?? null) { $app->layout->js(true, new JsToast('Details link is in simulation mode.')); } @@ -24,8 +24,6 @@ Button::addTo($bb, ['Refresh Table', 'icon' => 'refresh']) ->on('click', new JsReload($table)); -$bb->on('click', $table->js()->reload()); - $table->setModel(new SomeData(), []); $table->addColumn('name', new Table\Column\Link(['table', 'foo' => 'bar'], ['person_id' => 'id'], ['target' => '_blank'])); @@ -63,12 +61,15 @@ } }); -$table->addTotals(['name' => 'Totals:', 'salary' => ['sum']]); +$table->addTotals([ + 'name' => 'Totals:', + 'salary' => ['sum'], +]); $myArray = [ ['name' => 'Vinny', 'surname' => 'Sihra', 'birthdate' => '1973-02-03', 'cv' => 'I am BIG Vinny'], ['name' => 'Zoe', 'surname' => 'Shatwell', 'birthdate' => '1958-08-21', 'cv' => null], - ['name' => 'Darcy', 'surname' => 'Wild', 'birthdate' => '1968-11-01', 'cv' => 'I like icecream'], + ['name' => 'Darcy', 'surname' => 'Wild', 'birthdate' => '1968-11-01', 'cv' => 'I like This is sub-header, goes inside "thead" tag'); -$table->template->dangerouslyAppendHtml('Body', 'This is part of body, goes before other rows'); +$table->template->dangerouslyAppendHtml('SubHead', $app->getTag('tr', ['class' => 'center aligned'], [['th', ['colspan' => '2'], 'This is sub-header, goes inside "thead" tag']])); +$table->template->dangerouslyAppendHtml('Body', $app->getTag('tr', ['class' => 'center aligned'], [['td', ['colspan' => '2'], 'This is part of body, goes before other rows']])); // Hook can be used to display data before row. You can also inject and format extra rows. $table->onHook(Lister::HOOK_BEFORE_ROW, function (Table $table) { if ($table->currentRow->getId() === 2) { - $table->template->dangerouslyAppendHtml('Body', 'This goes above row with ID=2 (' . $table->currentRow->get('action') . ')'); + $table->template->dangerouslyAppendHtml('Body', $table->getApp()->getTag('tr', ['class' => 'center aligned'], [['td', ['colspan' => '2'], 'This goes above row with ID=2 (' . $table->currentRow->get('action') . ')']])); } elseif ($table->currentRow->get('action') === 'Tax') { - // renders current row $table->renderRow(); // adjusts data for next render @@ -47,8 +48,11 @@ } }); -$table->template->dangerouslyAppendHtml('Foot', 'This will appear above totals'); -$table->addTotals(['action' => 'Totals:', 'amount' => ['sum']]); +$table->template->dangerouslyAppendHtml('Foot', $app->getTag('tr', ['class' => 'center aligned'], [['td', ['colspan' => '2'], 'This will appear above totals']])); +$table->addTotals([ + 'action' => 'Totals:', + 'amount' => ['sum'], +]); Header::addTo($app, ['Columns with multiple formats', 'subHeader' => 'Single column can use logic to swap out formatters', 'icon' => 'table']); @@ -80,10 +84,17 @@ // one formatter return [[Table\Column\Money::class]]; -}, 'attr' => ['all' => ['class' => ['right aligned singel line']]]]); +}, 'attr' => ['all' => ['class' => ['right aligned single line']]]]); Header::addTo($app, ['Table with resizable columns', 'subHeader' => 'Just drag column header to resize', 'icon' => 'table']); $table = Table::addTo($app); $table->setModel($model); -$table->addClass('celled')->resizableColumn(); +$table->addClass('celled')->resizableColumn(function (Jquery $j, array $data) use ($app) { + $res = []; + foreach ($data as $column) { + $res[$column['column']] = $column['size'] < 100 ? 'narrow' : 'wide'; + } + + return new JsToast('New widths: ' . $app->encodeJson($res)); +}, [200, 200, 200]); diff --git a/demos/collection/tablecolumnmenu.php b/demos/collection/tablecolumnmenu.php index f2182cf738..151d7f4823 100644 --- a/demos/collection/tablecolumnmenu.php +++ b/demos/collection/tablecolumnmenu.php @@ -6,6 +6,7 @@ use Atk4\Ui\Grid; use Atk4\Ui\Header; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Table; use Atk4\Ui\Text; use Atk4\Ui\View; @@ -15,7 +16,7 @@ Header::addTo($app, ['Table column may contains popup or dropdown menu.']); -// Better Popup positionning when Popup are inside a container. +// Better Popup positioning when Popup are inside a container. $container = View::addTo($app, ['ui' => 'vertical segment']); $table = Table::addTo($container, ['class.celled' => true]); $table->setModel(new SomeData(), []); @@ -23,7 +24,7 @@ // will add popup to this column. $colName = $table->addColumn('name'); -// will add dropdown menu to this colum. +// will add dropdown menu to this column. $colSurname = $table->addColumn('surname'); $colTitle = $table->addColumn('title'); @@ -40,9 +41,9 @@ Text::addTo($pop)->set('This popup is loaded dynamically'); }); -// Another dropdown menu. +// another dropdown menu $colTitle->addDropdown(['Change', 'Reorder', 'Update'], function (string $item) { - return 'Title item: ' . $item; + return new JsToast(['message' => 'Title item: ' . $item]); }); // ----------------------------------------------------------------------------- diff --git a/demos/collection/tablecolumns.php b/demos/collection/tablecolumns.php index f7f021320b..c2aa21283a 100644 --- a/demos/collection/tablecolumns.php +++ b/demos/collection/tablecolumns.php @@ -12,7 +12,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/** @var Model $modelColorClass */ $modelColorClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model { protected function init(): void { @@ -90,7 +89,6 @@ protected function init(): void [ 'min' => 1, 'max' => 3, - 'steps' => 3, 'colors' => [ '#FF0000', '#FFFF00', @@ -117,17 +115,17 @@ protected function init(): void $model = new $modelColorClass(new Persistence\Static_([])); foreach (range(1, 10) as $id) { - $key_value = random_int(1, 4); + $keyValue = random_int(1, 4); $model->insert([ 'id' => $id, 'name' => 'name ' . $id, - 'key_value' => $key_value, - 'key_value_string' => $keyValueString[$key_value], + 'key_value' => $keyValue, + 'key_value_string' => $keyValueString[$keyValue], 'value_not_always_present' => random_int(0, 100) > 50 ? 'have value' : '', 'interests' => '1st label, 2nd label', 'rating' => random_int(100, 300) / 100, - 'note' => 'lorem ipsum lorem dorem lorem', + 'note' => $id !== 3 ? 'lorem ipsum lorem dorem lorem' : null, ]); } diff --git a/demos/collection/tablefilter.php b/demos/collection/tablefilter.php index 2952afe2ee..2c999daafd 100644 --- a/demos/collection/tablefilter.php +++ b/demos/collection/tablefilter.php @@ -10,7 +10,7 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// For popup positioning to work correctly, table need to be inside a view segment. +// For popup positioning to work correctly, table needs to be inside a view segment. $view = View::addTo($app, ['ui' => 'basic segment']); // Important: menu class added for Behat testing. $grid = Grid::addTo($view, ['menu' => ['class' => ['atk-grid-menu']]]); diff --git a/demos/data-action/actions.php b/demos/data-action/actions.php index 7b9b87aef5..159b847819 100644 --- a/demos/data-action/actions.php +++ b/demos/data-action/actions.php @@ -8,7 +8,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Columns; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\UserAction; use Atk4\Ui\View; @@ -30,7 +30,7 @@ 'description' => 'Import file in a specify path.', // Display information prior to execute the action. // ModalExecutor or PreviewExecutor will display preview. - 'preview' => function (Model $model, $path) { + 'preview' => function (Model $model, string $path) { return 'Execute Import using path: "' . $path . '"'; }, // Argument needed to run the callback action method. @@ -54,20 +54,19 @@ Header::addTo($rightColumn, [ 'JsCallbackExecutor', - 'subHeader' => 'Path argument is set via POST url when setting actions in executor.', + 'subHeader' => 'Path argument is set via POST URL when setting actions in executor.', ]); // Explicitly adding an Action executor. $executor = UserAction\JsCallbackExecutor::addTo($rightColumn); -// Passing Model action to executor and action argument via url. +// Passing Model action to executor and action argument via URL. $executor->setAction($action->getActionForEntity($files->createEntity())); // Setting user response after model action get execute. $executor->onHook(UserAction\BasicExecutor::HOOK_AFTER_EXECUTE, function () { return new JsToast('Files imported'); }); -$executor->executeModelAction(['path' => '.']); -$btn = Button::addTo($rightColumn, ['Import File']); -$btn->on('click', $executor, ['confirm' => 'This will import a lot of file. Are you sure?']); +$button = Button::addTo($rightColumn, ['Import File']); +$button->on('click', $executor, ['args' => ['path' => '.'], 'confirm' => 'This will import a lot of file. Are you sure?']); Header::addTo($rightColumn, ['BasicExecutor']); $executor = UserAction\BasicExecutor::addTo($rightColumn, ['executorButton' => [Button::class, 'Import', 'class.primary' => true]]); diff --git a/demos/data-action/factory-view.php b/demos/data-action/factory-view.php index 95dfcd7ee3..288643fe79 100644 --- a/demos/data-action/factory-view.php +++ b/demos/data-action/factory-view.php @@ -16,17 +16,17 @@ Button::addTo($app, ['Executor Factory in App instance', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']) ->link(['factory']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); -// Overriding basic ExecutorFactory in order to change Card button. +// overriding basic ExecutorFactory in order to change Card button $myFactory = AnonymousClassNameCache::get_class(fn () => new class() extends ExecutorFactory { - public const BUTTON_PRIMARY_COLOR = 'green'; + public $buttonPrimaryColor = 'green'; protected array $actionIcon = [ 'callback' => 'sync', 'preview' => 'eye', 'edit_argument' => 'user edit', - 'edit_argument_prev' => 'pen square', + 'edit_argument_preview' => 'pen square', 'edit_iso' => 'pencil', 'confirm' => 'check circle', 'multi_step' => 'window maximize outline', @@ -52,13 +52,18 @@ protected function getCardButton(Model\UserAction $action) Header::addTo($app, ['Executor Factory set for this Card View only.']); -DemoActionsUtil::setupDemoActions($country = new Country($app->db)); -$country = $country->loadAny(); +$country = new Country($app->db); +DemoActionsUtil::setupDemoActions($country); +$country = $country->loadBy($country->fieldName()->iso, 'fr'); +$country->name .= ' NO RELOAD'; +// suppress dirty field exception +// https://github.com/atk4/data/blob/35dd7b7d95909cfe574b15e32b7cc57c39a16a58/src/Model/UserAction.php#L164 +unset($country->getDirtyRef()[$country->fieldName()->name]); $cardActions = Card::addTo($app, ['useLabel' => true, 'executorFactory' => new $myFactory()]); $cardActions->setModel($country); foreach ($country->getModel()->getUserActions() as $action) { - $showActions = ['callback', 'preview', 'edit_argument', 'edit_argument_prev', 'edit_iso', 'confirm', 'multi_step']; + $showActions = ['callback', 'preview', 'edit_argument', 'edit_argument_preview', 'edit_iso', 'confirm', 'multi_step']; if (in_array($action->shortName, $showActions, true)) { $cardActions->addClickAction($action); } @@ -68,10 +73,11 @@ protected function getCardButton(Model\UserAction $action) Header::addTo($app, ['Card View using global Executor Factory']); -$model = new Country($app->db); -$model = $model->loadAny(); +$country = new Country($app->db); +$country = $country->loadBy($country->fieldName()->iso, 'cz'); +$country->name .= ' NO RELOAD'; $card = Card::addTo($app, ['useLabel' => true]); -$card->setModel($model); -$card->addClickAction($model->getUserAction('edit')); -$card->addClickAction($model->getUserAction('delete')); +$card->setModel($country); +$card->addClickAction($country->getUserAction('edit')); +$card->addClickAction($country->getUserAction('delete')); diff --git a/demos/data-action/factory.php b/demos/data-action/factory.php index bd1c26ef3e..9f7b4d5a7e 100644 --- a/demos/data-action/factory.php +++ b/demos/data-action/factory.php @@ -16,7 +16,7 @@ Button::addTo($app, ['Executor Factory in View Instance', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['factory-view']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); $msg = Message::addTo($app, [ 'Customizing action trigger by Overriding Executor Factory', @@ -27,10 +27,10 @@ $msg->text->addParagraph('In this example, Crud and Card button was changed and set through the App instance.'); -// Overriding basic ExecutorFactory in order to change Table and Modal button. -// and also changing default add action label. +// overriding basic ExecutorFactory in order to change Table and Modal button +// and also changing default add action label $myFactory = AnonymousClassNameCache::get_class(fn () => new class() extends ExecutorFactory { - public const BUTTON_PRIMARY_COLOR = 'green'; + public $buttonPrimaryColor = 'green'; protected $triggerSeed = [ self::TABLE_BUTTON => [ diff --git a/demos/data-action/jsactions-panel.php b/demos/data-action/jsactions-panel.php index dba5d74ce5..a5eda29b12 100644 --- a/demos/data-action/jsactions-panel.php +++ b/demos/data-action/jsactions-panel.php @@ -4,12 +4,13 @@ namespace Atk4\Ui\Demos; +use Atk4\Ui\UserAction\ExecutorFactory; use Atk4\Ui\UserAction\PanelExecutor; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; $factory = $app->getExecutorFactory(); -$factory->registerTypeExecutor($factory::STEP_EXECUTOR, [PanelExecutor::class]); +$factory->registerTypeExecutor(ExecutorFactory::STEP_EXECUTOR, [PanelExecutor::class]); require __DIR__ . '/jsactions2.php'; diff --git a/demos/data-action/jsactions-vp.php b/demos/data-action/jsactions-vp.php index 70d6e229ce..fbdce196ca 100644 --- a/demos/data-action/jsactions-vp.php +++ b/demos/data-action/jsactions-vp.php @@ -4,12 +4,13 @@ namespace Atk4\Ui\Demos; +use Atk4\Ui\UserAction\ExecutorFactory; use Atk4\Ui\UserAction\VpExecutor; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; $factory = $app->getExecutorFactory(); -$factory->registerTypeExecutor($factory::STEP_EXECUTOR, [VpExecutor::class]); +$factory->registerTypeExecutor(ExecutorFactory::STEP_EXECUTOR, [VpExecutor::class]); require_once 'jsactions2.php'; diff --git a/demos/data-action/jsactions.php b/demos/data-action/jsactions.php index d8572c2b89..6b1839b9b4 100644 --- a/demos/data-action/jsactions.php +++ b/demos/data-action/jsactions.php @@ -31,7 +31,7 @@ // ----------------------------------------------------------------------------- -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, [ 'Using Input button', @@ -48,7 +48,7 @@ 'required' => true, ], ], - 'callback' => function (Country $model, $name) { + 'callback' => function (Country $model, string $name) { return 'Hello ' . $name; }, ]); @@ -58,7 +58,7 @@ // ----------------------------------------------------------------------------- -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, [ 'Using buttons in a Card component', @@ -70,7 +70,7 @@ $card = Card::addTo($app); $content = new View(['class' => ['content']]); $img = Image::addTo($content, ['../images/kristy.png']); -$img->addClass('right floated mini ui image'); +$img->addClass('right floated mini'); Header::addTo($content, ['Kristy']); $card->addContent($content); diff --git a/demos/data-action/jsactions2.php b/demos/data-action/jsactions2.php index d875b8d762..452502b557 100644 --- a/demos/data-action/jsactions2.php +++ b/demos/data-action/jsactions2.php @@ -29,11 +29,11 @@ $msg->text->addParagraph('When passing an action to a button event, Ui will determine what executor is required base on the action properties.'); $msg->text->addParagraph('If action require arguments, fields and/or preview, then a ModalExecutor will be use.'); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); $gl = GridLayout::addTo($app, ['rows' => 1, 'columns' => 2]); $c = Card::addTo($gl, ['useLabel' => true], ['r1c1']); -$c->addContent(new Header(['Using country: '])); +$c->addContent(new Header(['Using country:'])); $c->setModel($entity, [$country->fieldName()->iso, $country->fieldName()->iso3, $country->fieldName()->phonecode]); $buttons = View::addTo($gl, ['ui' => 'vertical basic buttons'], ['r1c2']); @@ -41,6 +41,6 @@ // Create a button for every action in Country model. foreach ($country->getUserActions() as $action) { $b = Button::addTo($buttons, [$action->getCaption()]); - // Assign action to button using current model id as url arguments. + // Assign action to button using current model id as URL arguments. $b->on('click', $action, ['args' => ['id' => $countryId]]); } diff --git a/demos/data-action/jsactionsgrid.php b/demos/data-action/jsactionsgrid.php index 69ea62eed4..c70358cd2c 100644 --- a/demos/data-action/jsactionsgrid.php +++ b/demos/data-action/jsactionsgrid.php @@ -38,13 +38,13 @@ $modelHeader = Factory::factory([View::class], ['name' => false, 'class' => ['header'], 'content' => 'Model Actions']); Icon::addTo($modelHeader, ['content' => 'database']); -$jsHeader = Factory::factory([View::class], ['name' => false, 'class' => ['header'], 'content' => 'Js Actions']); +$jsHeader = Factory::factory([View::class], ['name' => false, 'class' => ['header'], 'content' => 'JS Actions']); Icon::addTo($jsHeader, ['content' => 'file code']); $grid->addActionMenuItem($jsHeader); // Beside model user action, grid menu items can also execute javascript. -$grid->addActionMenuItem('Js Callback', function () { - return (new View())->set('Js Callback done!'); +$grid->addActionMenuItem('JS Callback', function () { + return (new View())->set('JS Callback done!'); }, 'Are you sure?'); $grid->addActionMenuItem($divider); @@ -56,7 +56,7 @@ if (in_array($action->shortName, ['add', 'edit', 'delete'], true)) { continue; } - $grid->addExecutorMenuItem($executor = $app->getExecutorFactory()->create($action, $grid)); + $grid->addExecutorMenuItem($executor = $app->getExecutorFactory()->createExecutor($action, $grid)); } $grid->ipp = 10; diff --git a/demos/db.default.php b/demos/db.default.php index 5eccc4a74f..22bfae2df1 100644 --- a/demos/db.default.php +++ b/demos/db.default.php @@ -14,7 +14,7 @@ $sqliteFile = __DIR__ . '/_demo-data/db.sqlite'; if (!file_exists($sqliteFile)) { - throw new \Exception('Sqlite database does not exist, create it first.'); + throw new \Exception('Sqlite database does not exist, create it first'); } $db = new Persistence\Sql('sqlite:' . $sqliteFile); unset($sqliteFile); diff --git a/demos/form-control/calendar.php b/demos/form-control/calendar.php index 6aeaa56f2d..d0872ce314 100644 --- a/demos/form-control/calendar.php +++ b/demos/form-control/calendar.php @@ -5,76 +5,57 @@ namespace Atk4\Ui\Demos; use Atk4\Ui\Form; -use Atk4\Ui\GridLayout; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsFunction; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -$layout = GridLayout::addTo($app, ['rows' => 1, 'columns' => 2]); +$demo = Demo::addTo($app, ['leftWidth' => 10, 'rightWidth' => 6]); -$form = Form::addTo($layout, [], ['r1c1']); +$form = Form::addTo($demo->left); -$app->uiPersistence->dateFormat = 'Y-m-d'; -$form->addControl('date_y_m_d', [Form\Control\Calendar::class, 'type' => 'date', 'caption' => 'Date (Y-m-d)']) +$form->addControl('date', [Form\Control\Calendar::class, 'type' => 'date']) ->set(new \DateTime()); -$app->uiPersistence->timeFormat = 'G:i A'; -$form->addControl('time_g_i_a', [Form\Control\Calendar::class, 'type' => 'time', 'caption' => 'Time using am/pm']) +$form->addControl('time', [Form\Control\Calendar::class, 'type' => 'time']) ->set(new \DateTime()); -$app->uiPersistence->timeFormat = 'H:i:s'; -$form->addControl('time_h_i_s', [Form\Control\Calendar::class, 'type' => 'time', 'caption' => 'Time using 24 hrs with seconds picker']) +$form->addControl('datetime', [Form\Control\Calendar::class, 'type' => 'datetime']) ->set(new \DateTime()); -$form->addControl('datetime', [Form\Control\Calendar::class, 'type' => 'datetime', 'caption' => 'Datetime (M d, Y H:i:s)']) - ->set(new \DateTime()); - -$app->uiPersistence->dateFormat = 'F d, Y'; -$form->addControl('date_f_d_y', [ - Form\Control\Calendar::class, - 'type' => 'date', - 'caption' => 'Allow input (F d, Y)', - 'options' => ['allowInput' => true], -])->set(new \DateTime()); - -$app->uiPersistence->dateFormat = 'Y-m-d'; -$form->addControl('date_js_format', [ +$control = $form->addControl('date_action', [ Form\Control\Calendar::class, 'type' => 'date', - 'caption' => 'Format via Javascript', - 'options' => [ - 'formatDate' => new JsFunction(['date', 'format'], [new JsExpression('return "Date selected: " + flatpickr.formatDate(date, format)')]), - ], + 'caption' => 'Date with actions', + 'options' => ['clickOpens' => false], ])->set(new \DateTime()); +$control->addAction(['Today', 'icon' => 'calendar day']) + ->on('click', $control->getJsInstance()->setDate($app->uiPersistence->typecastSaveField($control->entityField->getField(), new \DateTime()))); +$control->addAction(['Select...', 'icon' => 'calendar']) + ->on('click', $control->getJsInstance()->open()); +$control->addAction(['Clear', 'icon' => 'times red']) + ->on('click', $control->getJsInstance()->clear()); // TODO "date" type does not support ranges // $form->addControl('date_range', [ // Form\Control\Calendar::class, // 'type' => 'date', -// 'caption' => 'Range mode', // 'options' => ['mode' => 'range'], -// ])->set(date('Y-m-d') . ' to ' . date('Y-m-d', strtotime('+1 Week'))); +// ])->set(date('Y-m-d') . ' to ' . date('Y-m-d', strtotime('+1 week'))); // -// $form->addControl('date_multi', [ +// $form->addControl('date_multiple', [ // Form\Control\Calendar::class, // 'type' => 'date', -// 'caption' => 'Multiple mode', // 'options' => ['mode' => 'multiple'], // ])->set(date('Y-m-d') . ', ' . date('Y-m-d', strtotime('+1 Day')) . ', ' . date('Y-m-d', strtotime('+2 Day'))); -$control = $form->addControl('date_action', [ - Form\Control\Calendar::class, - 'type' => 'date', - 'caption' => 'Javascript action', - 'options' => ['clickOpens' => false], -])->set(new \DateTime()); -$control->addAction(['Today', 'icon' => 'calendar day'])->on('click', $control->getJsInstance()->setDate($app->uiPersistence->typecastSaveField($control->entityField->getField(), new \DateTime()))); -$control->addAction(['Select...', 'icon' => 'calendar'])->on('click', $control->getJsInstance()->open()); -$control->addAction(['Clear', 'icon' => 'times red'])->on('click', $control->getJsInstance()->clear()); - $form->onSubmit(function (Form $form) use ($app) { - return new JsToast($app->encodeJson($form->model->get())); + $data = []; + foreach ($form->model->get() as $k => $v) { + $data[$k] = $v !== null + ? $app->uiPersistence->typecastSaveField($form->model->getField($k), $v) + : 'empty'; + } + + return new JsToast(implode(', ', $data)); }); diff --git a/demos/form-control/checkbox.php b/demos/form-control/checkbox.php index 7a0aa8dbcf..f7edb04ad2 100644 --- a/demos/form-control/checkbox.php +++ b/demos/form-control/checkbox.php @@ -6,7 +6,7 @@ use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -32,13 +32,8 @@ $form = Form::addTo($app); $form->addControl('test', [Form\Control\Checkbox::class]); $form->addControl('test_checked', [Form\Control\Checkbox::class])->set(1); -$form->addControl('also_checked', ['caption' => 'Hello World'], ['type' => 'boolean'])->set(true); +$form->addControl('also_checked', ['caption' => 'Also checked by default'], ['type' => 'boolean'])->set(true); $form->onSubmit(function (Form $form) use ($app) { return new JsToast($app->encodeJson($form->model->get())); }); - -View::addTo($app, ['ui' => 'divider']); -$c = new Form\Control\Checkbox('Selected checkbox by default'); -$c->set(true); -$app->add($c); diff --git a/demos/form-control/dropdown-plus.php b/demos/form-control/dropdown-plus.php index 8d2783dc1a..5b12db02fe 100644 --- a/demos/form-control/dropdown-plus.php +++ b/demos/form-control/dropdown-plus.php @@ -4,7 +4,6 @@ namespace Atk4\Ui\Demos; -use Atk4\Data\Model; use Atk4\Ui\Form; use Atk4\Ui\Header; use Atk4\Ui\Message; @@ -30,6 +29,7 @@ $message = $app->encodeJson($form->model->get()); $view = new Message('Values: '); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addParagraph($message); @@ -48,7 +48,7 @@ // custom callback: alter title $form->addControl('withModel2', [ Form\Control\Dropdown::class, - 'caption' => 'Dropdown with data from Model', + 'caption' => 'Dropdown with data from Model and custom render', 'model' => (new Country($app->db))->setLimit(25), 'renderRowFunction' => function (Country $row) { return [ @@ -61,7 +61,7 @@ // custom callback: add icon $form->addControl('withModel3', [ Form\Control\Dropdown::class, - 'caption' => 'Dropdown with data from Model', + 'caption' => 'Dropdown with data from Model and custom render with icon', 'model' => (new File($app->db))->setLimit(25), 'renderRowFunction' => function (File $row) { return [ @@ -89,21 +89,27 @@ Form\Control\Dropdown::class, 'caption' => 'Using icon', 'empty' => 'Choose an icon', - 'values' => ['tag' => ['Tag', 'icon' => 'tag'], 'globe' => ['Globe', 'icon' => 'globe'], 'registered' => ['Registered', 'icon' => 'registered'], 'file' => ['File', 'icon' => 'file']], + 'values' => [ + 'tag' => ['Tag', 'icon' => 'tag'], + 'globe' => ['Globe', 'icon' => 'globe'], + 'registered' => ['Registered', 'icon' => 'registered'], + 'file' => ['File', 'icon' => 'file'], + ], ]); $form->addControl('multi', [ Form\Control\Dropdown::class, 'caption' => 'Multiple selection', 'empty' => 'Choose has many options needed', - 'isMultiple' => true, + 'multiple' => true, 'values' => ['default' => 'Default', 'option1' => 'Option 1', 'option2' => 'Option 2'], ]); $form->onSubmit(function (Form $form) use ($app) { $message = $app->encodeJson($form->model->get()); - $view = new Message('Values: '); + $view = new Message('Values:'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addParagraph($message); diff --git a/demos/form-control/form6.php b/demos/form-control/form6.php index 31e04b2490..f35e152894 100644 --- a/demos/form-control/form6.php +++ b/demos/form-control/form6.php @@ -6,7 +6,7 @@ use Atk4\Ui\Columns; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -20,17 +20,17 @@ $cc = Columns::addTo($app); $form = Form::addTo($cc->addColumn()); -$form->addControl('one', [], ['enum' => ['female', 'male']])->set('male'); -$form->addControl('two', [Form\Control\Radio::class], ['enum' => ['female', 'male']])->set('male'); +$form->addControl('enum_d', [], ['enum' => ['female', 'male']])->set('male'); +$form->addControl('enum_r', [Form\Control\Radio::class], ['enum' => ['female', 'male']])->set('male'); -$form->addControl('three', [], ['values' => ['female', 'male']])->set(1); -$form->addControl('four', [Form\Control\Radio::class], ['values' => ['female', 'male']])->set(1); +$form->addControl('list_d', [], ['values' => ['female', 'male']])->set(1); +$form->addControl('list_r', [Form\Control\Radio::class], ['values' => ['female', 'male']])->set(1); -$form->addControl('five', [], ['values' => [5 => 'female', 7 => 'male']])->set(7); -$form->addControl('six', [Form\Control\Radio::class], ['values' => [5 => 'female', 7 => 'male']])->set(7); +$form->addControl('int_d', [], ['values' => [5 => 'female', 7 => 'male']])->set(7); +$form->addControl('int_r', [Form\Control\Radio::class], ['values' => [5 => 'female', 7 => 'male']])->set(7); -$form->addControl('seven', [], ['values' => ['F' => 'female', 'M' => 'male']])->set('M'); -$form->addControl('eight', [Form\Control\Radio::class], ['values' => ['F' => 'female', 'M' => 'male']])->set('M'); +$form->addControl('string_d', [], ['values' => ['F' => 'female', 'M' => 'male']])->set('M'); +$form->addControl('string_r', [Form\Control\Radio::class], ['values' => ['F' => 'female', 'M' => 'male']])->set('M'); $form->onSubmit(function (Form $form) use ($app) { return new JsToast($app->encodeJson($form->model->get())); diff --git a/demos/form-control/input2.php b/demos/form-control/input2.php index 13540137b9..fe4beb9c4e 100644 --- a/demos/form-control/input2.php +++ b/demos/form-control/input2.php @@ -7,7 +7,8 @@ use Atk4\Ui\Form; use Atk4\Ui\Header; use Atk4\Ui\HtmlTemplate; -use Atk4\Ui\JsExpression; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsExpression; use Atk4\Ui\Tabs; use Atk4\Ui\View; @@ -43,7 +44,7 @@ ]; $group->addControl('d_norm', [Form\Control\Dropdown::class, 'values' => $values, 'width' => 'three'])->set('globe'); $group->addControl('d_read', [Form\Control\Dropdown::class, 'values' => $values, 'readOnly' => true, 'width' => 'three'])->set('globe'); // allows to change value -$group->addControl('d_disb', [Form\Control\Dropdown::class, 'values' => $values, 'disabled' => true, 'width' => 'three'])->set('globe'); // css disabled, but can focus with Tab and change value +$group->addControl('d_disb', [Form\Control\Dropdown::class, 'values' => $values, 'disabled' => true, 'width' => 'three'])->set('globe'); // CSS disabled, but can focus with Tab and change value $group = $form->addGroup('Radio'); @@ -75,7 +76,7 @@ $model = new Country($app->db); $group->addControl('Lookup_norm', [ - DemoLookup::class, + Form\Control\Lookup::class, 'model' => new Country($app->db), 'plus' => true, ])->set($model->loadAny()->getId()); @@ -110,7 +111,7 @@ $control->set('hello world'); $button = $control->addAction(['check value']); -$button->on('click', new JsExpression('alert("field value is: "+[])', [$control->jsInput()->val()])); +$button->on('click', new JsExpression('alert(\'field value is: \' + [])', [$control->jsInput()->val()])); Header::addTo($app, ['Line in a Form']); $form = Form::addTo($app); @@ -122,7 +123,7 @@ $control = $form->addControl('surname', new Form\Control\Line([ 'hint' => [View::class, 'template' => new HtmlTemplate( - 'Click here' + 'Click here' )], ])); @@ -153,34 +154,30 @@ $group = $form->addGroup('Calendar'); $c1 = $group->addControl('c1', new Form\Control\Calendar(['type' => 'date'])); $c2 = $group->addControl('c2', new Form\Control\Calendar(['type' => 'date'])); -$c3 = $group->addControl('c3', new Form\Control\Calendar(['type' => 'date'])); -$c1->onChange('console.log("c1 changed: "+date+","+text+","+mode)'); -$c2->onChange(new JsExpression('console.log("c2 changed: "+date+","+text+","+mode)')); -$c3->onChange([ - new JsExpression('console.log("c3 changed: "+date+","+text+","+mode)'), - new JsExpression('console.log("c3 really changed: "+date+","+text+","+mode)'), -]); +$c1->onChange(new JsExpression('console.log(\'c1 changed: \' + date + \', \' + text + \', \' + mode)')); +$c2->onChange(new JsBlock([ + new JsExpression('console.log(\'c2 changed: \' + date + \', \' + text + \', \' + mode)'), + new JsExpression('console.log(\'c2 really changed: \' + date + \', \' + text + \', \' + mode)'), +])); $group = $form->addGroup('Line'); $f1 = $group->addControl('f1'); $f2 = $group->addControl('f2'); $f3 = $group->addControl('f3'); -$f4 = $group->addControl('f4'); - -$f1->onChange('console.log("f1 changed")'); -$f2->onChange(new JsExpression('console.log("f2 changed")')); -$f3->onChange([ - new JsExpression('console.log("f3 changed")'), - new JsExpression('console.log("f3 really changed")'), -]); -$f4->onChange(function () { - return new JsExpression('console.log("f4 changed")'); + +$f1->onChange(new JsExpression('console.log(\'f1 changed\')')); +$f2->onChange(new JsBlock([ + new JsExpression('console.log(\'f2 changed\')'), + new JsExpression('console.log(\'f2 really changed\')'), +])); +$f3->onChange(function () { + return new JsExpression('console.log(\'f3 changed\')'); }); $group = $form->addGroup('CheckBox'); $b1 = $group->addControl('b1', new Form\Control\Checkbox()); -$b1->onChange('console.log("b1 changed")'); +$b1->onChange(new JsExpression('console.log(\'b1 changed\')')); $group = $form->addGroup(['Dropdown', 'width' => 'three']); $d1 = $group->addControl('d1', new Form\Control\Dropdown([ @@ -191,7 +188,7 @@ 'file' => ['File', 'icon' => 'file'], ], ])); -$d1->onChange('console.log("Dropdown changed")'); +$d1->onChange(new JsExpression('console.log(\'Dropdown changed\')')); $group = $form->addGroup('Radio'); $r1 = $group->addControl('r1', new Form\Control\Radio([ @@ -202,7 +199,7 @@ 'File', ], ])); -$r1->onChange('console.log("radio changed")'); +$r1->onChange(new JsExpression('console.log(\'radio changed\')')); Header::addTo($app, ['Line ends of Textarea']); diff --git a/demos/form-control/lookup-dep.php b/demos/form-control/lookup-dep.php index 7e02349cbb..97a33b62c7 100644 --- a/demos/form-control/lookup-dep.php +++ b/demos/form-control/lookup-dep.php @@ -23,7 +23,7 @@ 'b' => 'Letter B', 'c' => 'Letter C', ], - 'isMultiple' => true, + 'multiple' => true, 'hint' => 'Select start letter that lookup selection of Country will depend on.', 'placeholder' => 'Search for country starting with ...', ]); @@ -42,10 +42,16 @@ $model->addCondition($model->fieldName()->name, 'like', $letter . '%'); } - isset($data['contains']) ? $model->addCondition($model->fieldName()->name, 'like', '%' . $data['contains'] . '%') : null; + if (isset($data['contains'])) { + $model->addCondition($model->fieldName()->name, 'like', '%' . $data['contains'] . '%'); + } }, 'placeholder' => 'Selection depends on Dropdown above', - 'search' => [Country::hinting()->fieldName()->name, Country::hinting()->fieldName()->iso, Country::hinting()->fieldName()->iso3], + 'search' => [ + Country::hinting()->fieldName()->name, + Country::hinting()->fieldName()->iso, + Country::hinting()->fieldName()->iso3, + ], ]); $form->onSubmit(function (Form $form) { @@ -72,10 +78,16 @@ Form\Control\Lookup::class, 'model' => new Country($app->db), 'dependency' => function (Country $model, $data) { - isset($data['ends_with']) ? $model->addCondition($model->fieldName()->name, 'like', '%' . $data['ends_with']) : null; + if (isset($data['ends_with'])) { + $model->addCondition($model->fieldName()->name, 'like', '%' . $data['ends_with']); + } }, 'multiple' => true, - 'search' => [Country::hinting()->fieldName()->name, Country::hinting()->fieldName()->iso, Country::hinting()->fieldName()->iso3], + 'search' => [ + Country::hinting()->fieldName()->name, + Country::hinting()->fieldName()->iso, + Country::hinting()->fieldName()->iso3, + ], ]); $form->onSubmit(function (Form $form) { diff --git a/demos/form-control/lookup.php b/demos/form-control/lookup.php index 79d470ce8c..b97b2f822f 100644 --- a/demos/form-control/lookup.php +++ b/demos/form-control/lookup.php @@ -20,7 +20,7 @@ // create header Header::addTo($app, ['Lookup Input']); -Form\Control\Lookup::addTo($app, ['placeholder' => 'Search country', 'label' => 'Country: ']) +Form\Control\Lookup::addTo($app, ['placeholder' => 'Search country', 'label' => 'Country:']) ->setModel(new Country($app->db)); // create form @@ -29,14 +29,11 @@ $model = new Model($app->db, ['table' => 'test']); -// Without Lookup +// lookup without plus button $model->hasOne('country1', ['model' => [Country::class]]); -// With Lookup -$model->hasOne('country2', ['model' => [Country::class], 'ui' => ['form' => [ - DemoLookup::class, - 'plus' => true, -]]]); +// lookup with plus button +$model->hasOne('country2', ['model' => [Country::class], 'ui' => ['form' => ['plus' => true]]]); $form->setModel($model->createEntity()); @@ -44,20 +41,22 @@ Form\Control\Lookup::class, 'model' => new Country($app->db), 'placeholder' => 'Search for country by name or iso value', - 'search' => ['name', 'iso', 'iso3'], + 'search' => [ + Country::hinting()->fieldName()->name, + Country::hinting()->fieldName()->iso, + Country::hinting()->fieldName()->iso3, + ], ]); $form->onSubmit(function (Form $form) { - $str = $form->model->ref('country1')->get(Country::hinting()->fieldName()->name) - . '; ' - . $form->model->ref('country2')->get(Country::hinting()->fieldName()->name) - . '; ' - . (new Country($form->getApp()->db))->load($form->model->get('country3')) - ->get(Country::hinting()->fieldName()->name); - - $view = new Message('Select:'); // need in behat test. + $view = new Message('Select:'); + $view->setApp($form->getApp()); $view->invokeInit(); - $view->text->addParagraph($str); + $view->text->addParagraph(Country::assertInstanceOf($form->model->ref('country1'))->name ?? 'null'); + $view->text->addParagraph(Country::assertInstanceOf($form->model->ref('country2'))->name ?? 'null'); + $view->text->addParagraph($form->model->get('country3') !== '' // related with https://github.com/atk4/ui/pull/1805 + ? (new Country($form->getApp()->db))->load($form->model->get('country3'))->name + : 'null'); return $view; }); @@ -65,7 +64,7 @@ Header::addTo($app, ['Lookup input using label']); // from seed -Form\Control\Lookup::addTo($app, ['placeholder' => 'Search country', 'label' => 'Country: ']) +Form\Control\Lookup::addTo($app, ['placeholder' => 'Search country', 'label' => 'Country:']) ->setModel(new Country($app->db)); // through constructor @@ -89,7 +88,8 @@ Header::addTo($app, ['Lookup input inside modal']); $modal = Modal::addTo($app)->set(function (View $p) { - $a = Form\Control\Lookup::addTo($p, ['placeholder' => 'Search country', 'label' => 'Country: ']); + $a = Form\Control\Lookup::addTo($p, ['placeholder' => 'Search country', 'label' => 'Country:']); $a->setModel(new Country($p->getApp()->db)); }); -Button::addTo($app, ['Open Lookup on a Modal window'])->on('click', $modal->show()); +Button::addTo($app, ['Open Lookup on a Modal window']) + ->on('click', $modal->jsShow()); diff --git a/demos/form-control/multiline-containsmany.php b/demos/form-control/multiline-containsmany.php index bcb49c00fd..98f3d00c0a 100644 --- a/demos/form-control/multiline-containsmany.php +++ b/demos/form-control/multiline-containsmany.php @@ -5,52 +5,8 @@ namespace Atk4\Ui\Demos; use Atk4\Ui\Crud; -use Atk4\Ui\Form; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// This demo require specific Database setup. - -if (!class_exists(Client::class)) { - class Client extends ModelWithPrefixedFields - { - public $table = 'client'; - public $caption = 'Client'; - - protected function init(): void - { - parent::init(); - - $this->addField('name'); - $this->containsMany('accounts', ['model' => [Account::class]]); - } - } - - class Account extends ModelWithPrefixedFields - { - public $caption = ' '; - - protected function init(): void - { - parent::init(); - - $this->addField('email', [ - 'required' => true, - 'ui' => ['multiline' => [Form\Control\Multiline::INPUT => ['icon' => 'envelope', 'type' => 'email']]], - ]); - $this->addField('password', [ - 'required' => true, - 'ui' => ['multiline' => [Form\Control\Multiline::INPUT => ['icon' => 'key', 'type' => 'password']]], - ]); - $this->addField('site', ['required' => true]); - $this->addField('type', [ - 'default' => 'user', - 'values' => ['user' => 'Regular User', 'admin' => 'System Admin'], - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 'four']]], - ]); - } - } -} - -Crud::addTo($app)->setModel(new Client($app->db)); +Crud::addTo($app)->setModel(new MultilineDelivery($app->db)); diff --git a/demos/form-control/multiline.php b/demos/form-control/multiline.php index 399cb50ab0..4b259fd881 100644 --- a/demos/form-control/multiline.php +++ b/demos/form-control/multiline.php @@ -4,95 +4,39 @@ namespace Atk4\Ui\Demos; -use Atk4\Data\Model; -use Atk4\Data\Persistence; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsFunction; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsFunction; +use Atk4\Ui\Js\JsToast; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; Header::addTo($app, ['Multiline form control', 'icon' => 'database', 'subHeader' => 'Collect/Edit multiple rows of table record.']); -/** @var Model $inventoryItemClass */ -$inventoryItemClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model { - public Persistence $countryPersistence; - - protected function init(): void - { - parent::init(); - - $this->addField('item', [ - 'required' => true, - 'default' => 'item', - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]], - ]); - $this->addField('inv_date', [ - 'default' => new \DateTime(), - 'type' => 'date', - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]], - ]); - $this->addField('inv_time', [ - 'default' => new \DateTime(), - 'type' => 'time', - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]], - ]); - $this->hasOne('country', [ - 'model' => new Country($this->countryPersistence), - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 3]]], - ]); - $this->addField('qty', [ - 'type' => 'integer', - 'caption' => 'Qty / Box', - 'default' => 1, - 'required' => true, - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]], - ]); - $this->addField('box', [ - 'type' => 'integer', - 'caption' => '# of Boxes', - 'default' => 1, - 'required' => true, - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]], - ]); - $this->addExpression('total', [ - 'expr' => function (Model $row) { - return $row->get('qty') * $row->get('box'); - }, - 'type' => 'integer', - 'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']]], - ]); - } -}); - -$inventory = new $inventoryItemClass(new Persistence\Array_(), ['countryPersistence' => $app->db]); - -// Populate some data. -$total = 0; -for ($i = 1; $i < 3; ++$i) { - $entity = $inventory->createEntity(); - $entity->set('id', $i); - $entity->set('inv_date', new \DateTime()); - $entity->set('inv_time', new \DateTime()); - $entity->set('item', 'item_' . $i); - $entity->set('country', random_int(1, 100)); - $entity->set('qty', random_int(10, 100)); - $entity->set('box', random_int(1, 10)); - $total += $entity->get('qty') * $entity->get('box'); - $entity->saveAndUnload(); -} +$inventory = new MultilineItem($app->db); +$inventory->getField($inventory->fieldName()->item)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]; +$inventory->getField($inventory->fieldName()->inv_date)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]; +$inventory->getField($inventory->fieldName()->inv_time)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]; +$inventory->getField($inventory->fieldName()->country_id)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 3]]; +$inventory->getField($inventory->fieldName()->qty)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]; +$inventory->getField($inventory->fieldName()->box)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]; +$inventory->getField($inventory->fieldName()->total_sql)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']]; +$inventory->getField($inventory->fieldName()->total_php)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']]; $form = Form::addTo($app); // Add multiline field and set model. /** @var Form\Control\Multiline */ -$multiline = $form->addControl('ml', [Form\Control\Multiline::class, 'tableProps' => ['color' => 'blue'], 'itemLimit' => 10, 'addOnTab' => true]); +$multiline = $form->addControl('items', [Form\Control\Multiline::class, 'tableProps' => ['color' => 'blue'], 'itemLimit' => 10, 'addOnTab' => true]); $multiline->setModel($inventory); // Add total field. +$total = 0; +foreach ($inventory as $item) { + $total += $item->qty * $item->box; +} $sublayout = $form->layout->addSubLayout([Form\Layout\Section\Columns::class]); $sublayout->addColumn(12); $column = $sublayout->addColumn(4); @@ -102,19 +46,28 @@ protected function init(): void $multiline->onLineChange(function (array $rows, Form $form) use ($controlTotal) { $total = 0; foreach ($rows as $row => $cols) { - $qty = $cols['qty'] ?? 0; - $box = $cols['box'] ?? 0; - $total += $qty * $box; + $total += $cols[MultilineItem::hinting()->fieldName()->qty] * $cols[MultilineItem::hinting()->fieldName()->box]; } return $controlTotal->jsInput()->val($total); -}, ['qty', 'box']); +}, [$inventory->fieldName()->qty, $inventory->fieldName()->box]); $multiline->jsAfterAdd = new JsFunction(['value'], [new JsExpression('console.log(value)')]); $multiline->jsAfterDelete = new JsFunction(['value'], [new JsExpression('console.log(value)')]); $form->onSubmit(function (Form $form) use ($multiline) { - $rows = $multiline->saveRows()->model->export(); + $rows = $multiline->model->atomic(function () use ($multiline) { + return $multiline->saveRows()->model->export(); + }); + + // TODO typecast using https://github.com/atk4/ui/pull/1991 once merged + foreach ($rows as $kRow => $row) { + foreach ($row as $kV => $v) { + if ($v instanceof \DateTime) { + $rows[$kRow][$kV] = $form->getApp()->uiPersistence->typecastSaveField($multiline->model->getField($kV), $row[$kV]); + } + } + } - return new JsToast($form->getApp()->encodeJson(array_values($rows))); + return new JsToast($form->getApp()->encodeJson($rows)); }); diff --git a/demos/form-control/scope-builder.php b/demos/form-control/scope-builder.php index b49f9958d4..a8ecc23aa7 100644 --- a/demos/form-control/scope-builder.php +++ b/demos/form-control/scope-builder.php @@ -10,8 +10,8 @@ require_once __DIR__ . '/../init-app.php'; $model = new Stat($app->db, ['caption' => 'Demo Stat']); -$model->addCondition($model->fieldName()->finish_time, '=', '22:12:00'); -$model->addCondition($model->fieldName()->start_date, '=', '2020-10-22'); +$model->addCondition($model->fieldName()->finish_time, '=', new \DateTime('22:12:00')); +$model->addCondition($model->fieldName()->start_date, '=', new \DateTime('2020-10-22')); $form = Form::addTo($app); diff --git a/demos/form-control/tree-item-selector.php b/demos/form-control/tree-item-selector.php index e22bf92ed8..baa79ebedf 100644 --- a/demos/form-control/tree-item-selector.php +++ b/demos/form-control/tree-item-selector.php @@ -6,7 +6,7 @@ use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Message; /** @var \Atk4\Ui\App $app */ @@ -24,35 +24,50 @@ 'id' => 502, ], [ - 'name' => 'Google Pixels', + 'name' => 'Google Pixel', 'id' => 503, ], ], ], ['name' => 'Tv', 'id' => 501, 'nodes' => []], - ['name' => 'Radio', 'id' => 601, 'nodes' => []], + ['name' => 'Radio', 'id' => 601], ], ], - ['name' => 'Cleaner', 'id' => 201, 'nodes' => []], - ['name' => 'Appliances', 'id' => 301, 'nodes' => []], + ['name' => 'Cleaner', 'id' => 201], + ['name' => 'Appliances', 'id' => 301], ]; +$pathFromIdFx = function (array $items, int $id) use (&$pathFromIdFx): ?string { + foreach ($items as $item) { + if (($item['id'] ?? false) === $id) { + return $item['name']; + } + + $itemRes = $pathFromIdFx($item['nodes'] ?? [], $id); + if ($itemRes !== null) { + return $item['name'] . ' > ' . $itemRes; + } + } + + return null; +}; + Header::addTo($app, ['Tree item selector']); $form = Form::addTo($app); $control = $form->addControl('tree', [Form\Control\TreeItemSelector::class, 'treeItems' => $items, 'caption' => 'Multiple selection:'], ['type' => 'json']); $control->set([201, 301, 503]); -// $control->onItem(function (array $value) use ($app) { -// return new JsToast($app->encodeJson($value)); -// }); +$control->onItem(function (array $values) use ($pathFromIdFx, $items) { + return new JsToast('Selected: ' . implode(',
', array_map(fn ($v) => $pathFromIdFx($items, $v), $values))); +}); $control = $form->addControl('tree1', [Form\Control\TreeItemSelector::class, 'treeItems' => $items, 'allowMultiple' => false, 'caption' => 'Single selection:']); -$control->set(502); +$control->set(503); -// $control->onItem(function (int $value) { -// return new JsToast('Received ' . $value); -// }); +$control->onItem(function (int $value) use ($pathFromIdFx, $items) { + return new JsToast('Selected: ' . $pathFromIdFx($items, $value)); +}); $form->onSubmit(function (Form $form) use ($app) { $response = [ @@ -60,7 +75,8 @@ 'single' => $form->model->get('tree1'), ]; - $view = new Message('Items: '); + $view = new Message('Items:'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addParagraph($app->encodeJson($response)); diff --git a/demos/form-control/upload.php b/demos/form-control/upload.php index 5903733d5f..308f73420f 100644 --- a/demos/form-control/upload.php +++ b/demos/form-control/upload.php @@ -5,7 +5,7 @@ namespace Atk4\Ui\Demos; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; @@ -18,10 +18,10 @@ $control = $form->addControl('file', [Form\Control\Upload::class, ['accept' => ['.png', '.jpg']]]); // $control->set('a_generated_token', 'a-file-name'); -// $control->set('a_generated_token'); +// $control->set('a_generated_token', null); $img->onDelete(function (string $fileId) use ($img) { - $img->clearThumbnail('./images/default.png'); + $img->clearThumbnail(); return new JsToast([ 'title' => 'Delete successfully', @@ -32,7 +32,7 @@ $img->onUpload(function (array $postFile) use ($form, $img) { if ($postFile['error'] !== 0) { - return $form->error('img', 'Error uploading image.'); + return $form->jsError('img', 'Error uploading image.'); } $img->setThumbnailSrc($img->getApp()->cdn['atk'] . '/logo.png'); @@ -43,9 +43,9 @@ // This will get caught by JsCallback and show via modal. // new Blabla(); - // js Action can be return. + // JS Action can be return. // if using form, can return an error to form control directly. - // return $form->error('file', 'Unable to upload file.'); + // return $form->jsError('file', 'Unable to upload file.'); // can also return a notifier. return new JsToast([ @@ -65,7 +65,7 @@ $control->onUpload(function (array $postFile) use ($form, $control) { if ($postFile['error'] !== 0) { - return $form->error('file', 'Error uploading file.'); + return $form->jsError('file', 'Error uploading file.'); } $control->setFileId('a_token'); @@ -87,5 +87,5 @@ $form->onSubmit(function (Form $form) { // implement submission here - return $form->success('Thanks for submitting file: ' . $form->model->get('img') . ' / ' . $form->model->get('file')); + return $form->jsSuccess('Thanks for submitting file: ' . $form->model->get('img') . ' / ' . $form->model->get('file')); }); diff --git a/demos/form/form-section-accordion.php b/demos/form/form-section-accordion.php index 55b9b7dc9a..b9624597ee 100644 --- a/demos/form/form-section-accordion.php +++ b/demos/form/form-section-accordion.php @@ -14,7 +14,7 @@ Button::addTo($app, ['Form Sections', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']) ->link(['form-section']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); $form = Form::addTo($app); @@ -66,5 +66,5 @@ $accordionLayout->activate($contactSection); $form->onSubmit(function (Form $form) { - return $form->success('Yey!', 'You did well by filling out this form'); + return $form->jsSuccess('Yey!', 'You did well by filling out this form'); }); diff --git a/demos/form/form-section.php b/demos/form/form-section.php index 49a53f5033..b45b0cf60d 100644 --- a/demos/form/form-section.php +++ b/demos/form/form-section.php @@ -7,7 +7,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -15,7 +15,7 @@ Button::addTo($app, ['Accordion in Form', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['form-section-accordion']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); $model = new Country($app->db); $model = $model->loadAny(); diff --git a/demos/form/form.php b/demos/form/form.php index 0acb46412e..d3e44ef519 100644 --- a/demos/form/form.php +++ b/demos/form/form.php @@ -10,7 +10,8 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Message; use Atk4\Ui\Modal; use Atk4\Ui\Tabs; @@ -19,17 +20,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/* - * Apart from demonstrating the form, this example uses an alternative way of rendering the layouts. - * Here we don't create application object explicitly, instead we use our custom template - * with a generic layout. - * - * We then render everything recursively (renderAll) and plug accumulated JavaScript inside the tag, - * echoing results after. - * - * This approach will also prevent your application from registering shutdown handler or catching error, - * so we will need to do a bit of work about that too. - */ $tabs = Tabs::addTo($app); // ----------------------------------------------------------------------------- @@ -43,7 +33,7 @@ $form->onSubmit(function (Form $form) { // implement subscribe here - return $form->success('Subscribed ' . $form->model->get('email') . ' to newsletter.'); + return $form->jsSuccess('Subscribed ' . $form->model->get('email') . ' to newsletter.'); }); $form->buttonSave->set('Subscribe'); @@ -74,12 +64,13 @@ Header::addTo($tab, ['Comparing Field type vs Form control class']); $form = Form::addTo($tab); $form->addControl('field', [], ['type' => 'date', 'caption' => 'Date using model field:']); -$form->addControl('control', [Form\Control\Calendar::class, 'type' => 'date', 'caption' => 'Date using form control: ']); +$form->addControl('control', [Form\Control\Calendar::class, 'type' => 'date', 'caption' => 'Date using form control:']); $form->buttonSave->set('Compare Date'); $form->onSubmit(function (Form $form) { $message = 'field = ' . print_r($form->model->get('field'), true) . ';
control = ' . print_r($form->model->get('control'), true); $view = new Message('Date field vs control:'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addHtml($message); @@ -95,7 +86,9 @@ $form->addControl('email1'); $form->buttonSave->set('Save1'); $form->onSubmit(function (Form $form) { - return $form->error('email1', 'some error action ' . random_int(1, 100)); + if ($form->getControl('email1')->entityField->get() !== 'pass@bar') { + return $form->jsError('email1', 'some error action ' . random_int(1, 100)); + } }); Header::addTo($tab, ['..or success message']); @@ -103,7 +96,7 @@ $form->addControl('email2'); $form->buttonSave->set('Save2'); $form->onSubmit(function (Form $form) { - return $form->success('form was successful'); + return $form->jsSuccess('form was successful'); }); Header::addTo($tab, ['Any other view can be output']); @@ -112,6 +105,7 @@ $form->buttonSave->set('Save3'); $form->onSubmit(function (Form $form) { $view = new Message('some header'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addParagraph('some text ' . random_int(1, 100)); @@ -124,10 +118,12 @@ $form->buttonSave->set('Save4'); $form->onSubmit(function (Form $form) { $view = new Message('some header'); + $view->setApp($form->getApp()); $view->invokeInit(); $view->text->addParagraph('some text ' . random_int(1, 100)); - $modal = new Modal(['title' => 'Something happen', 'ui' => 'ui modal tiny']); + $modal = new Modal(['title' => 'Something happen', 'ui' => 'modal tiny']); + $modal->setApp($form->getApp()); $modal->add($view); return $modal; @@ -149,6 +145,7 @@ $form = Form::addTo($tab); $form->addControl('email'); +$form->buttonSave->set('SaveE1'); $form->onSubmit(function (Form $form) { $o = new \stdClass(); @@ -159,24 +156,26 @@ $form = Form::addTo($tab); $form->addControl('email'); +$form->buttonSave->set('SaveE2'); $form->onSubmit(function (Form $form) { - throw (new CoreException('testing')) + throw (new CoreException('Test exception I.')) ->addMoreInfo('arg1', 'val1'); // return 'somehow it did not crash'; }); -Button::addTo($form, ['Modal Test', 'class.secondary' => true])->on('click', Modal::addTo($form) - ->set(function (View $p) { +Button::addTo($form, ['Modal Test', 'class.secondary' => true]) + ->on('click', Modal::addTo($form)->set(function (View $p) { $form = Form::addTo($p); + $form->name = 'mf'; $form->addControl('email'); $form->onSubmit(function (Form $form) { - throw (new CoreException('testing')) + throw (new CoreException('Test exception II.')) ->addMoreInfo('arg1', 'val1'); // return 'somehow it did not crash'; }); - })->show()); + })->jsShow()); // ----------------------------------------------------------------------------- @@ -195,13 +194,13 @@ $form->onSubmit(function (Form $form) { if ($form->model->get('name') !== 'John') { - return $form->error('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); + return $form->jsError('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); } - return [ + return new JsBlock([ $form->jsInput('email')->val('john@gmail.com'), - $form->jsControl('is_accept_terms')->checkbox('set checked'), - ]; + $form->getControl('is_accept_terms')->js()->checkbox('set checked'), + ]); }); // ----------------------------------------------------------------------------- @@ -239,9 +238,9 @@ } if ($form->model->get($name) !== 'a') { - $errors[] = $form->error($name, 'Field ' . $name . ' should contain exactly "a", but contains ' . $form->model->get($name)); + $errors[] = $form->jsError($name, 'Field ' . $name . ' should contain exactly "a", but contains ' . $form->model->get($name)); } } - return $errors !== [] ? $errors : $form->success('No more errors', 'so we have saved everything into the database'); + return $errors !== [] ? new JsBlock($errors) : $form->jsSuccess('No more errors', 'so we have saved everything into the database'); }); diff --git a/demos/form/form2.php b/demos/form/form2.php index a5b2433527..7c43422277 100644 --- a/demos/form/form2.php +++ b/demos/form/form2.php @@ -8,8 +8,9 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Label; /** @var \Atk4\Ui\App $app */ @@ -29,14 +30,15 @@ // form basic field group $formAddress = $form->addGroup('Basic Country Information'); -$name = $formAddress->addControl(Country::hinting()->fieldName()->name, ['width' => 'sixteen']); -$name->addAction(['Check Duplicate', 'iconRight' => 'search'])->on('click', function (Jquery $jquery, string $name) use ($app, $form) { - if ((new Country($app->db))->tryLoadBy(Country::hinting()->fieldName()->name, $name) !== null) { - return $form->js()->form('add prompt', Country::hinting()->fieldName()->name, 'This country name is already added.'); - } +$nameInput = $formAddress->addControl(Country::hinting()->fieldName()->name, ['width' => 'sixteen']); +$nameInput->addAction(['Check Duplicate', 'iconRight' => 'search']) + ->on('click', function (Jquery $jquery, string $name) use ($app, $form) { + if ((new Country($app->db))->tryLoadBy(Country::hinting()->fieldName()->name, $name) !== null) { + return $form->js()->form('add prompt', Country::hinting()->fieldName()->name, 'This country name is already added.'); + } - return new JsToast('This country name can be added.'); -}, ['args' => ['_n' => $name->jsInput()->val()]]); + return new JsToast('This country name can be added.'); + }, ['args' => [$nameInput->jsInput()->val()]]); // form codes field group $formCodes = $form->addGroup(['Codes']); @@ -64,25 +66,24 @@ // In-form validation $errors = []; if (mb_strlen($form->model->get('first_name')) < 3) { - $errors[] = $form->error('first_name', 'too short, ' . $form->model->get('first_name')); + $errors[] = $form->jsError('first_name', 'too short, ' . $form->model->get('first_name')); } if (mb_strlen($form->model->get('last_name')) < 5) { - $errors[] = $form->error('last_name', 'too short'); + $errors[] = $form->jsError('last_name', 'too short'); } // Model validation. We do it manually because we are not using Model::save() method in demo mode. foreach ($countryEntity->validate('save') as $k => $error) { - $errors[] = $form->error($k, $error); + $errors[] = $form->jsError($k, $error); } if ($errors) { - return $errors; + return new JsBlock($errors); } return new JsToast($countryEntity->getUserAction('add')->execute()); }); -/** @var Model $personClass */ $personClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model { public $table = 'person'; diff --git a/demos/form/form3.php b/demos/form/form3.php index d4b580cf97..98b973fa2b 100644 --- a/demos/form/form3.php +++ b/demos/form/form3.php @@ -8,7 +8,8 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -39,9 +40,9 @@ foreach ($modelDirty as $field => $value) { // we should care only about editable fields if ($form->model->getField($field)->isEditable()) { - $errors[] = $form->error($field, 'Value was changed, ' . $form->getApp()->encodeJson($value) . ' to ' . $form->getApp()->encodeJson($form->model->get($field))); + $errors[] = $form->jsError($field, 'Value was changed, ' . $form->getApp()->encodeJson($value) . ' to ' . $form->getApp()->encodeJson($form->model->get($field))); } } - return $errors !== [] ? $errors : 'No fields were changed'; + return $errors !== [] ? new JsBlock($errors) : 'No fields were changed'; }); diff --git a/demos/form/form5.php b/demos/form/form5.php index 4c2b5224e4..7c617f2f26 100644 --- a/demos/form/form5.php +++ b/demos/form/form5.php @@ -8,7 +8,7 @@ use Atk4\Data\Persistence; use Atk4\Ui\Columns; use Atk4\Ui\Form; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -58,10 +58,11 @@ $model->addField('four', ['type' => 'boolean', 'ui' => ['form' => ['caption' => 'Caption2']]]); // Can specify class for a checkbox explicitly -$model->addField('five', ['ui' => ['form' => [Form\Control\Checkbox::class, 'caption' => 'Caption3']]]); +// type here in "six" should not be needed if we add Checkbox form control support for string type +$model->addField('five', ['type' => 'boolean', 'ui' => ['form' => [Form\Control\Checkbox::class, 'caption' => 'Caption3']]]); // Form-specific caption overrides general caption of a field. Also you can specify object instead of seed -$model->addField('six', ['caption' => 'badcaption', 'ui' => ['form' => new Form\Control\Checkbox(['caption' => 'Caption4'])]]); +$model->addField('six', ['type' => 'boolean', 'caption' => 'badcaption', 'ui' => ['form' => new Form\Control\Checkbox(['caption' => 'Caption4'])]]); $model = $model->createEntity(); @@ -69,7 +70,7 @@ $form->setModel($model); $form->onSubmit($formSubmit); -// Next form won't initalize default fields, but we'll add them individually +// Next form won't initialize default fields, but we'll add them individually $form = Form::addTo($cc->addColumn()); $form->setModel($model, []); diff --git a/demos/form/html-layout.php b/demos/form/html-layout.php index e78dfc4eba..641d1fbcee 100644 --- a/demos/form/html-layout.php +++ b/demos/form/html-layout.php @@ -8,7 +8,7 @@ use Atk4\Ui\Form; use Atk4\Ui\GridLayout; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Label; use Atk4\Ui\Tabs; use Atk4\Ui\View; @@ -16,7 +16,7 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -Header::addTo($app, ['Display form using Html template', 'subHeader' => 'Fully control how to display fields.']); +Header::addTo($app, ['Display form using HTML template', 'subHeader' => 'Fully control how to display fields.']); $tabs = Tabs::addTo($app); diff --git a/demos/form/jscondform.php b/demos/form/jscondform.php index 611a1cc669..7ad998daa0 100644 --- a/demos/form/jscondform.php +++ b/demos/form/jscondform.php @@ -83,7 +83,7 @@ $groupBasic->addControl('middle_name', ['width' => 'three']); $groupBasic->addControl('last_name', ['width' => 'five']); -$formGroup->addControl('dev', [Form\Control\Checkbox::class, 'caption' => 'I am a developper']); +$formGroup->addControl('dev', [Form\Control\Checkbox::class, 'caption' => 'I am a developer']); $groupCode = $formGroup->addGroup(['Check all language that apply']); $groupCode->addControl('php', [Form\Control\Checkbox::class]); @@ -102,55 +102,45 @@ // ----------------------------------------------------------------------------- -/* Header::addTo($app, ['Hide or show accordion section', 'size' => 2]); -$f_acc = Form::addTo($app, ['class.segment' => true]); -Label::addTo($f_acc, ['Work on section layouts too.', 'class.top attached' => true], ['AboveControls']); +$formAccordion = Form::addTo($app, ['class.segment' => true]); +Label::addTo($formAccordion, ['Work on section layouts too.', 'class.top attached' => true], ['AboveControls']); -// Accordion -$accordion_layout = $f_acc->layout->addSubLayout([Form\Layout\Section\Accordion::class, 'type' => ['styled', 'fluid'], 'settings' => ['exclusive' => false]]); +$accordionLayout = $formAccordion->layout->addSubLayout([Form\Layout\Section\Accordion::class, 'type' => ['styled', 'fluid'], 'settings' => ['exclusive' => false]]); -// Section - business address -$adr_section = $accordion_layout->addSection('Business Address'); +$invoiceAddressSection = $accordionLayout->addSection('Invoice Address'); +$group = $invoiceAddressSection->addGroup('Street and City'); +$group->addControl('invoice_addr', ['width' => 'eight'], ['required' => true]); +$group->addControl('invoice_city', ['width' => 'eight']); +$group = $invoiceAddressSection->addGroup('State, Country and Postal Code'); +$group->addControl('invoice_state', ['width' => 'six']); +$group->addControl('country', ['width' => 'six']); +$group->addControl('invoice_postal', ['width' => 'four']); -$gr = $adr_section->addGroup('Street and City'); -$gr->addControl('addr1', ['width' => 'eight'], ['required' => true]); -$gr->addControl('city1', ['width' => 'eight']); +$invoiceAddressSection->addControl('has_custom_delivery_address', [Form\Control\Checkbox::class, 'caption' => 'Different Delivery Address']); -$gr = $adr_section->addGroup('State, Country and Postal Code'); -$gr->addControl('state1', ['width' => 'six']); -$gr->addControl('country1', ['width' => 'six']); -$gr->addControl('postal1', ['width' => 'four']); +$deliveryAddressSection = $accordionLayout->addSection('Delivery Address'); +$group = $deliveryAddressSection->addGroup('Street and City'); +$group->addControl('delivery_addr', ['width' => 'eight'], ['required' => true]); +$group->addControl('delivery_city', ['width' => 'eight']); +$group = $deliveryAddressSection->addGroup('State, Country and Postal Code'); +$group->addControl('delivery_state', ['width' => 'six']); +$group->addControl('delivery_country', ['width' => 'six']); +$group->addControl('delivery_postal', ['width' => 'four']); -$adr_section->addControl('custom_shipping', [Form\Control\Checkbox::class, 'caption' => 'Different Shipping Address']); - -// Section - shipping address -$ship_section = $accordion_layout->addSection('Shipping address'); - -$gr = $ship_section->addGroup('Street and City'); -$gr->addControl('addr2', ['width' => 'eight'], ['required' => true]); -$gr->addControl('city2', ['width' => 'eight']); - -$gr = $ship_section->addGroup('State, Country and Postal Code'); -$gr->addControl('state2', ['width' => 'six']); -$gr->addControl('country2', ['width' => 'six']); -$gr->addControl('postal2', ['width' => 'four']); - -// activate #1 section -$accordion_layout->activate($adr_section); +$accordionLayout->activate($invoiceAddressSection); // To hide-show group or section simply select a field in that group. // Show group where 'php' belong when dev is checked. // Show group where 'language' belong when dev is checked. -$f_acc->setGroupDisplayRules( - // rules - ['addr2' => ['custom_shipping' => 'checked']], +$formAccordion->setGroupDisplayRules( + ['delivery_addr' => ['has_custom_delivery_address' => 'checked']], + // TODO not implemented // JS selector of container // '.atk-form-group' // this will hide group // '.content' // this will hide content of 2nd accordion section - $ship_section->getOwner() // this way we set selector to accordion section title block - so what? we still can't do anything about it - // // BUT there is no way how to show/hide all accordion section including title and content + $deliveryAddressSection->getOwner() // this way we set selector to accordion section title block - so what? we still can't do anything about it + // BUT there is no way how to show/hide all accordion section including title and content ); -*/ diff --git a/demos/form/templates/form-custom-layout.html b/demos/form/templates/form-custom-layout.html index d802999845..d8a6386540 100644 --- a/demos/form/templates/form-custom-layout.html +++ b/demos/form/templates/form-custom-layout.html @@ -12,7 +12,7 @@
Number Code: {$atk_fp_country__numcode}
-
{$Buttons}
+
{$Buttons}
diff --git a/demos/form/templates/form-custom-layout.pug b/demos/form/templates/form-custom-layout.pug index 2e2ba7e906..2e322b00d5 100644 --- a/demos/form/templates/form-custom-layout.pug +++ b/demos/form/templates/form-custom-layout.pug @@ -14,6 +14,6 @@ .column | Number Code: {$atk_fp_country__numcode} .centered.row - .colum + .column | {$Buttons} = "\n" diff --git a/demos/form/templates/input.html b/demos/form/templates/input.html index 84f3e0ebec..e4f2b0a108 100644 --- a/demos/form/templates/input.html +++ b/demos/form/templates/input.html @@ -9,7 +9,7 @@ {/} {LabeledControl}
- {$Input} + {$Input}
{/} {NoLabelControl}
{$Input}{$Hint}
{/} diff --git a/demos/form/templates/input.pug b/demos/form/templates/input.pug index 4f4cdec02e..9258e6217b 100644 --- a/demos/form/templates/input.pug +++ b/demos/form/templates/input.pug @@ -15,7 +15,7 @@ div(class="{$controlClass} field atk-form-group") | {LabeledControl} div(class="{$controlClass} field") label(for="{$labelFor}") {$label} - span(style="float: right") {$Hint} + span(style="float: right;") {$Hint} | {$Input} | {/} diff --git a/demos/index.php b/demos/index.php index 1724515ed5..ba32d561cf 100644 --- a/demos/index.php +++ b/demos/index.php @@ -14,7 +14,7 @@ Header::addTo($app)->set('Welcome to Agile Toolkit Demo!'); -$t = Text::addTo(View::addTo($app, [false, 'class.green' => true, 'ui' => 'segment'])); +$t = Text::addTo(View::addTo($app, ['class.green' => true, 'ui' => 'segment'])); $t->addParagraph('Take a quick stroll through some of the amazing features of Agile Toolkit.'); Button::addTo($app, ['Begin the demo..', 'class.huge primary fluid' => true, 'iconRight' => 'right arrow']) @@ -22,7 +22,7 @@ Header::addTo($app)->set('What is new in Agile Toolkit'); -$t = Text::addTo(View::addTo($app, [false, 'class.green' => true, 'ui' => 'segment'])); +$t = Text::addTo(View::addTo($app, ['class.green' => true, 'ui' => 'segment'])); $t->addParagraph('In this version of Agile Toolkit we introduce "User Actions"!'); Button::addTo($app, ['Learn about User Actions', 'class.huge basic primary fluid' => true, 'iconRight' => 'right arrow']) diff --git a/demos/init-app.php b/demos/init-app.php index 3965719017..2cd8b0a8a5 100644 --- a/demos/init-app.php +++ b/demos/init-app.php @@ -47,13 +47,20 @@ final class AnonymousClassNameCache { - /** @var array */ + /** @var array */ private static $classNameByFxHash = []; private function __construct() { } + /** + * @template T of object + * + * @param \Closure(): T $createAnonymousClassFx + * + * @return class-string + */ public static function get_class(\Closure $createAnonymousClassFx): string { $fxRefl = new \ReflectionFunction($createAnonymousClassFx); @@ -157,6 +164,7 @@ public static function get_class(\Closure $createAnonymousClassFx): string $menu = $layout->addMenuGroup(['Interactive', 'icon' => 'talk']); $layout->addMenuItem('Tabs', [$path . 'tabs'], $menu); $layout->addMenuItem('Card', [$path . 'card'], $menu); + $layout->addMenuItem('Card Table', [$path . 'cardtable'], $menu); $layout->addMenuItem(['Accordion'], [$path . 'accordion'], $menu); $layout->addMenuItem(['Wizard'], [$path . 'wizard'], $menu); $layout->addMenuItem(['Virtual Page'], [$path . 'virtual'], $menu); @@ -169,7 +177,7 @@ public static function get_class(\Closure $createAnonymousClassFx): string $layout->addMenuItem(['Pop-up'], [$path . 'popup'], $menu); $layout->addMenuItem(['Toast'], [$path . 'toast'], $menu); $layout->addMenuItem('Paginator', [$path . 'paginator'], $menu); - $layout->addMenuItem(['Drag n Drop sorting'], [$path . 'jssortable'], $menu); + $layout->addMenuItem(['Drag sorting'], [$path . 'jssortable'], $menu); $path = $demosUrl . 'javascript/'; $menu = $layout->addMenuGroup(['Javascript', 'icon' => 'code']); diff --git a/demos/init-db.php b/demos/init-db.php index fb66801e76..dfbb3eefc1 100644 --- a/demos/init-db.php +++ b/demos/init-db.php @@ -9,6 +9,7 @@ use Atk4\Data\Model; use Atk4\Ui\Exception; use Atk4\Ui\Form; +use Atk4\Ui\Table; use Mvorisek\Atk4\Hintable\Data\HintablePropertyDef; try { @@ -21,10 +22,18 @@ ->addMoreInfo('PDO error', $e->getMessage()); } -// a very basic file that sets up Agile Data to be used in some demonstrations - trait ModelPreventModificationTrait { + protected function isAllowDbModifications(): bool + { + static $rw = null; + if ($rw === null) { + $rw = file_exists(__DIR__ . '/db-behat-rw.txt'); + } + + return $rw; + } + public function atomic(\Closure $fx) { $eRollback = new \Exception('Prevent modification'); @@ -33,7 +42,9 @@ public function atomic(\Closure $fx) parent::atomic(function () use ($fx, $eRollback, &$res) { $res = $fx(); - throw $eRollback; + if (!$this->isAllowDbModifications()) { + throw $eRollback; + } }); } catch (\Exception $e) { if ($e !== $eRollback) { @@ -44,39 +55,52 @@ public function atomic(\Closure $fx) return $res; } + /** + * @param \Closure(Model): string $outputCallback + */ protected function wrapUserActionCallbackPreventModification(Model\UserAction $action, \Closure $outputCallback): void { $originalCallback = $action->callback; $action->callback = function (Model $model, ...$args) use ($action, $originalCallback, $outputCallback) { if ($model->isEntity()) { $action = $action->getActionForEntity($model); + $loadedEntity = clone $model; } $callbackBackup = $action->callback; try { $action->callback = $originalCallback; - $action->execute(...$args); + $res = $action->execute(...$args); + + if ($this->isAllowDbModifications()) { + return $res; + } } finally { $action->callback = $callbackBackup; } - return $outputCallback($model, ...$args); + return $outputCallback($model->isEntity() && !$model->isLoaded() ? $loadedEntity : $model, ...$args); }; } protected function initPreventModification(): void { - $this->wrapUserActionCallbackPreventModification($this->getUserAction('add'), function (Model $model) { - return 'Form Submit! Data are not save in demo mode.'; + $makeMessageFx = function (string $actionName, Model $model) { + return $model->getModelCaption() . ' action "' . $actionName . '" with "' . $model->getTitle() . '" entity ' + . ' was executed. In demo mode all changes are reversed.'; + }; + + $this->wrapUserActionCallbackPreventModification($this->getUserAction('add'), function (Model $model) use ($makeMessageFx) { + return $makeMessageFx('add', $model); }); - $this->wrapUserActionCallbackPreventModification($this->getUserAction('edit'), function (Model $model) { - return 'Form Submit! Data are not save in demo mode.'; + $this->wrapUserActionCallbackPreventModification($this->getUserAction('edit'), function (Model $model) use ($makeMessageFx) { + return $makeMessageFx('edit', $model); }); $this->getUserAction('delete')->confirmation = 'Please go ahead. Demo mode does not really delete data.'; - $this->wrapUserActionCallbackPreventModification($this->getUserAction('delete'), function (Model $model) { - return 'Only simulating delete when in demo mode.'; + $this->wrapUserActionCallbackPreventModification($this->getUserAction('delete'), function (Model $model) use ($makeMessageFx) { + return $makeMessageFx('delete', $model); }); } } @@ -213,7 +237,7 @@ public function validate(string $intent = null): array // look if name is unique $c = $this->getModel()->tryLoadBy($this->fieldName()->name, $this->name); - if ($c !== null && $c->getId() !== $this->getId()) { + if ($c !== null && !$this->compare($this->idField, $c->getId())) { $errors[$this->fieldName()->name] = 'Country name must be unique'; } @@ -269,6 +293,7 @@ protected function init(): void 'type' => 'string', 'ui' => [ 'form' => [Form\Control\Line::class], + 'table' => [Table\Column\CountryFlag::class], ], ]) ->addField($this->fieldName()->client_country, Country::hinting()->fieldName()->name); @@ -277,12 +302,6 @@ protected function init(): void $this->addField($this->fieldName()->currency, ['values' => ['EUR' => 'Euro', 'USD' => 'US Dollar', 'GBP' => 'Pound Sterling']]); $this->addField($this->fieldName()->currency_symbol, ['neverPersist' => true]); $this->onHook(Model::HOOK_AFTER_LOAD, function (self $model) { - /* implementation for "intl" - $locale = 'en-UK'; - $fmt = new \NumberFormatter($locale . '@currency=' . $model->currency, NumberFormatter::CURRENCY); - $model->currency_symbol = $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL); - */ - $map = ['EUR' => '€', 'USD' => '$', 'GBP' => '£']; $model->currency_symbol = $map[$model->currency] ?? '?'; }); @@ -311,7 +330,7 @@ protected function init(): void class Percent extends Field { - public ?string $type = 'float'; + public string $type = 'float'; } /** @@ -348,9 +367,6 @@ protected function init(): void ->addTitle(); } - /** - * Perform import from filesystem. - */ public function importFromFilesystem(string $path, bool $isSub = null): void { if ($isSub === null) { @@ -363,11 +379,6 @@ public function importFromFilesystem(string $path, bool $isSub = null): void $this->atomic(function () use ($path) { foreach ($this as $entity) { $entity->delete(); - - // skip full/slow import for Behat testing - if ($_ENV['CI'] ?? null) { - break; - } } $path = __DIR__ . '/../' . $path; @@ -379,7 +390,7 @@ public function importFromFilesystem(string $path, bool $isSub = null): void } foreach (new \DirectoryIterator($path) as $fileinfo) { - if ($fileinfo->isDot() || in_array($fileinfo->getFilename(), ['.git', 'vendor', 'js'], true)) { + if ($fileinfo->isDot() || in_array($fileinfo->getFilename(), ['.git', 'vendor', 'node_modules', 'external'], true)) { continue; } @@ -395,7 +406,7 @@ public function importFromFilesystem(string $path, bool $isSub = null): void $entity->SubFolder->importFromFilesystem($fileinfo->getPath() . '/' . $fileinfo->getFilename(), true); } - // skip full/slow import for Behat testing + // skip full/slow import for Behat CI testing if ($_ENV['CI'] ?? null) { break; } @@ -489,3 +500,63 @@ protected function init(): void ])->addTitle(); } } + +/** + * @property string $item @Atk4\Field() + * @property \DateTime $inv_date @Atk4\Field() + * @property \DateTime $inv_time @Atk4\Field() + * @property Country $country_id @Atk4\RefOne() + * @property int $qty @Atk4\Field() + * @property int $box @Atk4\Field() + * @property int $total_sql @Atk4\Field() + * @property int $total_php @Atk4\Field() + */ +class MultilineItem extends ModelWithPrefixedFields +{ + public $table = 'multiline_item'; + + protected function init(): void + { + parent::init(); + + $this->addField($this->fieldName()->item, ['required' => true]); + $this->addField($this->fieldName()->inv_date, ['type' => 'date']); + $this->addField($this->fieldName()->inv_time, ['type' => 'time']); + $this->hasOne($this->fieldName()->country_id, [ + 'model' => [Country::class], + ]); + $this->addField($this->fieldName()->qty, ['type' => 'integer', 'required' => true]); + $this->addField($this->fieldName()->box, ['type' => 'integer', 'required' => true]); + $this->addExpression($this->fieldName()->total_sql, [ + 'expr' => function (Model /* TODO self is not working because of clone in Multiline */ $row) { + return $row->expr('{' . $this->fieldName()->qty . '} * {' . $this->fieldName()->box . '}'); // @phpstan-ignore-line + }, + 'type' => 'integer', + ]); + $this->addCalculatedField($this->fieldName()->total_php, [ + 'expr' => function (self $row) { + return $row->qty * $row->box; + }, + 'type' => 'integer', + ]); + } +} + +/** + * @property string $name @Atk4\Field() + * @property Country $country @Atk4\RefOne() + * @property MultilineItem $items @Atk4\RefMany() + */ +class MultilineDelivery extends ModelWithPrefixedFields +{ + public $table = 'multiline_delivery'; + + protected function init(): void + { + parent::init(); + + $this->addField($this->fieldName()->name, ['required' => true]); + $this->containsOne($this->fieldName()->country, ['model' => [Country::class]]); + $this->containsMany($this->fieldName()->items, ['model' => [MultilineItem::class]]); + } +} diff --git a/demos/interactive/accordion-nested.php b/demos/interactive/accordion-nested.php index a845921ee2..0f9737d6c4 100644 --- a/demos/interactive/accordion-nested.php +++ b/demos/interactive/accordion-nested.php @@ -41,7 +41,7 @@ $form = Form::addTo($vp); $form->addControl('email'); $form->onSubmit(function (Form $form) { - return $form->success('Subscribed ' . $form->model->get('email') . ' to newsletter.'); + return $form->jsSuccess('Subscribed ' . $form->model->get('email') . ' to newsletter.'); }); $addAccordionFunc($vp, $maxDepth, $level + 1); diff --git a/demos/interactive/accordion.php b/demos/interactive/accordion.php index 79249158a1..27f43fbd50 100644 --- a/demos/interactive/accordion.php +++ b/demos/interactive/accordion.php @@ -49,7 +49,7 @@ $form = Form::addTo($vp); $form->addControl('Email'); $form->onSubmit(function (Form $form) { - return $form->success('Subscribed ' . $form->model->get('Email') . ' to newsletter.'); + return $form->jsSuccess('Subscribed ' . $form->model->get('Email') . ' to newsletter.'); }); }); diff --git a/demos/interactive/card-action.php b/demos/interactive/card-action.php index 8dcd6e29c4..f7303afc26 100644 --- a/demos/interactive/card-action.php +++ b/demos/interactive/card-action.php @@ -15,7 +15,7 @@ Button::addTo($app, ['Card', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']) ->link(['card']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Models', 'size' => 1, 'subHeader' => 'Card may display information from many models.']); diff --git a/demos/interactive/card.php b/demos/interactive/card.php index 3e9cd10027..e5d93e0443 100644 --- a/demos/interactive/card.php +++ b/demos/interactive/card.php @@ -15,7 +15,7 @@ Button::addTo($app, ['Card Model', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['card-action']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Card.', 'size' => 1, 'subHeader' => 'Component based on Fomantic-UI Card view.']); @@ -40,7 +40,7 @@ $card = Card::addTo($app); $content = new View(['class' => ['content']]); $img = Image::addTo($content, ['../images/kristy.png']); -$img->addClass('right floated mini ui image'); +$img->addClass('right floated mini'); $header = Header::addTo($content, ['Kristy']); $card->addContent($content); @@ -57,7 +57,7 @@ $stat = (new Stat($app->db))->loadAny(); $cardStat->setModel($stat, [$stat->fieldName()->project_name, $stat->fieldName()->project_code, $stat->fieldName()->client_name, $stat->fieldName()->start_date]); -$btn = $cardStat->addButton(new Button(['Email Client'])); +$button = $cardStat->addButton(new Button(['Email Client'])); $cardStat = Card::addTo($deck, ['useLabel' => true]); $cardStat->addContent(new Header(['Project Info'])); diff --git a/demos/interactive/console.php b/demos/interactive/console.php index 9a302695db..c409e77189 100644 --- a/demos/interactive/console.php +++ b/demos/interactive/console.php @@ -10,6 +10,7 @@ use Atk4\Ui\Console; use Atk4\Ui\Form; use Atk4\Ui\Header; +use Atk4\Ui\Js\JsBlock; use Atk4\Ui\Message; use Atk4\Ui\Tabs; use Atk4\Ui\View; @@ -18,7 +19,6 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/** @var View $testRunClass */ $testRunClass = AnonymousClassNameCache::get_class(fn () => new class() extends View { use DebugTrait; @@ -62,8 +62,8 @@ public function generateReport() $tab = $tabs->addTab('runMethod()', function (VirtualPage $vp) use ($testRunClass) { Header::addTo($vp, [ 'icon' => 'terminal', - 'Non-interractive method invocation', - 'subHeader' => 'console can invoke a method, which normaly would be non-interractive and can still capture debug output', + 'Non-interactive method invocation', + 'subHeader' => 'console can invoke a method, which normally would be non-interactive and can still capture debug output', ]); Console::addTo($vp)->runMethod($testRunClass::addTo($vp), 'generateReport'); }); @@ -107,7 +107,7 @@ public function generateReport() $button = Button::addTo($message, ['I understand, proceed anyway', 'class.primary big' => true]); $console = Console::addTo($vp, ['event' => false]); - $console->exec('bash', ['-c', 'cd ../..; echo "Running \'composer update\' in `pwd`"; composer --no-ansi update; echo "Self-updated. OK to refresh now!"']); + $console->exec('bash', ['-c', 'cd ../..; echo \'Running "composer update" in `pwd`\'; composer --no-ansi update; echo \'Self-updated. OK to refresh now!\'']); $button->on('click', $console->jsExecute()); }); @@ -122,7 +122,8 @@ public function generateReport() session_start(); $form = Form::addTo($vp); - $form->addControls([['foo'], ['bar']]); + $form->addControl('foo'); + $form->addControl('bar'); $console = Console::addTo($vp, ['event' => false]); $console->set(function (Console $console) { @@ -139,9 +140,9 @@ public function generateReport() $form->onSubmit(function (Form $form) use ($console) { $_SESSION['atk4_ui_console_demo'] = $form->model; // only option is to store model in session here in demo - return [ + return new JsBlock([ $console->js()->show(), $console->jsExecute(), - ]; + ]); }); }); diff --git a/demos/interactive/jssortable.php b/demos/interactive/jssortable.php index bda9fcd34a..9bd12dc5c3 100644 --- a/demos/interactive/jssortable.php +++ b/demos/interactive/jssortable.php @@ -8,8 +8,8 @@ use Atk4\Ui\Grid; use Atk4\Ui\Header; use Atk4\Ui\HtmlTemplate; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\JsSortable; -use Atk4\Ui\JsToast; use Atk4\Ui\Lister; use Atk4\Ui\View; @@ -18,7 +18,7 @@ $view = View::addTo($app, ['template' => new HtmlTemplate( '
Click and drag country to reorder
-
+
    {List}
  • {$atk_fp_country__name}
  • {/}
@@ -45,12 +45,12 @@ }); $button = Button::addTo($app)->set('Get countries order'); -$button->js('click', $sortable->jsGetOrders(['btn' => '1'])); +$button->on('click', $sortable->jsSendSortOrders(['btn' => '1'])); // ----------------------------------------------------------------------------- View::addTo($app, ['ui' => 'divider']); -Header::addTo($app, ['Add Drag n drop to Grid']); +Header::addTo($app, ['Add drag sorting to grid']); $grid = Grid::addTo($app, ['paginator' => false]); $grid->setModel((new Country($app->db))->setLimit(6)); diff --git a/demos/interactive/loader.php b/demos/interactive/loader.php index 9cb3d4af18..d9138c5951 100644 --- a/demos/interactive/loader.php +++ b/demos/interactive/loader.php @@ -29,7 +29,7 @@ Header::addTo($p, ['Loader #1']); LoremIpsum::addTo($p, ['size' => 1]); - // Any dynamic views can perform call-backs just fine + // Any dynamic views can perform callbacks just fine ViewTester::addTo($p); // Loader may be inside another loader, works fine. @@ -51,19 +51,21 @@ }); // button may contain load event. - Button::addTo($p, ['Load Segment Manually (2s)', 'class.red' => true])->js('click', $loader->jsLoad(['color' => 'red'])); - Button::addTo($p, ['Load Segment Manually (2s)', 'class.blue' => true])->js('click', $loader->jsLoad(['color' => 'blue'])); + Button::addTo($p, ['Load Segment Manually (2s)', 'class.red' => true]) + ->on('click', $loader->jsLoad(['color' => 'red'])); + Button::addTo($p, ['Load Segment Manually (2s)', 'class.blue' => true]) + ->on('click', $loader->jsLoad(['color' => 'blue'])); }); // Example 2 - Loader with custom body. Loader::addTo($app, [ - 'ui' => '', // this will prevent "loading spinner" from showing + 'ui' => false, // this will prevent "loading spinner" from showing 'shim' => [ // shim is displayed while content is leaded Message::class, 'Generating LoremIpsum, please wait...', 'class.red' => true, ], ])->set(function (Loader $p) { - usleep(500_000); + sleep(1); LoremIpsum::addTo($p, ['size' => 2]); }); diff --git a/demos/interactive/loader2.php b/demos/interactive/loader2.php index 2afc38abd4..0e755bcb66 100644 --- a/demos/interactive/loader2.php +++ b/demos/interactive/loader2.php @@ -17,7 +17,7 @@ Button::addTo($app, ['Loader Example - page 1', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']) ->link(['loader']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); $c = Columns::addTo($app); @@ -26,7 +26,7 @@ $countryLoader = Loader::addTo($c->addColumn(), ['loadEvent' => false, 'shim' => [Text::class, 'Select country on your left']]); -$grid->table->onRowClick($countryLoader->jsLoad(['id' => $grid->table->jsRow()->data('id')])); +$grid->table->onRowClick($countryLoader->jsLoad(['id' => $grid->jsRow()->data('id')])); $countryLoader->set(function (Loader $p) { Form::addTo($p)->setModel((new Country($p->getApp()->db))->load($_GET['id'])); diff --git a/demos/interactive/modal.php b/demos/interactive/modal.php index 6504677e50..0001881c32 100644 --- a/demos/interactive/modal.php +++ b/demos/interactive/modal.php @@ -9,7 +9,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsExpression; +use Atk4\Ui\Js\JsExpression; use Atk4\Ui\LoremIpsum; use Atk4\Ui\Menu; use Atk4\Ui\Message; @@ -31,22 +31,28 @@ $modal = Modal::addTo($app, ['title' => 'Add a name']); LoremIpsum::addTo($modal); -Button::addTo($modal, ['Hide'])->on('click', $modal->hide()); - -$noTitle = Modal::addTo($app, ['title' => false]); -LoremIpsum::addTo($noTitle); -Button::addTo($noTitle, ['Hide'])->on('click', $noTitle->hide()); - -$scrolling = Modal::addTo($app, ['title' => 'Long Content that Scrolls inside Modal']); -$scrolling->addScrolling(); -LoremIpsum::addTo($scrolling); -LoremIpsum::addTo($scrolling); -LoremIpsum::addTo($scrolling); -Button::addTo($scrolling, ['Hide'])->on('click', $scrolling->hide()); - -Button::addTo($bar, ['Show'])->on('click', $modal->show()); -Button::addTo($bar, ['No Title'])->on('click', $noTitle->show()); -Button::addTo($bar, ['Scrolling Content'])->on('click', $scrolling->show()); +Button::addTo($modal, ['Hide']) + ->on('click', $modal->jsHide()); + +$modalNoTitle = Modal::addTo($app, ['title' => false]); +LoremIpsum::addTo($modalNoTitle); +Button::addTo($modalNoTitle, ['Hide']) + ->on('click', $modalNoTitle->jsHide()); + +$modalScrolling = Modal::addTo($app, ['title' => 'Long Content that Scrolls inside Modal']); +$modalScrolling->addScrolling(); +LoremIpsum::addTo($modalScrolling); +LoremIpsum::addTo($modalScrolling); +LoremIpsum::addTo($modalScrolling); +Button::addTo($modalScrolling, ['Hide']) + ->on('click', $modalScrolling->jsHide()); + +Button::addTo($bar, ['Show']) + ->on('click', $modal->jsShow()); +Button::addTo($bar, ['No Title']) + ->on('click', $modalNoTitle->jsShow()); +Button::addTo($bar, ['Scrolling Content']) + ->on('click', $modalScrolling->jsShow()); // Modal demos. @@ -58,16 +64,16 @@ $menuBar = View::addTo($app, ['ui' => 'buttons']); $button = Button::addTo($menuBar)->set('Show Modal'); -$button->on('click', $simpleModal->show()); +$button->on('click', $simpleModal->jsShow()); // DYNAMIC Header::addTo($app, ['Three levels of Modal loading dynamic content via callback']); -// vp1Modal will be render into page but hide until $vp1Modal->show() is activate. +// vp1Modal will be render into page but hide until $vp1Modal->jsShow() is activate. $vp1Modal = Modal::addTo($app, ['title' => 'Lorem Ipsum load dynamically']); -// vp2Modal will be render into page but hide until $vp1Modal->show() is activate. +// vp2Modal will be render into page but hide until $vp1Modal->jsShow() is activate. $vp2Modal = Modal::addTo($app, ['title' => 'Text message load dynamically'])->addClass('small'); $vp3Modal = Modal::addTo($app, ['title' => 'Third level modal'])->addClass('small'); @@ -76,7 +82,7 @@ LoremIpsum::addTo($p, ['size' => 2]); }); -// When $vp1Modal->show() is activate, it will dynamically add this content to it. +// When $vp1Modal->jsShow() is activate, it will dynamically add this content to it. $vp1Modal->set(function (View $p) use ($vp2Modal) { ViewTester::addTo($p); View::addTo($p, ['Showing lorem ipsum']); // need in behat test. @@ -84,20 +90,21 @@ $form = Form::addTo($p); $form->addControl('color', [], ['enum' => ['red', 'green', 'blue'], 'default' => 'green']); $form->onSubmit(function (Form $form) use ($vp2Modal) { - return $vp2Modal->show(['color' => $form->model->get('color')]); + return $vp2Modal->jsShow(['color' => $form->model->get('color')]); }); }); -// When $vp2Modal->show() is activate, it will dynamically add this content to it. +// When $vp2Modal->jsShow() is activate, it will dynamically add this content to it. $vp2Modal->set(function (View $p) use ($vp3Modal) { ViewTester::addTo($p); Message::addTo($p, [$_GET['color'] ?? 'No color'])->text->addParagraph('This text is loaded using a second modal.'); - Button::addTo($p)->set('Third modal')->on('click', $vp3Modal->show()); + Button::addTo($p)->set('Third modal') + ->on('click', $vp3Modal->jsShow()); }); $bar = View::addTo($app, ['ui' => 'buttons']); $button = Button::addTo($bar)->set('Open Lorem Ipsum'); -$button->on('click', $vp1Modal->show()); +$button->on('click', $vp1Modal->jsShow()); // ANIMATION @@ -117,7 +124,7 @@ $transitionModal = Modal::addTo($app, ['title' => 'Animated modal']); Message::addTo($transitionModal)->set('A lot of animated transition available'); -$transitionModal->duration(1000); +$transitionModal->setOption('duration', 1000); $menuBar = View::addTo($app, ['ui' => 'buttons']); $main = Menu::addTo($menuBar); @@ -142,13 +149,13 @@ $denyApproveModal = Modal::addTo($app, ['title' => 'Deny / Approve actions']); Message::addTo($denyApproveModal)->set('This modal is only closable via the green button'); -$denyApproveModal->addDenyAction('No', new JsExpression('function() { window.alert("Can\'t do that."); return false; }')); -$denyApproveModal->addApproveAction('Yes', new JsExpression('function() { window.alert("You\'re good to go!"); }')); +$denyApproveModal->addDenyAction('No', new JsExpression('function () { window.alert(\'Cannot do that.\'); return false; }')); +$denyApproveModal->addApproveAction('Yes', new JsExpression('function () { window.alert(\'You are good to go!\'); }')); $denyApproveModal->notClosable(); $menuBar = View::addTo($app, ['ui' => 'buttons']); $button = Button::addTo($menuBar)->set('Show Deny/Approve'); -$button->on('click', $denyApproveModal->show()); +$button->on('click', $denyApproveModal->jsShow()); // MULTI STEP @@ -156,27 +163,26 @@ // Add modal to layout. $stepModal = Modal::addTo($app, ['title' => 'Multi step actions']); -$stepModal->setOption('observeChanges', true); // Add buttons to modal for next and previous actions. $action = new View(['ui' => 'buttons']); -$prevAction = new Button(['Prev', 'class.labeled' => true, 'icon' => 'left arrow']); +$previousAction = new Button(['Previous', 'icon' => 'left arrow']); $nextAction = new Button(['Next', 'iconRight' => 'right arrow']); -$action->add($prevAction); +$action->add($previousAction); $action->add($nextAction); $stepModal->addButtonAction($action); // Set modal functionality. Will changes content according to page being displayed. -$stepModal->set(function (View $p) use ($stepModal, $session, $prevAction, $nextAction) { +$stepModal->set(function (View $p) use ($session, $previousAction, $nextAction) { $page = $session->recall('page', 1); $success = $session->recall('success', false); if (isset($_GET['move'])) { if ($_GET['move'] === 'next' && $success) { ++$page; } - if ($_GET['move'] === 'prev' && $page > 1) { + if ($_GET['move'] === 'previous' && $page > 1) { --$page; } $session->memorize('success', false); @@ -188,10 +194,10 @@ if ($page === 1) { Message::addTo($p)->set('Thanks for choosing us. We will be asking some questions along the way.'); $session->memorize('success', true); - $p->js(true, $prevAction->js(true)->show()); - $p->js(true, $nextAction->js(true)->show()); - $p->js(true, $prevAction->js()->addClass('disabled')); - $p->js(true, $nextAction->js(true)->removeClass('disabled')); + $p->js(true, $previousAction->js()->show()); + $p->js(true, $nextAction->js()->show()); + $p->js(true, $previousAction->js()->addClass('disabled')); + $p->js(true, $nextAction->js()->removeClass('disabled')); } elseif ($page === 2) { $modelRegister = new Model(new Persistence\Array_()); $modelRegister->addField('name', ['caption' => 'Please enter your name (John)']); @@ -201,41 +207,38 @@ $form->onSubmit(function (Form $form) use ($nextAction, $session) { if ($form->model->get('name') !== 'John') { - return $form->error('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); + return $form->jsError('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); } $session->memorize('success', true); $session->memorize('name', $form->model->get('name')); $js = []; - $js[] = $form->success('Thank you, ' . $form->model->get('name') . ' you can go on!'); + $js[] = $form->jsSuccess('Thank you, ' . $form->model->get('name') . ' you can go on!'); $js[] = $nextAction->js()->removeClass('disabled'); return $js; }); - $p->js(true, $prevAction->js()->removeClass('disabled')); - $p->js(true, $nextAction->js(true)->addClass('disabled')); + $p->js(true, $previousAction->js()->removeClass('disabled')); + $p->js(true, $nextAction->js()->addClass('disabled')); } elseif ($page === 3) { $name = $session->recall('name'); Message::addTo($p)->set("Thank you {$name} for visiting us! We will be in touch"); $session->memorize('success', true); - $p->js(true, $prevAction->js(true)->hide()); - $p->js(true, $nextAction->js(true)->hide()); + $p->js(true, $previousAction->js()->hide()); + $p->js(true, $nextAction->js()->hide()); } - $stepModal->js(true)->modal('refresh'); }); -// Bind next action to modal next button. -$nextAction->on('click', $stepModal->js()->atkReloadView( - ['uri' => $stepModal->cb->getJsUrl(), 'uri_options' => ['move' => 'next']] +$previousAction->on('click', $stepModal->js()->atkReloadView( + ['url' => $stepModal->cb->getJsUrl(), 'urlOptions' => ['move' => 'previous']] )); -// Bin prev action to modal previous button. -$prevAction->on('click', $stepModal->js()->atkReloadView( - ['uri' => $stepModal->cb->getJsUrl(), 'uri_options' => ['move' => 'prev']] +$nextAction->on('click', $stepModal->js()->atkReloadView( + ['url' => $stepModal->cb->getJsUrl(), 'urlOptions' => ['move' => 'next']] )); // Bind display modal to page display button. $menuBar = View::addTo($app, ['ui' => 'buttons']); $button = Button::addTo($menuBar)->set('Multi Step Modal'); -$button->on('click', $stepModal->show()); +$button->on('click', $stepModal->jsShow()); diff --git a/demos/interactive/popup.php b/demos/interactive/popup.php index 46f1d4727e..131aa4eed5 100644 --- a/demos/interactive/popup.php +++ b/demos/interactive/popup.php @@ -9,9 +9,10 @@ use Atk4\Ui\Dropdown as UiDropdown; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsExpressionable; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsExpressionable; use Atk4\Ui\Label; use Atk4\Ui\Lister; use Atk4\Ui\Menu; @@ -24,14 +25,12 @@ /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -/* +/** * Example implementation of a dynamic view which support session. * * Cart will memorize and restore its items into session. Cart will also * render the items. */ - -/** @var Lister $cartClass */ $cartClass = AnonymousClassNameCache::get_class(fn () => new class() extends Lister { use SessionTrait; @@ -96,14 +95,12 @@ protected function renderView(): void } }); -/* +/** * Implementation of a generic item shelf. Shows selection of products and allow to bind click event. * * Method linkCart allow you to link ItemShelf with Cart. Clicking on a shelf item will place that * item inside a cart reloading it afterwards. */ - -/** @var View $itemShelfClass */ $itemShelfClass = AnonymousClassNameCache::get_class(fn () => new class() extends View { public $ui = 'green segment'; @@ -141,12 +138,8 @@ protected function init(): void /** * Associate your shelf with cart, so that when item is clicked, the content of a * cart is updated. - * - * Also - you can supply jsAction to execute when this happens. - * - * @param JsExpressionable|array $jsAction */ - public function linkCart(View $cart, $jsAction = null): void + public function linkCart(View $cart, JsExpressionable $jsAction = null): void { $this->on('click', '.item', function (Jquery $a, string $b) use ($cart, $jsAction) { $cart->addItem($b); @@ -168,18 +161,18 @@ public function linkCart(View $cart, $jsAction = null): void $cartItem = $menu->addItem([$cartClass, 'icon' => 'cart'])->set('Cart'); $cartPopup = Popup::addTo($app, [$cartItem, 'position' => 'bottom left']); -// Popup won't dissapear as you hover over it. +// Popup won't disappear as you hover over it. $cartPopup->setHoverable(); $shelf = $itemShelfClass::addTo($app); // Here we are facing a pretty interesting problem. If you attempt to put "Cart" object inside a popup directly, // it won't work, because it will be located inside the menu item's DOM tree and, although hidden, will be -// impacted by some css rules of the menu. +// impacted by some CSS rules of the menu. // // This can happen when your popup content is non-trivial. So we are moving Popup into the app and linking up // the triggers. Now, since it's outside, we can't use a single jsAction to reload menu item (along with label) -// and the contens. We could use 2 requests for reloading, but that's not good. +// and the content. We could use 2 requests for reloading, but that's not good. // // The next idea is to make cart dynamic, so it loads when you move mouse over the menu. This probably is good, // as it will always be accurate, even if you added items form multiple browser tabs. @@ -201,9 +194,9 @@ public function linkCart(View $cart, $jsAction = null): void // Label now can be added referencing Cart's items. Init() was colled when I added it into app, so the // item property is populated. -$cartOutterLabel = Label::addTo($cartItem, [count($cart->items), 'class.floating red' => true]); +$cartOuterLabel = Label::addTo($cartItem, [(string) count($cart->items), 'class.floating red' => true]); if (!$cart->items) { - $cartOutterLabel->addStyle('display', 'none'); + $cartOuterLabel->setStyle('display', 'none'); } $cartPopup->set(function (View $popup) use ($cart) { @@ -219,13 +212,13 @@ public function linkCart(View $cart, $jsAction = null): void }); // Add item shelf below menu and link it with the cart -$shelf->linkCart($cart, [ - // array is a valid js action. Will relad cart item (along with drop-down and label) - $cartOutterLabel->jsReload(), +$shelf->linkCart($cart, new JsBlock([ + // array is a valid JS action. Will relad cart item (along with drop-down and label) + $cartOuterLabel->jsReload(), // also will hide current item from the shelf (new Jquery())->hide(), -]); +])); // label placed on top of menu item, not in the popup @@ -248,7 +241,7 @@ public function linkCart(View $cart, $jsAction = null): void // This popup will be dynamically loaded. $signup->stickyGet('logged'); $signup->set(function (View $pop) { - // contetn of the popup will be different depending on this condition. + // content of the popup will be different depending on this condition. if (isset($_GET['logged'])) { Message::addTo($pop, ['You are already logged in as ' . $_GET['logged']]); Button::addTo($pop, ['Logout', 'class.primary' => true, 'icon' => 'sign out']) @@ -263,7 +256,7 @@ public function linkCart(View $cart, $jsAction = null): void // perfectly inside a popup. $form->onSubmit(function (Form $form) { if ($form->model->get('password') !== '123') { - return $form->error('password', 'Please use password "123"'); + return $form->jsError('password', 'Please use password "123"'); } // refreshes entire page @@ -292,4 +285,5 @@ public function linkCart(View $cart, $jsAction = null): void $button = Button::addTo($app, [null, 'icon' => 'volume down']); $buttonPopup = Popup::addTo($app, [$button, 'triggerOn' => 'hover'])->setHoverable(); -Form\Control\Checkbox::addTo($buttonPopup, ['Just On/Off', 'class.slider' => true])->on('change', $button->js()->find('.icon')->toggleClass('up down')); +Form\Control\Checkbox::addTo($buttonPopup, ['Just On/Off', 'class.slider' => true]) + ->on('change', $button->js()->find('.icon')->toggleClass('up down')); diff --git a/demos/interactive/progress.php b/demos/interactive/progress.php index 414326a876..c0608623a0 100644 --- a/demos/interactive/progress.php +++ b/demos/interactive/progress.php @@ -13,5 +13,7 @@ $p = ProgressBar::addTo($app, [20]); $p = ProgressBar::addTo($app, [60, 'indicating progress', 'class.indicating' => true]); -Button::addTo($app, ['increment'])->on('click', $p->jsIncrement()); -Button::addTo($app, ['set'])->on('click', $p->jsValue(20)); +Button::addTo($app, ['increment']) + ->on('click', $p->jsIncrement()); +Button::addTo($app, ['set']) + ->on('click', $p->jsValue(20)); diff --git a/demos/interactive/scroll-container.php b/demos/interactive/scroll-container.php index 26a2a7b9af..8446ce095c 100644 --- a/demos/interactive/scroll-container.php +++ b/demos/interactive/scroll-container.php @@ -17,15 +17,17 @@ ->link(['scroll-table']); Button::addTo($app, ['Dynamic scroll in Grid', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['scroll-grid']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Dynamic scroll in Container']); $view = View::addTo($app)->addClass('ui basic segment atk-scroller'); -$scrollContainer = View::addTo($view)->addClass('ui segment')->addStyle(['max-height' => '400px', 'overflow-y' => 'scroll']); +$scrollContainer = View::addTo($view)->addClass('ui segment')->setStyle(['max-height' => '400px', 'overflow-y' => 'scroll']); -$listerTemplate = '
{List}
{name}andorra{/}
{/}{$Content}
'; +$listerTemplate = '
{List}
{$' + . Country::hinting()->fieldName()->name . '}
{/}{$Content}
'; $listerContainer = View::addTo($scrollContainer, ['template' => new HtmlTemplate($listerTemplate)]); @@ -36,5 +38,4 @@ }); $lister->setModel(new Country($app->db)); -// add dynamic scrolling. $lister->addJsPaginator(20, ['stateContext' => '.atk-scroller'], $scrollContainer); diff --git a/demos/interactive/scroll-grid-container.php b/demos/interactive/scroll-grid-container.php index 7652d68234..3621f7c6ac 100644 --- a/demos/interactive/scroll-grid-container.php +++ b/demos/interactive/scroll-grid-container.php @@ -10,7 +10,7 @@ use Atk4\Ui\Crud; use Atk4\Ui\Grid; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; +use Atk4\Ui\Js\Jquery; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -18,7 +18,7 @@ Button::addTo($app, ['Dynamic scroll in Crud and Grid', 'class.small left floated basic blue' => true, 'icon' => 'left arrow']) ->link(['scroll-grid']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Dynamic scroll in Grid with fixed column headers']); diff --git a/demos/interactive/scroll-grid.php b/demos/interactive/scroll-grid.php index 76d732f44c..841b50926b 100644 --- a/demos/interactive/scroll-grid.php +++ b/demos/interactive/scroll-grid.php @@ -16,7 +16,7 @@ ->link(['scroll-container']); Button::addTo($app, ['Dynamic scroll in Grid using Container', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['scroll-grid-container']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Dynamic scroll in Grid']); diff --git a/demos/interactive/scroll-lister.php b/demos/interactive/scroll-lister.php index d4230c92ab..0833fe7699 100644 --- a/demos/interactive/scroll-lister.php +++ b/demos/interactive/scroll-lister.php @@ -15,14 +15,14 @@ Button::addTo($app, ['Dynamic scroll in Table', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['scroll-table']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Dynamic scroll in Lister']); $container = View::addTo($app); $view = View::addTo($container, ['template' => new HtmlTemplate(' -{List}
{$atk_fp_country__name}
{/} +{List}
{$atk_fp_country__name}
{/} {$Content}')]); $lister = Lister::addTo($view, [], ['List']); @@ -33,7 +33,5 @@ $model = new Country($app->db); $lister->setModel($model); -// $model->addCondition(Country::hinting()->fieldName()->name, 'like', 'A%'); -// add dynamic scrolling. $lister->addJsPaginator(30, [], $container); diff --git a/demos/interactive/scroll-table.php b/demos/interactive/scroll-table.php index bf594f072d..0a9bf764f1 100644 --- a/demos/interactive/scroll-table.php +++ b/demos/interactive/scroll-table.php @@ -16,7 +16,7 @@ ->link(['scroll-lister']); Button::addTo($app, ['Dynamic scroll in Container', 'class.small right floated basic blue' => true, 'iconRight' => 'right arrow']) ->link(['scroll-container']); -View::addTo($app, ['ui' => 'ui clearing divider']); +View::addTo($app, ['ui' => 'clearing divider']); Header::addTo($app, ['Dynamic scroll in Table']); @@ -24,6 +24,5 @@ $model = new Country($app->db); $table->setModel($model); -// $model->addCondition(Country::hinting()->fieldName()->name, 'like', 'A%'); $table->addJsPaginator(30); diff --git a/demos/interactive/sse.php b/demos/interactive/sse.php index f7fa28bd8a..f41c373354 100644 --- a/demos/interactive/sse.php +++ b/demos/interactive/sse.php @@ -6,7 +6,8 @@ use Atk4\Ui\Button; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsBlock; use Atk4\Ui\JsSse; use Atk4\Ui\ProgressBar; use Atk4\Ui\View; @@ -23,7 +24,7 @@ // non-SSE way // $button->on('click', $bar->js()->progress(['percent' => 40])); -$sse = JsSse::addTo($app, ['showLoader' => true]); +$sse = JsSse::addTo($button, ['showLoader' => true]); $button->on('click', $sse->set(function () use ($button, $sse, $bar) { $sse->send($button->js()->addClass('disabled')); @@ -38,13 +39,16 @@ sleep(1); // non-SSE way - return [ + return new JsBlock([ $bar->jsValue(100), $button->js()->removeClass('disabled'), - ]; + ]); })); -$buttonStop->on('click', [$button->js()->atkServerEvent('stop'), $button->js()->removeClass('disabled')]); +$buttonStop->on('click', new JsBlock([ + $button->js()->atkServerEvent('stop'), + $button->js()->removeClass('disabled'), +])); View::addTo($app, ['ui' => 'divider']); Header::addTo($app, ['SSE operation with user confirmation']); @@ -52,9 +56,9 @@ $sse = JsSse::addTo($app); $button = Button::addTo($app, ['Click me to change my text']); -$button->on('click', $sse->set(function (Jquery $jsChain) use ($sse, $button) { +$button->on('click', $sse->set(function (Jquery $jsChain, string $newButtonText) use ($sse, $button) { $sse->send($button->js()->text('Please wait for 2 seconds...')); sleep(2); - return $button->js()->text($sse->args['newButtonText']); + return $button->js()->text($newButtonText); }, ['newButtonText' => 'This is my new text!']), ['confirm' => 'Please confirm that you wish to continue']); diff --git a/demos/interactive/tabs.php b/demos/interactive/tabs.php index 48cb82c941..39f031c911 100644 --- a/demos/interactive/tabs.php +++ b/demos/interactive/tabs.php @@ -40,9 +40,10 @@ // modal tab $tabs->addTab('Modal popup', function (VirtualPage $vp) { - Button::addTo($vp, ['Load Lorem'])->on('click', Modal::addTo($vp)->set(function (View $p) { - LoremIpsum::addTo($p, ['size' => 2]); - })->show()); + Button::addTo($vp, ['Load Lorem']) + ->on('click', Modal::addTo($vp)->set(function (View $p) { + LoremIpsum::addTo($p, ['size' => 2]); + })->jsShow()); }); // dynamic tab @@ -56,7 +57,7 @@ $form->setModel($modelRegister->createEntity()); $form->onSubmit(function (Form $form) { if ($form->model->get('name') !== 'John') { - return $form->error('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); + return $form->jsError('name', 'Your name is not John! It is "' . $form->model->get('name') . '". It should be John. Pleeease!'); } }); }); diff --git a/demos/interactive/toast.php b/demos/interactive/toast.php index 7728a95775..1917b4b7e0 100644 --- a/demos/interactive/toast.php +++ b/demos/interactive/toast.php @@ -6,42 +6,40 @@ use Atk4\Ui\Button; use Atk4\Ui\Header; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsToast; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; Header::addTo($app, ['Toast']); -$btn = Button::addTo($app)->set('Minimal'); +$button = Button::addTo($app)->set('Minimal'); +$button->on('click', new JsToast('Hi there!')); -$btn->on('click', new JsToast('Hi there!')); - -$btn = Button::addTo($app)->set('Using a title'); - -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Using a title'); +$button->on('click', new JsToast([ 'title' => 'Title', 'message' => 'See I have a title', ])); Header::addTo($app, ['Using class name']); -$btn = Button::addTo($app)->set('Success'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Success'); +$button->on('click', new JsToast([ 'title' => 'Success', 'message' => 'Well done', 'class' => 'success', ])); -$btn = Button::addTo($app)->set('Error'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Error'); +$button->on('click', new JsToast([ 'title' => 'Error', - 'message' => 'An error occured', + 'message' => 'An error occurred', 'class' => 'error', ])); -$btn = Button::addTo($app)->set('Warning'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Warning'); +$button->on('click', new JsToast([ 'title' => 'Warning', 'message' => 'Behind you!', 'class' => 'warning', @@ -49,15 +47,15 @@ Header::addTo($app, ['Using different position']); -$btn = Button::addTo($app)->set('Bottom Right'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Bottom Right'); +$button->on('click', new JsToast([ 'title' => 'Bottom Right', 'message' => 'Should appear at the bottom on your right', 'position' => 'bottom right', ])); -$btn = Button::addTo($app)->set('Top Center'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Top Center'); +$button->on('click', new JsToast([ 'title' => 'Top Center', 'message' => 'Should appear at the top center', 'position' => 'top center', @@ -65,30 +63,30 @@ Header::addTo($app, ['Other Options']); -$btn = Button::addTo($app)->set('5 seconds'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('5 seconds'); +$button->on('click', new JsToast([ 'title' => 'Timeout', 'message' => 'I will stay here for 5 sec.', 'displayTime' => 5000, ])); -$btn = Button::addTo($app)->set('For ever'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('For ever'); +$button->on('click', new JsToast([ 'title' => 'No Timeout', 'message' => 'I will stay until you click me', 'displayTime' => 0, ])); -$btn = Button::addTo($app)->set('Using Message style'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('Using Message style'); +$button->on('click', new JsToast([ 'title' => 'Awesome', 'message' => 'I got my style from the message class', 'class' => 'purple', - 'className' => ['toast' => 'ui message', 'title' => 'ui header'], + 'className' => ['toast' => 'ui message', 'title' => 'header cust'], ])); -$btn = Button::addTo($app)->set('With progress bar'); -$btn->on('click', new JsToast([ +$button = Button::addTo($app)->set('With progress bar'); +$button->on('click', new JsToast([ 'title' => 'Awesome', 'message' => 'See how long I will last', 'showProgress' => 'bottom', diff --git a/demos/interactive/virtual.php b/demos/interactive/virtual.php index 1b1be40c3a..731c541d10 100644 --- a/demos/interactive/virtual.php +++ b/demos/interactive/virtual.php @@ -6,7 +6,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Header; -use Atk4\Ui\JsModal; +use Atk4\Ui\Js\JsModal; use Atk4\Ui\LoremIpsum; use Atk4\Ui\Message; use Atk4\Ui\Modal; @@ -38,18 +38,18 @@ LoremIpsum::addTo($p, ['size' => 2]); }); $button = Button::addTo($virtualPage)->set('Open Lorem Ipsum'); -$button->on('click', $modal->show()); +$button->on('click', $modal->jsShow()); $msg = Message::addTo($app, ['Virtual Page']); $msg->text->addParagraph('Virtual page content are not rendered on page load. They will ouptput their content when trigger.'); $msg->text->addParagraph('Click button below to trigger it.'); // button that trigger virtual page. -$btn = Button::addTo($app, ['More info on Car']); -$btn->link($virtualPage->cb->getUrl() . '&p_id=Car'); +$button = Button::addTo($app, ['More info on Car']); +$button->link($virtualPage->cb->getUrl() . '&p_id=Car'); -$btn = Button::addTo($app, ['More info on Bike']); -$btn->link($virtualPage->cb->getUrl() . '&p_id=Bike'); +$button = Button::addTo($app, ['More info on Bike']); +$button->link($virtualPage->cb->getUrl() . '&p_id=Bike'); // Test 1 - Basic reloading Header::addTo($app, ['Virtual Page Logic']); @@ -70,18 +70,21 @@ Header::addTo($app, ['Inside Modal', 'subHeader' => 'Virtual page content can be display using JsModal Class.']); $bar = View::addTo($app, ['ui' => 'buttons']); -Button::addTo($bar)->set('Load in Modal')->on('click', new JsModal('My Popup Title', $virtualPage->getJsUrl('cut'))); +Button::addTo($bar)->set('Load in Modal') + ->on('click', new JsModal('My Popup Title', $virtualPage->getJsUrl('cut'))); -Button::addTo($bar)->set('Simulate slow load')->on('click', new JsModal('My Popup Title', $virtualPage->getJsUrl('cut') . '&slow=true')); +Button::addTo($bar)->set('Simulate slow load') + ->on('click', new JsModal('My Popup Title', $virtualPage->getJsUrl('cut') . '&slow=true')); if (isset($_GET['slow'])) { sleep(1); } -Button::addTo($bar)->set('No title')->on('click', new JsModal(null, $virtualPage->getJsUrl('cut'))); +Button::addTo($bar)->set('No title') + ->on('click', new JsModal(null, $virtualPage->getJsUrl('cut'))); View::addTo($app, ['ui' => 'hidden divider']); $text = Text::addTo($app); -$text->addParagraph('Can also be trigger from a js event, like clicking on a table row.'); +$text->addParagraph('Can also be trigger from a JS event, like clicking on a table row.'); $table = Table::addTo($app, ['class.celled' => true]); $table->setModel(new SomeData()); diff --git a/demos/interactive/wizard.php b/demos/interactive/wizard.php index 3a9937b6db..481ecff23c 100644 --- a/demos/interactive/wizard.php +++ b/demos/interactive/wizard.php @@ -17,7 +17,7 @@ require_once __DIR__ . '/../init-app.php'; $wizard = Wizard::addTo($app, ['urlTrigger' => 'demo_wizard']); -// First step will automatcally be active when you open page first. It +// First step will automatically be active when you open page first. It // will contain the 'Next' button with a link. $wizard->addStep('Welcome', function (Wizard $wizard) { Message::addTo($wizard, ['Welcome to wizard demonstration'])->text @@ -43,8 +43,8 @@ }); }); -// Alternatvely, you may access buttonNext , buttonPrev properties of a wizard -// and set a custom js action or even set a different link. You can use recall() +// Alternately, you may access buttonNext, buttonPrevious properties of a wizard +// and set a custom JS action or even set a different link. You can use recall() // to access some values that were recorded on another steps. $wizard->addStep(['Select Model', 'description' => '"Country" or "Stat"', 'icon' => 'table'], function (Wizard $wizard) { if (isset($_GET['name'])) { @@ -68,8 +68,8 @@ $wizard->buttonNext->addClass('disabled'); }); -// Steps may contain interractive elements. You can disable navigational buttons -// and enable them as you see fit. Use handy js method to trigger advancement to +// Steps may contain interactive elements. You can disable navigational buttons +// and enable them as you see fit. Use handy JS method to trigger advancement to // the next step. $wizard->addStep(['Migration', 'description' => 'Create or update table', 'icon' => 'database'], function (Wizard $wizard) { $console = Console::addTo($wizard); diff --git a/demos/javascript/js.php b/demos/javascript/js.php index 91231d8e3a..f0ce9080b8 100644 --- a/demos/javascript/js.php +++ b/demos/javascript/js.php @@ -7,14 +7,15 @@ use Atk4\Ui\Button; use Atk4\Ui\Exception; use Atk4\Ui\Header; -use Atk4\Ui\Jquery; -use Atk4\Ui\JsExpression; +use Atk4\Ui\Js\Jquery; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsExpression; use Atk4\Ui\Label; /** @var \Atk4\Ui\App $app */ require_once __DIR__ . '/../init-app.php'; -// Demonstrates how to use interractive buttons. +// Demonstrates how to use interactive buttons. Header::addTo($app, ['Basic Button']); // This button hides on page load @@ -25,7 +26,8 @@ $b = Button::addTo($app, ['name' => 'b2'])->set('Hide on click Button'); $b->js('click')->hide(); -Button::addTo($app, ['Redirect'])->on('click', null, $app->jsRedirect(['foo' => 'bar'])); +Button::addTo($app, ['Redirect']) + ->on('click', null, $app->jsRedirect(['foo' => 'bar'])); if (isset($_GET['foo']) && $_GET['foo'] === 'bar') { $app->redirect(['foo' => 'baz']); @@ -35,13 +37,19 @@ $b = Button::addTo($app, ['Hide button B']); $b2 = Button::addTo($app, ['B']); -$b->js('click', $b2->js()->hide('b2'))->hide('b1'); +$b->on('click', new JsBlock([ + $b->js()->addClass('disabled')->addClass('disabled'), + $b2->js()->hide(), +])); Header::addTo($app, ['on() method']); -$b = Button::addTo($app, ['Hide button C']); +$b = Button::addTo($app, ['Hide button C and self']); $b2 = Button::addTo($app, ['C']); -$b->on('click', null, $b2->js()->hide('c2'))->hide('c1'); +$b->on('click', null, new JsBlock([ + $b->js()->hide(), + $b2->js()->hide(), +])); Header::addTo($app, ['Callbacks']); diff --git a/demos/javascript/reloading.php b/demos/javascript/reloading.php index afe88496d6..bdeb8919a7 100644 --- a/demos/javascript/reloading.php +++ b/demos/javascript/reloading.php @@ -7,8 +7,8 @@ use Atk4\Ui\Button; use Atk4\Ui\Form; use Atk4\Ui\Header; -use Atk4\Ui\JsExpression; -use Atk4\Ui\JsReload; +use Atk4\Ui\Js\JsExpression; +use Atk4\Ui\Js\JsReload; use Atk4\Ui\View; /** @var \Atk4\Ui\App $app */ @@ -17,20 +17,21 @@ // Test 1 - Basic reloading Header::addTo($app, ['Button reloading segment']); $v = View::addTo($app, ['ui' => 'segment'])->set((string) random_int(1, 100)); -Button::addTo($app, ['Reload random number'])->js('click', new JsReload($v, [], new JsExpression('console.log("Output with afterSuccess");'))); +Button::addTo($app, ['Reload random number']) + ->on('click', new JsReload($v, [], new JsExpression('console.log(\'Output with afterSuccess\');'))); // Test 2 - Reloading self Header::addTo($app, ['JS-actions will be re-applied']); $b2 = Button::addTo($app, ['Reload Myself']); -$b2->js('click', new JsReload($b2)); +$b2->on('click', new JsReload($b2)); // Test 3 - avoid duplicate Header::addTo($app, ['No duplicate JS bindings']); $b3 = Button::addTo($app, ['Reload other button']); $b4 = Button::addTo($app, ['Add one dot']); -$b4->js('click', $b4->js()->text(new JsExpression('[]+"."', [$b4->js()->text()]))); -$b3->js('click', new JsReload($b4)); +$b4->on('click', $b4->js()->text(new JsExpression('[] + \'.\'', [$b4->js()->text()]))); +$b3->on('click', new JsReload($b4)); // Test 3 - avoid duplicate Header::addTo($app, ['Make sure nested JS bindings are applied too']); @@ -43,15 +44,19 @@ // Add button to reload all counters $bar = View::addTo($app, ['ui' => 'buttons']); -$b = Button::addTo($bar, ['Reload counter'])->js('click', new JsReload($seg)); +$b = Button::addTo($bar, ['Reload counter']) + ->on('click', new JsReload($seg)); // Relading with argument Header::addTo($app, ['We can pass argument to reloader']); $v = View::addTo($app, ['ui' => 'segment'])->set($_GET['val'] ?? 'No value'); -Button::addTo($app, ['Set value to "hello"'])->js('click', new JsReload($v, ['val' => 'hello'])); -Button::addTo($app, ['Set value to "world"'])->js('click', new JsReload($v, ['val' => 'world'])); +Button::addTo($app, ['Set value to "hello"']) + ->on('click', new JsReload($v, ['val' => 'hello'])); +Button::addTo($app, ['Set value to "world"']) + ->on('click', new JsReload($v, ['val' => 'world'])); $val = Form\Control\Line::addTo($app, ['']); -$val->addAction(['Set Custom Value'])->js('click', new JsReload($v, ['val' => $val->jsInput()->val()], $val->jsInput()->focus())); +$val->addAction(['Set Custom Value']) + ->on('click', new JsReload($v, ['val' => $val->jsInput()->val()], $val->jsInput()->focus())); diff --git a/demos/javascript/vue-component.php b/demos/javascript/vue-component.php index aaa83e5704..3d0a668776 100644 --- a/demos/javascript/vue-component.php +++ b/demos/javascript/vue-component.php @@ -7,6 +7,7 @@ use Atk4\Ui\Button; use Atk4\Ui\Header; use Atk4\Ui\HtmlTemplate; +use Atk4\Ui\Js\JsExpression; use Atk4\Ui\Lister; use Atk4\Ui\Message; use Atk4\Ui\View; @@ -20,18 +21,25 @@ // Inline Edit -$model = new Country($app->db); -$model = $model->loadAny(); +$entity = (new Country($app->db)) + ->setOrder(Country::hinting()->fieldName()->id) + ->loadAny(); $subHeader = 'Try me. I will restore value on "Escape" or save it on "Enter" or when field get blur after it has been changed.'; Header::addTo($app, ['Inline editing.', 'size' => 3, 'subHeader' => $subHeader]); -$inline_edit = VueComponent\InlineEdit::addTo($app); -$inline_edit->fieldName = $model->fieldName()->name; -$inline_edit->setModel($model); +View::addTo($app)->set('with autoSave'); +$inlineEditWithAutoSave = VueComponent\InlineEdit::addTo($app, ['autoSave' => true]); +$inlineEditWithAutoSave->fieldName = $entity->fieldName()->name; +$inlineEditWithAutoSave->setModel($entity); -$inline_edit->onChange(function (string $value) { +View::addTo($app)->set('with onChange callback'); +$inlineEditWithCallback = VueComponent\InlineEdit::addTo($app); +$inlineEditWithCallback->fieldName = $entity->fieldName()->name; +$inlineEditWithCallback->setModel($entity); +$inlineEditWithCallback->onChange(function (string $value) use ($app) { $view = new Message(); + $view->setApp($app); $view->invokeInit(); $view->text->addParagraph('new value: ' . $value); @@ -47,13 +55,13 @@ $model = new Country($app->db); -$lister_template = new HtmlTemplate('
{List}
{$atk_fp_country__name}
{$end}{/}
'); +$listerTemplate = new HtmlTemplate('
{List}
{$atk_fp_country__name}
{$end}{/}
'); $view = View::addTo($app); -$search = VueComponent\ItemSearch::addTo($view, ['ui' => 'ui compact segment']); -$lister_container = View::addTo($view, ['template' => $lister_template]); -$lister = Lister::addTo($lister_container, [], ['List']); +$search = VueComponent\ItemSearch::addTo($view, ['ui' => 'compact segment']); +$listerContainer = View::addTo($view, ['template' => $listerTemplate]); +$lister = Lister::addTo($listerContainer, [], ['List']); $lister->onHook(Lister::HOOK_BEFORE_ROW, function (Lister $lister) { $row = Country::assertInstanceOf($lister->currentRow); $row->iso = mb_strtolower($row->iso); @@ -64,7 +72,7 @@ } }); -$search->reload = $lister_container; +$search->reload = $listerContainer; $search->setModelCondition($model); $model->setLimit(50); $lister->setModel($model); @@ -75,79 +83,99 @@ Header::addTo($app, ['External Component', 'subHeader' => 'Creating component using an external component definition.']); -// same as $app->requireJs('https://unpkg.com/vue-clock2@1.1.5/dist/vue-clock.min.js'); -// for Behat testing without internet access -$app->requireJs('data:application/javascript;base64,IWZ1bmN0aW9uKHQsZSl7Im9iamVjdCI9PXR5cGVvZiBleHBvcnRzJiYib2JqZWN0Ij09dHlwZW9mIG1vZHVsZT9tb2R1bGUuZXhwb3J0cz1lKCk6ImZ1bmN0aW9uIj09dHlwZW9mIGRlZmluZSYmZGVmaW5lLmFtZD9kZWZpbmUoIkNsb2NrIixbXSxlKToib2JqZWN0Ij09dHlwZW9mIGV4cG9ydHM/ZXhwb3J0cy5DbG9jaz1lKCk6dC5DbG9jaz1lKCl9KHRoaXMsZnVuY3Rpb24oKXtyZXR1cm4gZnVuY3Rpb24odCl7ZnVuY3Rpb24gZShyKXtpZihvW3JdKXJldHVybiBvW3JdLmV4cG9ydHM7dmFyIG49b1tyXT17aTpyLGw6ITEsZXhwb3J0czp7fX07cmV0dXJuIHRbcl0uY2FsbChuLmV4cG9ydHMsbixuLmV4cG9ydHMsZSksbi5sPSEwLG4uZXhwb3J0c312YXIgbz17fTtyZXR1cm4gZS5tPXQsZS5jPW8sZS5pPWZ1bmN0aW9uKHQpe3JldHVybiB0fSxlLmQ9ZnVuY3Rpb24odCxvLHIpe2Uubyh0LG8pfHxPYmplY3QuZGVmaW5lUHJvcGVydHkodCxvLHtjb25maWd1cmFibGU6ITEsZW51bWVyYWJsZTohMCxnZXQ6cn0pfSxlLm49ZnVuY3Rpb24odCl7dmFyIG89dCYmdC5fX2VzTW9kdWxlP2Z1bmN0aW9uKCl7cmV0dXJuIHQuZGVmYXVsdH06ZnVuY3Rpb24oKXtyZXR1cm4gdH07cmV0dXJuIGUuZChvLCJhIixvKSxvfSxlLm89ZnVuY3Rpb24odCxlKXtyZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHQsZSl9LGUucD0iL2Rpc3QvIixlKGUucz0yKX0oW2Z1bmN0aW9uKHQsZSxvKXtvKDUpO3ZhciByPW8oOCkobygxKSxvKDkpLCJkYXRhLXYtN2UzZjcxMjYiLG51bGwpO3QuZXhwb3J0cz1yLmV4cG9ydHN9LGZ1bmN0aW9uKHQsZSxvKXsidXNlIHN0cmljdCI7T2JqZWN0LmRlZmluZVByb3BlcnR5KGUsIl9fZXNNb2R1bGUiLHt2YWx1ZTohMH0pLGUuZGVmYXVsdD17ZGF0YTpmdW5jdGlvbigpe3JldHVybnt0aW1lTGlzdDpbMTIsMSwyLDMsNCw1LDYsNyw4LDksMTAsMTFdLHRyYW5zZm9ybToic2NhbGUoMSkiLGhvdXJSb3RhdGU6InJvdGF0ZXooMGRlZykiLG1pbnV0ZVJvdGF0ZToicm90YXRleigwZGVnKSIsc2Vjb25kUm90YXRlOiJyb3RhdGV6KDBkZWcpIn19LHByb3BzOlsidGltZSIsImNvbG9yIiwiYm9yZGVyIiwiYmciLCJzaXplIl0sY29tcHV0ZWQ6e2Nsb2NrU3R5bGU6ZnVuY3Rpb24oKXtyZXR1cm57aGVpZ2h0OnRoaXMuc2l6ZSx3aWR0aDp0aGlzLnNpemUsY29sb3I6dGhpcy5jb2xvcixib3JkZXI6dGhpcy5ib3JkZXIsYmFja2dyb3VuZDp0aGlzLmJnfX19LHdhdGNoOnt0aW1lOmZ1bmN0aW9uKCl7dGhpcy5zaG93KCl9fSxtZXRob2RzOntzaG93OmZ1bmN0aW9uKCl7dmFyIHQ9dGhpczt0aGlzLnNob3dUaW1lKCksdGhpcy5fdGltZXImJmNsZWFySW50ZXJ2YWwodGhpcy5fdGltZXIpLHRoaXMudGltZXx8KHRoaXMuX3RpbWVyPXNldEludGVydmFsKGZ1bmN0aW9uKCl7dC5zaG93VGltZSgpfSwxZTMpKX0sc2hvd1RpbWU6ZnVuY3Rpb24oKXt2YXIgdD12b2lkIDA7aWYodGhpcy50aW1lKXQ9dGhpcy50aW1lLnNwbGl0KCI6Iik7ZWxzZXt2YXIgZT1uZXcgRGF0ZTt0PVtlLmdldEhvdXJzKCksZS5nZXRNaW51dGVzKCksZS5nZXRTZWNvbmRzKCldfXZhciBvPSt0WzBdO289bz4xMT9vLTEyOm87dmFyIHI9K3RbMV0sbj0rdFsyXXx8MCxhPTMwKm8rNipyLzM2MCozMCxpPTYqcixzPTYqbjt0aGlzLmhvdXJSb3RhdGU9InJvdGF0ZXooIithKyJkZWcpIix0aGlzLm1pbnV0ZVJvdGF0ZT0icm90YXRleigiK2krImRlZykiLHRoaXMuc2Vjb25kUm90YXRlPSJyb3RhdGV6KCIrcysiZGVnKSJ9fSxtb3VudGVkOmZ1bmN0aW9uKCl7dmFyIHQ9dGhpcy4kZWwuY2xpZW50V2lkdGgvMTIwO3Q9dD4zPzM6dCx0aGlzLnRyYW5zZm9ybT0ic2NhbGUoIit0KyIpIix0aGlzLnNob3coKX0sZGVzdHJveWVkOmZ1bmN0aW9uKCl7dGhpcy5fdGltZXImJmNsZWFySW50ZXJ2YWwodGhpcy5fdGltZXIpfX19LGZ1bmN0aW9uKHQsZSxvKXsidXNlIHN0cmljdCI7T2JqZWN0LmRlZmluZVByb3BlcnR5KGUsIl9fZXNNb2R1bGUiLHt2YWx1ZTohMH0pO3ZhciByPW8oMCksbj1mdW5jdGlvbih0KXtyZXR1cm4gdCYmdC5fX2VzTW9kdWxlP3Q6e2RlZmF1bHQ6dH19KHIpO2UuZGVmYXVsdD1uLmRlZmF1bHQsInVuZGVmaW5lZCIhPXR5cGVvZiB3aW5kb3cmJndpbmRvdy5WdWUmJndpbmRvdy5WdWUuY29tcG9uZW50KCJjbG9jayIsbi5kZWZhdWx0KX0sZnVuY3Rpb24odCxlLG8pe2U9dC5leHBvcnRzPW8oNCkoKSxlLnB1c2goW3QuaSwnLmNsb2NrW2RhdGEtdi03ZTNmNzEyNl17cG9zaXRpb246cmVsYXRpdmU7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246bWlkZGxlO3dpZHRoOjE1MHB4O2hlaWdodDoxNTBweDtib3JkZXI6MnB4IHNvbGlkO2JvcmRlci1yYWRpdXM6MTAwJTt0ZXh0LWFsaWduOmNlbnRlcjtmb250LXNpemU6MTRweH0uY2xvY2sgLmhvdXJbZGF0YS12LTdlM2Y3MTI2XXtwb3NpdGlvbjphYnNvbHV0ZTt0b3A6MDtsZWZ0OjUwJTtkaXNwbGF5OmJsb2NrO3dpZHRoOjIwcHg7aGVpZ2h0OjUwJTttYXJnaW4tbGVmdDotMTBweDtwYWRkaW5nLXRvcDo0JTtmb250LXdlaWdodDo0MDA7dHJhbnNmb3JtLW9yaWdpbjpib3R0b207dXNlci1zZWxlY3Q6bm9uZTtib3gtc2l6aW5nOmJvcmRlci1ib3h9LmNsb2NrIC5ob3VyPnNwYW5bZGF0YS12LTdlM2Y3MTI2XXtkaXNwbGF5OmJsb2NrfS5jbG9jayAuaG91cj5zcGFuPmlbZGF0YS12LTdlM2Y3MTI2XXtkaXNwbGF5OmJsb2NrO2ZvbnQtc3R5bGU6bm9ybWFsfS5jbG9jayAuaG91cltkYXRhLXYtN2UzZjcxMjZdOm50aC1vZi10eXBlKDIpe3RyYW5zZm9ybTpyb3RhdGV6KDMwZGVnKX0uY2xvY2sgLmhvdXI6bnRoLW9mLXR5cGUoMik+c3BhbltkYXRhLXYtN2UzZjcxMjZde3RyYW5zZm9ybTpyb3RhdGV6KC0zMGRlZyl9LmNsb2NrIC5ob3VyW2RhdGEtdi03ZTNmNzEyNl06bnRoLW9mLXR5cGUoMyl7dHJhbnNmb3JtOnJvdGF0ZXooNjBkZWcpfS5jbG9jayAuaG91cjpudGgtb2YtdHlwZSgzKT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTYwZGVnKX0uY2xvY2sgLmhvdXJbZGF0YS12LTdlM2Y3MTI2XTpudGgtb2YtdHlwZSg0KXt0cmFuc2Zvcm06cm90YXRleig5MGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDQpPnNwYW5bZGF0YS12LTdlM2Y3MTI2XXt0cmFuc2Zvcm06cm90YXRleigtOTBkZWcpfS5jbG9jayAuaG91cltkYXRhLXYtN2UzZjcxMjZdOm50aC1vZi10eXBlKDUpe3RyYW5zZm9ybTpyb3RhdGV6KDEyMGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDUpPnNwYW5bZGF0YS12LTdlM2Y3MTI2XXt0cmFuc2Zvcm06cm90YXRleigtMTIwZGVnKX0uY2xvY2sgLmhvdXJbZGF0YS12LTdlM2Y3MTI2XTpudGgtb2YtdHlwZSg2KXt0cmFuc2Zvcm06cm90YXRleigxNTBkZWcpfS5jbG9jayAuaG91cjpudGgtb2YtdHlwZSg2KT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTE1MGRlZyl9LmNsb2NrIC5ob3VyW2RhdGEtdi03ZTNmNzEyNl06bnRoLW9mLXR5cGUoNyl7dHJhbnNmb3JtOnJvdGF0ZXooMTgwZGVnKX0uY2xvY2sgLmhvdXI6bnRoLW9mLXR5cGUoNyk+c3BhbltkYXRhLXYtN2UzZjcxMjZde3RyYW5zZm9ybTpyb3RhdGV6KC0xODBkZWcpfS5jbG9jayAuaG91cltkYXRhLXYtN2UzZjcxMjZdOm50aC1vZi10eXBlKDgpe3RyYW5zZm9ybTpyb3RhdGV6KDIxMGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDgpPnNwYW5bZGF0YS12LTdlM2Y3MTI2XXt0cmFuc2Zvcm06cm90YXRleigtMjEwZGVnKX0uY2xvY2sgLmhvdXJbZGF0YS12LTdlM2Y3MTI2XTpudGgtb2YtdHlwZSg5KXt0cmFuc2Zvcm06cm90YXRleigyNDBkZWcpfS5jbG9jayAuaG91cjpudGgtb2YtdHlwZSg5KT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTI0MGRlZyl9LmNsb2NrIC5ob3VyW2RhdGEtdi03ZTNmNzEyNl06bnRoLW9mLXR5cGUoMTApe3RyYW5zZm9ybTpyb3RhdGV6KDI3MGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDEwKT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTI3MGRlZyl9LmNsb2NrIC5ob3VyW2RhdGEtdi03ZTNmNzEyNl06bnRoLW9mLXR5cGUoMTEpe3RyYW5zZm9ybTpyb3RhdGV6KDMwMGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDExKT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTMwMGRlZyl9LmNsb2NrIC5ob3VyW2RhdGEtdi03ZTNmNzEyNl06bnRoLW9mLXR5cGUoMTIpe3RyYW5zZm9ybTpyb3RhdGV6KDMzMGRlZyl9LmNsb2NrIC5ob3VyOm50aC1vZi10eXBlKDEyKT5zcGFuW2RhdGEtdi03ZTNmNzEyNl17dHJhbnNmb3JtOnJvdGF0ZXooLTMzMGRlZyl9LmNsb2NrIC5jbG9jay1jaXJjbGVbZGF0YS12LTdlM2Y3MTI2XXtwb3NpdGlvbjphYnNvbHV0ZTt0b3A6NTAlO2xlZnQ6NTAlO3dpZHRoOjE2cHg7aGVpZ2h0OjE2cHg7dHJhbnNmb3JtOnRyYW5zbGF0ZSgtNTAlLC01MCUpO2JvcmRlcjoycHggc29saWQgIzY2Njtib3JkZXItcmFkaXVzOjEwMCU7YmFja2dyb3VuZC1jb2xvcjojZmZmO3otaW5kZXg6MTtib3gtc2l6aW5nOmJvcmRlci1ib3h9LmNsb2NrIC5jbG9jay1jaXJjbGVbZGF0YS12LTdlM2Y3MTI2XTpiZWZvcmV7cG9zaXRpb246YWJzb2x1dGU7dG9wOjUwJTtsZWZ0OjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKC01MCUsLTUwJSk7ZGlzcGxheTpibG9jaztjb250ZW50OiIiO3dpZHRoOjRweDtoZWlnaHQ6NHB4O2JvcmRlci1yYWRpdXM6MTAwJTtiYWNrZ3JvdW5kLWNvbG9yOiM2NjZ9LmNsb2NrIC5jbG9jay1ob3VyW2RhdGEtdi03ZTNmNzEyNl0sLmNsb2NrIC5jbG9jay1taW51dGVbZGF0YS12LTdlM2Y3MTI2XSwuY2xvY2sgLmNsb2NrLXNlY29uZFtkYXRhLXYtN2UzZjcxMjZde3Bvc2l0aW9uOmFic29sdXRlO3RvcDoxNSU7bGVmdDo1MCU7ZGlzcGxheTpibG9jazt3aWR0aDoycHg7aGVpZ2h0OjM1JTttYXJnaW4tbGVmdDotMXB4O2JvcmRlci1yYWRpdXM6NXB4O3RyYW5zZm9ybS1vcmlnaW46Ym90dG9tO2JhY2tncm91bmQtY29sb3I6IzY2Nn0uY2xvY2sgLmNsb2NrLWhvdXJbZGF0YS12LTdlM2Y3MTI2XXt0b3A6MzAlO3dpZHRoOjRweDtoZWlnaHQ6MjAlO21hcmdpbi1sZWZ0Oi0ycHh9LmNsb2NrIC5jbG9jay1zZWNvbmRbZGF0YS12LTdlM2Y3MTI2XXt3aWR0aDoxcHh9LmNsb2NrLmlzLXNtYWxsW2RhdGEtdi03ZTNmNzEyNl17d2lkdGg6ODBweDtoZWlnaHQ6ODBweDtib3JkZXItd2lkdGg6MXB4O2ZvbnQtc2l6ZToxMnB4fS5jbG9jay5pcy1zbWFsbCAuY2xvY2stY2lyY2xlW2RhdGEtdi03ZTNmNzEyNl17d2lkdGg6MTBweDtoZWlnaHQ6MTBweDtib3JkZXItd2lkdGg6MXB4fS5jbG9jay5pcy1zbWFsbCAuY2xvY2stY2lyY2xlW2RhdGEtdi03ZTNmNzEyNl06YmVmb3Jle3dpZHRoOjJweDtoZWlnaHQ6MnB4fScsIiJdKX0sZnVuY3Rpb24odCxlKXt0LmV4cG9ydHM9ZnVuY3Rpb24oKXt2YXIgdD1bXTtyZXR1cm4gdC50b1N0cmluZz1mdW5jdGlvbigpe2Zvcih2YXIgdD1bXSxlPTA7ZTx0aGlzLmxlbmd0aDtlKyspe3ZhciBvPXRoaXNbZV07b1syXT90LnB1c2goIkBtZWRpYSAiK29bMl0rInsiK29bMV0rIn0iKTp0LnB1c2gob1sxXSl9cmV0dXJuIHQuam9pbigiIil9LHQuaT1mdW5jdGlvbihlLG8peyJzdHJpbmciPT10eXBlb2YgZSYmKGU9W1tudWxsLGUsIiJdXSk7Zm9yKHZhciByPXt9LG49MDtuPHRoaXMubGVuZ3RoO24rKyl7dmFyIGE9dGhpc1tuXVswXTsibnVtYmVyIj09dHlwZW9mIGEmJihyW2FdPSEwKX1mb3Iobj0wO248ZS5sZW5ndGg7bisrKXt2YXIgaT1lW25dOyJudW1iZXIiPT10eXBlb2YgaVswXSYmcltpWzBdXXx8KG8mJiFpWzJdP2lbMl09bzpvJiYoaVsyXT0iKCIraVsyXSsiKSBhbmQgKCIrbysiKSIpLHQucHVzaChpKSl9fSx0fX0sZnVuY3Rpb24odCxlLG8pe3ZhciByPW8oMyk7InN0cmluZyI9PXR5cGVvZiByJiYocj1bW3QuaSxyLCIiXV0pO3ZhciBuPXtobXI6ITB9O24udHJhbnNmb3JtPXZvaWQgMCxuLmluc2VydEludG89dm9pZCAwO28oNikocixuKTtyLmxvY2FscyYmKHQuZXhwb3J0cz1yLmxvY2Fscyl9LGZ1bmN0aW9uKHQsZSxvKXtmdW5jdGlvbiByKHQsZSl7Zm9yKHZhciBvPTA7bzx0Lmxlbmd0aDtvKyspe3ZhciByPXRbb10sbj12W3IuaWRdO2lmKG4pe24ucmVmcysrO2Zvcih2YXIgYT0wO2E8bi5wYXJ0cy5sZW5ndGg7YSsrKW4ucGFydHNbYV0oci5wYXJ0c1thXSk7Zm9yKDthPHIucGFydHMubGVuZ3RoO2ErKyluLnBhcnRzLnB1c2godShyLnBhcnRzW2FdLGUpKX1lbHNle2Zvcih2YXIgaT1bXSxhPTA7YTxyLnBhcnRzLmxlbmd0aDthKyspaS5wdXNoKHUoci5wYXJ0c1thXSxlKSk7dltyLmlkXT17aWQ6ci5pZCxyZWZzOjEscGFydHM6aX19fX1mdW5jdGlvbiBuKHQsZSl7Zm9yKHZhciBvPVtdLHI9e30sbj0wO248dC5sZW5ndGg7bisrKXt2YXIgYT10W25dLGk9ZS5iYXNlP2FbMF0rZS5iYXNlOmFbMF0scz1hWzFdLGM9YVsyXSxmPWFbM10sbD17Y3NzOnMsbWVkaWE6Yyxzb3VyY2VNYXA6Zn07cltpXT9yW2ldLnBhcnRzLnB1c2gobCk6by5wdXNoKHJbaV09e2lkOmkscGFydHM6W2xdfSl9cmV0dXJuIG99ZnVuY3Rpb24gYSh0LGUpe3ZhciBvPXkodC5pbnNlcnRJbnRvKTtpZighbyl0aHJvdyBuZXcgRXJyb3IoIkNvdWxkbid0IGZpbmQgYSBzdHlsZSB0YXJnZXQuIFRoaXMgcHJvYmFibHkgbWVhbnMgdGhhdCB0aGUgdmFsdWUgZm9yIHRoZSAnaW5zZXJ0SW50bycgcGFyYW1ldGVyIGlzIGludmFsaWQuIik7dmFyIHI9eFt4Lmxlbmd0aC0xXTtpZigidG9wIj09PXQuaW5zZXJ0QXQpcj9yLm5leHRTaWJsaW5nP28uaW5zZXJ0QmVmb3JlKGUsci5uZXh0U2libGluZyk6by5hcHBlbmRDaGlsZChlKTpvLmluc2VydEJlZm9yZShlLG8uZmlyc3RDaGlsZCkseC5wdXNoKGUpO2Vsc2UgaWYoImJvdHRvbSI9PT10Lmluc2VydEF0KW8uYXBwZW5kQ2hpbGQoZSk7ZWxzZXtpZigib2JqZWN0IiE9dHlwZW9mIHQuaW5zZXJ0QXR8fCF0Lmluc2VydEF0LmJlZm9yZSl0aHJvdyBuZXcgRXJyb3IoIltTdHlsZSBMb2FkZXJdXG5cbiBJbnZhbGlkIHZhbHVlIGZvciBwYXJhbWV0ZXIgJ2luc2VydEF0JyAoJ29wdGlvbnMuaW5zZXJ0QXQnKSBmb3VuZC5cbiBNdXN0IGJlICd0b3AnLCAnYm90dG9tJywgb3IgT2JqZWN0LlxuIChodHRwczovL2dpdGh1Yi5jb20vd2VicGFjay1jb250cmliL3N0eWxlLWxvYWRlciNpbnNlcnRhdClcbiIpO3ZhciBuPXkodC5pbnNlcnRBdC5iZWZvcmUsbyk7by5pbnNlcnRCZWZvcmUoZSxuKX19ZnVuY3Rpb24gaSh0KXtpZihudWxsPT09dC5wYXJlbnROb2RlKXJldHVybiExO3QucGFyZW50Tm9kZS5yZW1vdmVDaGlsZCh0KTt2YXIgZT14LmluZGV4T2YodCk7ZT49MCYmeC5zcGxpY2UoZSwxKX1mdW5jdGlvbiBzKHQpe3ZhciBlPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInN0eWxlIik7aWYodm9pZCAwPT09dC5hdHRycy50eXBlJiYodC5hdHRycy50eXBlPSJ0ZXh0L2NzcyIpLHZvaWQgMD09PXQuYXR0cnMubm9uY2Upe3ZhciBvPWwoKTtvJiYodC5hdHRycy5ub25jZT1vKX1yZXR1cm4gZihlLHQuYXR0cnMpLGEodCxlKSxlfWZ1bmN0aW9uIGModCl7dmFyIGU9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgibGluayIpO3JldHVybiB2b2lkIDA9PT10LmF0dHJzLnR5cGUmJih0LmF0dHJzLnR5cGU9InRleHQvY3NzIiksdC5hdHRycy5yZWw9InN0eWxlc2hlZXQiLGYoZSx0LmF0dHJzKSxhKHQsZSksZX1mdW5jdGlvbiBmKHQsZSl7T2JqZWN0LmtleXMoZSkuZm9yRWFjaChmdW5jdGlvbihvKXt0LnNldEF0dHJpYnV0ZShvLGVbb10pfSl9ZnVuY3Rpb24gbCgpe3JldHVybiBvLm5jfWZ1bmN0aW9uIHUodCxlKXt2YXIgbyxyLG4sYTtpZihlLnRyYW5zZm9ybSYmdC5jc3Mpe2lmKCEoYT0iZnVuY3Rpb24iPT10eXBlb2YgZS50cmFuc2Zvcm0/ZS50cmFuc2Zvcm0odC5jc3MpOmUudHJhbnNmb3JtLmRlZmF1bHQodC5jc3MpKSlyZXR1cm4gZnVuY3Rpb24oKXt9O3QuY3NzPWF9aWYoZS5zaW5nbGV0b24pe3ZhciBmPWsrKztvPWd8fChnPXMoZSkpLHI9ZC5iaW5kKG51bGwsbyxmLCExKSxuPWQuYmluZChudWxsLG8sZiwhMCl9ZWxzZSB0LnNvdXJjZU1hcCYmImZ1bmN0aW9uIj09dHlwZW9mIFVSTCYmImZ1bmN0aW9uIj09dHlwZW9mIFVSTC5jcmVhdGVPYmplY3RVUkwmJiJmdW5jdGlvbiI9PXR5cGVvZiBVUkwucmV2b2tlT2JqZWN0VVJMJiYiZnVuY3Rpb24iPT10eXBlb2YgQmxvYiYmImZ1bmN0aW9uIj09dHlwZW9mIGJ0b2E/KG89YyhlKSxyPWguYmluZChudWxsLG8sZSksbj1mdW5jdGlvbigpe2kobyksby5ocmVmJiZVUkwucmV2b2tlT2JqZWN0VVJMKG8uaHJlZil9KToobz1zKGUpLHI9cC5iaW5kKG51bGwsbyksbj1mdW5jdGlvbigpe2kobyl9KTtyZXR1cm4gcih0KSxmdW5jdGlvbihlKXtpZihlKXtpZihlLmNzcz09PXQuY3NzJiZlLm1lZGlhPT09dC5tZWRpYSYmZS5zb3VyY2VNYXA9PT10LnNvdXJjZU1hcClyZXR1cm47cih0PWUpfWVsc2UgbigpfX1mdW5jdGlvbiBkKHQsZSxvLHIpe3ZhciBuPW8/IiI6ci5jc3M7aWYodC5zdHlsZVNoZWV0KXQuc3R5bGVTaGVldC5jc3NUZXh0PXooZSxuKTtlbHNle3ZhciBhPWRvY3VtZW50LmNyZWF0ZVRleHROb2RlKG4pLGk9dC5jaGlsZE5vZGVzO2lbZV0mJnQucmVtb3ZlQ2hpbGQoaVtlXSksaS5sZW5ndGg/dC5pbnNlcnRCZWZvcmUoYSxpW2VdKTp0LmFwcGVuZENoaWxkKGEpfX1mdW5jdGlvbiBwKHQsZSl7dmFyIG89ZS5jc3Mscj1lLm1lZGlhO2lmKHImJnQuc2V0QXR0cmlidXRlKCJtZWRpYSIsciksdC5zdHlsZVNoZWV0KXQuc3R5bGVTaGVldC5jc3NUZXh0PW87ZWxzZXtmb3IoO3QuZmlyc3RDaGlsZDspdC5yZW1vdmVDaGlsZCh0LmZpcnN0Q2hpbGQpO3QuYXBwZW5kQ2hpbGQoZG9jdW1lbnQuY3JlYXRlVGV4dE5vZGUobykpfX1mdW5jdGlvbiBoKHQsZSxvKXt2YXIgcj1vLmNzcyxuPW8uc291cmNlTWFwLGE9dm9pZCAwPT09ZS5jb252ZXJ0VG9BYnNvbHV0ZVVybHMmJm47KGUuY29udmVydFRvQWJzb2x1dGVVcmxzfHxhKSYmKHI9dyhyKSksbiYmKHIrPSJcbi8qIyBzb3VyY2VNYXBwaW5nVVJMPWRhdGE6YXBwbGljYXRpb24vanNvbjtiYXNlNjQsIitidG9hKHVuZXNjYXBlKGVuY29kZVVSSUNvbXBvbmVudChKU09OLnN0cmluZ2lmeShuKSkpKSsiICovIik7dmFyIGk9bmV3IEJsb2IoW3JdLHt0eXBlOiJ0ZXh0L2NzcyJ9KSxzPXQuaHJlZjt0LmhyZWY9VVJMLmNyZWF0ZU9iamVjdFVSTChpKSxzJiZVUkwucmV2b2tlT2JqZWN0VVJMKHMpfXZhciB2PXt9LG09ZnVuY3Rpb24odCl7dmFyIGU7cmV0dXJuIGZ1bmN0aW9uKCl7cmV0dXJuIHZvaWQgMD09PWUmJihlPXQuYXBwbHkodGhpcyxhcmd1bWVudHMpKSxlfX0oZnVuY3Rpb24oKXtyZXR1cm4gd2luZG93JiZkb2N1bWVudCYmZG9jdW1lbnQuYWxsJiYhd2luZG93LmF0b2J9KSxiPWZ1bmN0aW9uKHQsZSl7cmV0dXJuIGU/ZS5xdWVyeVNlbGVjdG9yKHQpOmRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IodCl9LHk9ZnVuY3Rpb24odCl7dmFyIGU9e307cmV0dXJuIGZ1bmN0aW9uKHQsbyl7aWYoImZ1bmN0aW9uIj09dHlwZW9mIHQpcmV0dXJuIHQoKTtpZih2b2lkIDA9PT1lW3RdKXt2YXIgcj1iLmNhbGwodGhpcyx0LG8pO2lmKHdpbmRvdy5IVE1MSUZyYW1lRWxlbWVudCYmciBpbnN0YW5jZW9mIHdpbmRvdy5IVE1MSUZyYW1lRWxlbWVudCl0cnl7cj1yLmNvbnRlbnREb2N1bWVudC5oZWFkfWNhdGNoKHQpe3I9bnVsbH1lW3RdPXJ9cmV0dXJuIGVbdF19fSgpLGc9bnVsbCxrPTAseD1bXSx3PW8oNyk7dC5leHBvcnRzPWZ1bmN0aW9uKHQsZSl7aWYoInVuZGVmaW5lZCIhPXR5cGVvZiBERUJVRyYmREVCVUcmJiJvYmplY3QiIT10eXBlb2YgZG9jdW1lbnQpdGhyb3cgbmV3IEVycm9yKCJUaGUgc3R5bGUtbG9hZGVyIGNhbm5vdCBiZSB1c2VkIGluIGEgbm9uLWJyb3dzZXIgZW52aXJvbm1lbnQiKTtlPWV8fHt9LGUuYXR0cnM9Im9iamVjdCI9PXR5cGVvZiBlLmF0dHJzP2UuYXR0cnM6e30sZS5zaW5nbGV0b258fCJib29sZWFuIj09dHlwZW9mIGUuc2luZ2xldG9ufHwoZS5zaW5nbGV0b249bSgpKSxlLmluc2VydEludG98fChlLmluc2VydEludG89ImhlYWQiKSxlLmluc2VydEF0fHwoZS5pbnNlcnRBdD0iYm90dG9tIik7dmFyIG89bih0LGUpO3JldHVybiByKG8sZSksZnVuY3Rpb24odCl7Zm9yKHZhciBhPVtdLGk9MDtpPG8ubGVuZ3RoO2krKyl7dmFyIHM9b1tpXSxjPXZbcy5pZF07Yy5yZWZzLS0sYS5wdXNoKGMpfWlmKHQpe3Iobih0LGUpLGUpfWZvcih2YXIgaT0wO2k8YS5sZW5ndGg7aSsrKXt2YXIgYz1hW2ldO2lmKDA9PT1jLnJlZnMpe2Zvcih2YXIgZj0wO2Y8Yy5wYXJ0cy5sZW5ndGg7ZisrKWMucGFydHNbZl0oKTtkZWxldGUgdltjLmlkXX19fX07dmFyIHo9ZnVuY3Rpb24oKXt2YXIgdD1bXTtyZXR1cm4gZnVuY3Rpb24oZSxvKXtyZXR1cm4gdFtlXT1vLHQuZmlsdGVyKEJvb2xlYW4pLmpvaW4oIlxuIil9fSgpfSxmdW5jdGlvbih0LGUpe3QuZXhwb3J0cz1mdW5jdGlvbih0KXt2YXIgZT0idW5kZWZpbmVkIiE9dHlwZW9mIHdpbmRvdyYmd2luZG93LmxvY2F0aW9uO2lmKCFlKXRocm93IG5ldyBFcnJvcigiZml4VXJscyByZXF1aXJlcyB3aW5kb3cubG9jYXRpb24iKTtpZighdHx8InN0cmluZyIhPXR5cGVvZiB0KXJldHVybiB0O3ZhciBvPWUucHJvdG9jb2wrIi8vIitlLmhvc3Qscj1vK2UucGF0aG5hbWUucmVwbGFjZSgvXC9bXlwvXSokLywiLyIpO3JldHVybiB0LnJlcGxhY2UoL3VybFxzKlwoKCg/OlteKShdfFwoKD86W14pKF0rfFwoW14pKF0qXCkpKlwpKSopXCkvZ2ksZnVuY3Rpb24odCxlKXt2YXIgbj1lLnRyaW0oKS5yZXBsYWNlKC9eIiguKikiJC8sZnVuY3Rpb24odCxlKXtyZXR1cm4gZX0pLnJlcGxhY2UoL14nKC4qKSckLyxmdW5jdGlvbih0LGUpe3JldHVybiBlfSk7aWYoL14oI3xkYXRhOnxodHRwOlwvXC98aHR0cHM6XC9cL3xmaWxlOlwvXC9cL3xccyokKS9pLnRlc3QobikpcmV0dXJuIHQ7dmFyIGE7cmV0dXJuIGE9MD09PW4uaW5kZXhPZigiLy8iKT9uOjA9PT1uLmluZGV4T2YoIi8iKT9vK246cituLnJlcGxhY2UoL15cLlwvLywiIiksInVybCgiK0pTT04uc3RyaW5naWZ5KGEpKyIpIn0pfX0sZnVuY3Rpb24odCxlKXt0LmV4cG9ydHM9ZnVuY3Rpb24odCxlLG8scil7dmFyIG4sYT10PXR8fHt9LGk9dHlwZW9mIHQuZGVmYXVsdDsib2JqZWN0IiE9PWkmJiJmdW5jdGlvbiIhPT1pfHwobj10LGE9dC5kZWZhdWx0KTt2YXIgcz0iZnVuY3Rpb24iPT10eXBlb2YgYT9hLm9wdGlvbnM6YTtpZihlJiYocy5yZW5kZXI9ZS5yZW5kZXIscy5zdGF0aWNSZW5kZXJGbnM9ZS5zdGF0aWNSZW5kZXJGbnMpLG8mJihzLl9zY29wZUlkPW8pLHIpe3ZhciBjPU9iamVjdC5jcmVhdGUocy5jb21wdXRlZHx8bnVsbCk7T2JqZWN0LmtleXMocikuZm9yRWFjaChmdW5jdGlvbih0KXt2YXIgZT1yW3RdO2NbdF09ZnVuY3Rpb24oKXtyZXR1cm4gZX19KSxzLmNvbXB1dGVkPWN9cmV0dXJue2VzTW9kdWxlOm4sZXhwb3J0czphLG9wdGlvbnM6c319fSxmdW5jdGlvbih0LGUpe3QuZXhwb3J0cz17cmVuZGVyOmZ1bmN0aW9uKCl7dmFyIHQ9dGhpcyxlPXQuJGNyZWF0ZUVsZW1lbnQsbz10Ll9zZWxmLl9jfHxlO3JldHVybiBvKCJkaXYiLHtzdGF0aWNDbGFzczoiY2xvY2siLHN0eWxlOnQuY2xvY2tTdHlsZX0sW28oImRpdiIse3N0YXRpY0NsYXNzOiJjbG9jay1jaXJjbGUifSksdC5fdigiICIpLG8oImRpdiIse3N0YXRpY0NsYXNzOiJjbG9jay1ob3VyIixzdHlsZTp7dHJhbnNmb3JtOnQuaG91clJvdGF0ZX19KSx0Ll92KCIgIiksbygiZGl2Iix7c3RhdGljQ2xhc3M6ImNsb2NrLW1pbnV0ZSIsc3R5bGU6e3RyYW5zZm9ybTp0Lm1pbnV0ZVJvdGF0ZX19KSx0Ll92KCIgIiksbygiZGl2Iix7c3RhdGljQ2xhc3M6ImNsb2NrLXNlY29uZCIsc3R5bGU6e3RyYW5zZm9ybTp0LnNlY29uZFJvdGF0ZX19KSx0Ll92KCIgIiksdC5fbCh0LnRpbWVMaXN0LGZ1bmN0aW9uKGUpe3JldHVybiBvKCJiIix7a2V5OmUsc3RhdGljQ2xhc3M6ImhvdXIifSxbbygic3BhbiIsW28oImkiLHtzdHlsZTp7dHJhbnNmb3JtOnQudHJhbnNmb3JtfX0sW3QuX3YodC5fcyhlKSldKV0pXSl9KV0sMil9LHN0YXRpY1JlbmRlckZuczpbXX19XSl9KTsKLy8jIHNvdXJjZU1hcHBpbmdVUkw9dnVlLWNsb2NrLm1pbi5qcy5tYXA='); +$app->html->template->dangerouslyAppendHtml('Head', $app->getTag('script', [], <<<'EOF' + window.vueDemoClock = { + template: '
{{ time }}
', + props: ['color', 'textShadow', 'background'], + data: function () { + return { + time: '-', + }; + }, + mounted: function () { + this.interval = setInterval(this.updateClock, 100); + }, + beforeUnmount: function () { + clearInterval(this.interval); + }, + methods: { + updateClock: function () { + const date = new Date(); + this.time = date.getHours().toString().padStart(2, '0') + + ':' + date.getMinutes().toString().padStart(2, '0') + + ':' + date.getSeconds().toString().padStart(2, '0'); + }, + }, + }; + EOF)); // Injecting template but normally you would create a template file. -$clock_template = new HtmlTemplate(<<<'EOF' -
- -
- -
Change Style
-
-
-
{$script} +$clockTemplate = new HtmlTemplate(<<<'EOF' +
+ +
+ {$script} EOF); -// Injecting script but normally you would create a separate js file and include it in your page. -// This is the vue component definition. It is also using another external vue component 'vue-clock2' -$clock_script = <<<'EOF' - - EOF; - -// Creating the clock view and injecting js. -$clock = View::addTo($app, ['template' => $clock_template]); -$clock->template->tryDangerouslySetHtml('script', $clock_script); + }, + name: 'my-clock', + methods: { + onChangeStyle: function () { + this.currentIndex++; + if (this.currentIndex >= this.style.length) { + this.currentIndex = 0; + } + }, + }, + }; + EOF); + +// Creating the clock view and injecting JS. +$clock = View::addTo($app, ['template' => $clockTemplate]); +$clock->template->dangerouslySetHtml('script', $clockScript); // passing some style to my-clock component. -$clock_style = [ - ['color' => '#4AB7BD', 'border' => '', 'bg' => 'none'], - ['color' => '#FFFFFF', 'border' => 'none', 'bg' => '#E0DCFF'], - ['color' => '', 'border' => 'none', 'bg' => 'radial-gradient(circle, #ecffe5, #fffbe1, #38ff91)'], +$clockStyle = [ + ['color' => 'maroon', 'background' => '', 'textShadow' => '5px 5px 10px teal'], + ['color' => 'white', 'background' => '', 'textShadow' => '0px 0px 10px blue'], + ['color' => '', 'background' => 'radial-gradient(ellipse at center, rgba(0, 255, 0, 0.25) 0%,rgba(0, 255, 0, 0) 50%)', 'textShadow' => ''], ]; // creating vue using an external definition. -$clock->vue('my-clock', ['clock' => $clock_style], 'myClock'); +$clock->vue('my-clock', ['styles' => $clockStyle], new JsExpression('myClock')); -$btn = Button::addTo($app, ['Change Style']); -$btn->on('click', $clock->jsEmitEvent($clock->name . '-clock-change-style')); +$button = Button::addTo($app, ['Change Style']); +$button->on('click', $clock->jsEmitEvent($clock->name . '-clock-change-style')); View::addTo($app, ['element' => 'p', 'I am not part of the component but I can still change style using the eventBus.']); diff --git a/demos/layout/layout-panel.php b/demos/layout/layout-panel.php index b266563f2a..481f2e5a7c 100644 --- a/demos/layout/layout-panel.php +++ b/demos/layout/layout-panel.php @@ -9,8 +9,9 @@ use Atk4\Ui\Form; use Atk4\Ui\Header; use Atk4\Ui\Icon; -use Atk4\Ui\JsReload; -use Atk4\Ui\JsToast; +use Atk4\Ui\Js\JsBlock; +use Atk4\Ui\Js\JsReload; +use Atk4\Ui\Js\JsToast; use Atk4\Ui\Message; use Atk4\Ui\Panel; use Atk4\Ui\Text; @@ -29,8 +30,8 @@ Header::addTo($app, ['Static', 'size' => 4, 'subHeader' => 'Panel may have static content only.']); $panel = Panel\Right::addTo($app, ['dynamic' => []]); Message::addTo($panel, ['This panel contains only static content.']); -$btn = Button::addTo($app, ['Open Static']); -$btn->on('click', $panel->jsOpen()); +$button = Button::addTo($app, ['Open Static']); +$button->on('click', $panel->jsOpen()); View::addTo($app, ['ui' => 'divider']); // PANEL_1 @@ -39,13 +40,13 @@ $panel1 = Panel\Right::addTo($app); Message::addTo($panel1, ['This panel will load content dynamically below according to button select on the right.']); -$btn = Button::addTo($app, ['Button 1']); -$btn->js(true)->data('btn', '1'); -$btn->on('click', $panel1->jsOpen([], ['btn'], 'orange')); +$button = Button::addTo($app, ['Button 1']); +$button->js(true)->data('btn', '1'); +$button->on('click', $panel1->jsOpen([], ['btn'], 'orange')); -$btn = Button::addTo($app, ['Button 2']); -$btn->js(true)->data('btn', '2'); -$btn->on('click', $panel1->jsOpen([], ['btn'], 'orange')); +$button = Button::addTo($app, ['Button 2']); +$button->js(true)->data('btn', '2'); +$button->on('click', $panel1->jsOpen([], ['btn'], 'orange')); $view = View::addTo($app, ['ui' => 'segment']); $text = Text::addTo($view); @@ -63,10 +64,10 @@ View::addTo($panel, ['ui' => 'divider']); $panelButton = Button::addTo($panel, ['Complete']); - $panelButton->on('click', [ + $panelButton->on('click', new JsBlock([ $p->getOwner()->jsClose(), new JsReload($view, ['txt' => 'Complete using button #' . $buttonNumber]), - ]); + ])); }); View::addTo($app, ['ui' => 'divider']); @@ -76,7 +77,7 @@ Header::addTo($app, ['Closing option', 'size' => 4, 'subHeader' => 'Panel can prevent from closing.']); $panel2 = Panel\Right::addTo($app, ['hasClickAway' => false]); -$icon = Icon::addTo($app, ['big cog'])->addStyle('cursor', 'pointer'); +$icon = Icon::addTo($app, ['big cog'])->setStyle('cursor', 'pointer'); $icon->on('click', $panel2->jsOpen()); $panel2->addConfirmation('Changes will be lost. Are you sure?'); @@ -94,11 +95,11 @@ ->onChange($p->getOwner()->jsDisplayWarning(true)); $form->onSubmit(function (Form $form) use ($p) { - return [ + return new JsBlock([ new JsToast('Saved, closing panel.'), $p->getOwner()->jsDisplayWarning(false), $p->getOwner()->jsClose(), - ]; + ]); }); }); View::addTo($app, ['ui' => 'divider']); @@ -115,7 +116,7 @@ $country->setLimit(3); foreach ($country as $ct) { - $c = Card::addTo($deck, ['useLabel' => true])->addStyle('cursor', 'pointer'); + $c = Card::addTo($deck, ['useLabel' => true])->setStyle('cursor', 'pointer'); $c->setModel($ct); $c->on('click', $panel3->jsOpen([], ['id'], 'orange')); } diff --git a/demos/layout/layouts.php b/demos/layout/layouts.php index 7956f3b3e0..dda49732ba 100644 --- a/demos/layout/layouts.php +++ b/demos/layout/layouts.php @@ -33,7 +33,6 @@ // add buttons in toolbar foreach ($buttons as $k => $args) { - Button::addTo($tb) - ->set([$args['title'], 'iconRight' => 'down arrow']) - ->js('click', $i->js()->attr('src', $app->url($args['page']))); + $button = Button::addTo($tb, [$args['title'], 'iconRight' => 'down arrow']); + $button->on('click', $i->js()->attr('src', $app->url($args['page']))); } diff --git a/demos/layout/layouts_admin.php b/demos/layout/layouts_admin.php index 2c686af223..903acebaa8 100644 --- a/demos/layout/layouts_admin.php +++ b/demos/layout/layouts_admin.php @@ -7,6 +7,7 @@ use Atk4\Data\Model; use Atk4\Ui\Form; use Atk4\Ui\Header; +use Atk4\Ui\Js\JsBlock; use Atk4\Ui\Layout; /** @var \Atk4\Ui\App $app */ @@ -57,9 +58,9 @@ $errors = []; foreach (['first_name', 'last_name', 'address'] as $field) { if (!$form->model->get($field)) { - $errors[] = $form->error($field, 'Field ' . $field . ' is mandatory'); + $errors[] = $form->jsError($field, 'Field ' . $field . ' is mandatory'); } } - return $errors !== [] ? $errors : $form->success('No more errors', 'so we have saved everything into the database'); + return $errors !== [] ? new JsBlock($errors) : $form->jsSuccess('No more errors', 'so we have saved everything into the database'); }); diff --git a/demos/layout/layouts_manual.php b/demos/layout/layouts_manual.php index 6483312077..9245ee2bbd 100644 --- a/demos/layout/layouts_manual.php +++ b/demos/layout/layouts_manual.php @@ -19,4 +19,5 @@ $app->html = null; $app->initLayout([Layout::class]); +$layout->setApp($app); Text::addTo($app->layout)->addHtml($layout->render()); diff --git a/demos/layout/templates/layout1.html b/demos/layout/templates/layout1.html index c2927d2406..7e3c1dcbbf 100644 --- a/demos/layout/templates/layout1.html +++ b/demos/layout/templates/layout1.html @@ -3,15 +3,14 @@ - - + Rainas homepage - diff --git a/js/src/components/query-builder/query-builder.component.vue b/js/src/components/query-builder/query-builder.component.vue deleted file mode 100644 index f3f1c328cf..0000000000 --- a/js/src/components/query-builder/query-builder.component.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - diff --git a/js/src/components/share/atk-date-picker.js b/js/src/components/share/atk-date-picker.js deleted file mode 100644 index 1109268abc..0000000000 --- a/js/src/components/share/atk-date-picker.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Wrapper for vue-flatpickr-component component. - * https://github.com/ankurk91/vue-flatpickr-component - * - * Props - * config: Any of flatpickr options - * Will emit a dateChange event when date is set. - */ - -const template = ''; - -export default { - name: 'atk-date-picker', - template: template, - props: ['config', 'value'], - data: function () { - const { useDefault, ...fpickr } = this.config; - - if (useDefault && !fpickr.defaultDate && !this.value) { - fpickr.defaultDate = new Date(); - } else if (this.value) { - fpickr.defaultDate = this.value; - } - - if (!fpickr.locale) { - fpickr.locale = flatpickr.l10ns.default; - } - - return { - flatPickr: fpickr, - date: null, - }; - }, - mounted: function () { - // if value is not set but default date is, then emit proper string value to parent. - if (!this.value && this.flatPickr.defaultDate) { - if (this.flatPickr.defaultDate instanceof Date) { - this.$emit('setDefault', flatpickr.formatDate(this.config.defaultDate, this.config.dateFormat)); - } else { - this.$emit('setDefault', this.flatPickr.defaultDate); - } - } - }, - methods: { - onChange: function (date) { - this.$emit('onChange', flatpickr.formatDate(date[0], this.flatPickr.dateFormat)); - }, - }, -}; diff --git a/js/src/components/share/atk-lookup.js b/js/src/components/share/atk-lookup.js deleted file mode 100644 index 07ce2e57f0..0000000000 --- a/js/src/components/share/atk-lookup.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Wrapper for Fomantic-UI dropdown component into a lookup component. - * - * Props - * config : - * url : the callback url. Callback should return model data in form - * of {key: model_id, text: model_title, value: model_id} - * reference: the reference field name associate with model or hasOne name. - * This field name will be sent along with url callback parameter as of 'field=name'. - * ui: the css class name to apply to dropdown. - * Note: The remaining config object may contain any or sui-dropdown {props: value} pair. - * - * value: The selected value. - * optionalValue: The initial list of options for the dropdown. - */ - -const template = ``; - -export default { - name: 'atk-lookup', - template: template, - props: ['config', 'value', 'optionalValue'], - data: function () { - const { - url, reference, ui, ...suiDropdown - } = this.config; - suiDropdown.selection = true; - - return { - dropdownProps: suiDropdown, - current: this.value, - url: url || null, - css: [ui], - isLoading: false, - field: reference, - query: '', - temp: '', - }; - }, - mounted: function () { - if (this.optionalValue) { - this.dropdownProps.options = Array.isArray(this.optionalValue) ? this.optionalValue : [this.optionalValue]; - } - }, - methods: { - onChange: function (value) { - this.$emit('onChange', value); - }, - /** - * Receive user input text for search. - */ - onFiltered: function (inputValue) { - if (inputValue) { - this.isLoading = true; - } - this.temp = inputValue; - atk.debounce(() => { - if (this.query !== this.temp) { - this.query = this.temp; - if (this.query) { - this.fetchItems(this.query); - } - } - }, 300).call(this); - }, - /** - * Fetch new data from server. - */ - fetchItems: async function (q) { - try { - const data = { atk_vlookup_q: q, atk_vlookup_field: this.field }; - const response = await atk.apiService.suiFetch(this.url, { method: 'get', data: data }); - if (response.success) { - this.dropdownProps.options = response.results; - } - this.isLoading = false; - } catch (e) { - console.error(e); - this.isLoading = false; - } - }, - }, -}; diff --git a/js/src/directives/click-outside.directive.js b/js/src/directives/click-outside.directive.js deleted file mode 100644 index ab66dd9e38..0000000000 --- a/js/src/directives/click-outside.directive.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Vue directive for handling click - * outside a component. You can specify other components - * outside of the component using the directive via an array where the directive will not be apply. - * Example a button use for opening a popup. Exclude is indicate - * via a reference name. - * - * - * - * Example usage: - *
- */ -let handleOutsideClick; - -export default { - bind: function (el, binding, vnode) { - // the click/touchstart handler - handleOutsideClick = (e) => { - e.stopPropagation(); - // Get the handler method name and the exclude array - // from the object used in v-closable - const { handler, exclude } = binding.value; - // This variable indicates if the clicked element is excluded - let clickedOnExcludedEl = false; - exclude.forEach((refName) => { - // We only run this code if we haven't detected - // any excluded element yet - if (!clickedOnExcludedEl) { - // Get the element using the reference name - const excludedEl = vnode.context.$refs[refName]; - // See if this excluded element - // is the same element the user just clicked on - clickedOnExcludedEl = excludedEl.contains(e.target); - } - }); - // We check to see if the clicked element is not - // the component element and not excluded - if (!el.contains(e.target) && !clickedOnExcludedEl) { - // If the clicked element is outside the component && one of the exclude element. - vnode.context[handler](e); - } - }; - // Register click/touchstart event listeners on the whole page - document.addEventListener('click', handleOutsideClick); - document.addEventListener('touchstart', handleOutsideClick); - }, - unbind: function () { - // If the element that has v-closable is removed, then - // unbind click/touchstart listeners from the whole page - document.removeEventListener('click', handleOutsideClick); - document.removeEventListener('touchstart', handleOutsideClick); - }, -}; diff --git a/js/src/directives/commons.directive.js b/js/src/directives/commons.directive.js deleted file mode 100644 index 6e862ae1bf..0000000000 --- a/js/src/directives/commons.directive.js +++ /dev/null @@ -1,7 +0,0 @@ -const focus = { - inserted: function (el) { - el.focus(); - }, -}; - -export { focus }; diff --git a/js/src/helpers/table-dropdown.helper.js b/js/src/helpers/table-dropdown.helper.js index 1d0d9bb893..dcdb26d963 100644 --- a/js/src/helpers/table-dropdown.helper.js +++ b/js/src/helpers/table-dropdown.helper.js @@ -1,13 +1,12 @@ +import $ from 'external/jquery'; import throttle from 'lodash/throttle'; -import $ from 'jquery'; /** * Simple helper to help displaying Fomantic-UI Dropdown within an atk table. * Because atk table use overflow: scroll, Dropdown is not * display on top of table. * - * This utility will properly set css style for dropdown menu to be display correctly. - * + * This utility will properly set CSS style for dropdown menu to be displayed correctly. */ function showTableDropdown() { // getting element composing dropdown. @@ -29,7 +28,7 @@ function showTableDropdown() { * Set menu style for displaying at right position. */ function setCssPosition() { - // console.log(position.top, $that.scrollTop()); + // console.log(position.top, $that.scrollTop()); let top = 0; let left = 0; // check if we need to place menu above or down button. @@ -43,7 +42,8 @@ function showTableDropdown() { top -= $(window).scrollTop(); left = position.left; - const style = `position: fixed; z-index: 12; top: 0px; margin-top: ${top}px !important; left: ${left}px !important; width: fit-content !important; height: fit-content; min-width: 12px`; + const style = 'position: fixed; z-index: 12; top: 0px; margin-top: ' + top + 'px !important;' + + ' left: ' + left + 'px !important; width: fit-content !important; height: fit-content; min-width: 12px;'; $menu.css('cssText', style); } @@ -55,7 +55,7 @@ function showTableDropdown() { } /** - * Reset css and handler when hiding dropdown. + * Reset CSS and handler when hiding dropdown. */ function hideTableDropdown() { // reset positioning. @@ -65,8 +65,7 @@ function hideTableDropdown() { $(window).off('resize.atktable'); } -// Export function to atk. -export const tableDropdown = { +export default { onShow: showTableDropdown, onHide: hideTableDropdown, }; diff --git a/js/src/helpers/url.helper.js b/js/src/helpers/url.helper.js index 54458bd801..4e86a2df03 100644 --- a/js/src/helpers/url.helper.js +++ b/js/src/helpers/url.helper.js @@ -1,99 +1,71 @@ -/** - * Url helper jQuery functions. - * - * - AddParams - Pass an url with an object and object key=value pair will be - * added to the url as get parameter. - * ex: $.atkAddParams('myurl.php', {q: 'test', 'reload': 'my_view'}) - * will return: myurl.php?q=test&reload=my_view - * - * -RemoveParam - remove a parameter from an url string. - * ex: $.atkRemoveParam('myurl.php?q=test&reload=my_view', 'q') - * will return: myurl.php?reload=my_view - * - */ - -(function ($) { - if (!$.atk) { - $.atk = {}; - } +import $ from 'external/jquery'; +export default { /** - * Get the base url from string. + * Get each URL query parameter as a key:value pair object. * - * @param url - * @returns {*|string} + * @returns {object} */ - $.atk.getUrl = function (url) { - return url.split('?')[0]; - }; + parseParams: function (url) { + const query = url.includes('?') ? url.slice(url.indexOf('?') + 1) : ''; - /** - * Get each url query parameter as a key:value pair object. - * - * @param str - * @returns {{}|unknown} - */ - $.atk.getQueryParams = function (str) { - if (str.split('?')[1]) { - return decodeURIComponent(str.split('?')[1]) - .split('&') - .reduce((obj, unsplitArg) => { - const arg = unsplitArg.split('='); - // eslint-disable-next-line prefer-destructuring - obj[arg[0]] = arg[1]; + const res = {}; + for (const queryPart of query.split('&')) { + if (queryPart.length > 0) { + let k = queryPart; + let v = null; + if (k.includes('=')) { + v = k.slice(k.indexOf('=') + 1); + k = k.slice(0, k.indexOf('=')); + } - return obj; - }, {}); + res[decodeURIComponent(k)] = decodeURIComponent(v); + } } - return {}; - }; + return res; + }, /** - * Add param to an url string. + * Add param to an URL string. + * + * ex: atk.urlHelper.appendParams('myurl.php', { q: 'test', 'reload': 'myView' }) + * will return: myurl.php?q=test&reload=myView * - * @param url - * @param data - * @returns {*} + * @returns {string} */ - $.atk.addParams = function (url, data) { - if (!$.isEmptyObject(data)) { - url += (url.indexOf('?') >= 0 ? '&' : '?') + $.param(data); + appendParams: function (url, data) { + const query = $.param(data); + if (query !== '') { + url += (url.includes('?') ? '&' : '?') + query; } return url; - }; + }, /** - * Remove param from an url string. + * Remove param from an URL string. * - * @param url - * @param param - * @returns {string|*|string} + * ex: atk.urlHelper.removeParam('myurl.php?q=test&reload=myView', 'q') + * will return: myurl.php?reload=myView + * + * @returns {string} */ - $.atk.removeParam = function (url, param) { - const splitUrl = url.split('?'); - if (splitUrl.length === 0) { - return url; - } + removeParam: function (url, param) { + const query = url.includes('?') ? url.slice(url.indexOf('?') + 1) : ''; + const newParams = (query.length > 0 ? query.split('&') : []) + .filter((queryPart) => decodeURIComponent(queryPart.split('=')[0]) !== param); - const urlBase = splitUrl[0]; - if (splitUrl.length === 1) { - return urlBase; - } - - const newParams = splitUrl[1].split('&').filter((item) => item.split('=')[0] !== param); - if (newParams.length > 0) { - return urlBase + '?' + newParams.join('&'); - } - - return urlBase; - }; -}(jQuery)); + return url.slice(0, Math.max(0, url.indexOf('?'))) + + (newParams.length > 0 ? '?' + newParams.join('&') : ''); + }, -export default (function ($) { - $.atkGetUrl = $.atk.getUrl; - $.atkAddParams = $.atk.addParams; - $.atkRemoveParam = $.atk.removeParam; - $.atkGetQueryParam = $.atk.getQueryParams; -}(jQuery)); + /** + * Remove whole query string from an URL string. + * + * @returns {string} + */ + removeAllParams: function (url) { + return url.split('?')[0]; + }, +}; diff --git a/js/src/main.js b/js/src/main.js new file mode 100644 index 0000000000..5abfc17235 --- /dev/null +++ b/js/src/main.js @@ -0,0 +1,9 @@ +import 'core-js/stable'; +import atk from './setup-atk'; // must be the first non-vendor import +import './setup-plugins'; +import './setup-utils'; +import './setup-fomantic-ui'; + +__webpack_public_path__ = window.__atkBundlePublicPath + '/'; // eslint-disable-line no-undef, camelcase + +export default atk; // eslint-disable-line unicorn/prefer-export-from diff --git a/js/src/plugin.js b/js/src/plugin.js deleted file mode 100644 index 8fc0813acc..0000000000 --- a/js/src/plugin.js +++ /dev/null @@ -1,104 +0,0 @@ -import $ from 'jquery'; -import spinner from './plugins/spinner.plugin'; -import serverEvent from './plugins/server-event.plugin'; -import reloadView from './plugins/reload-view.plugin'; -import ajaxec from './plugins/ajaxec.plugin'; -import createModal from './plugins/create-modal.plugin'; -import notify from './plugins/notify.plugin'; -import fileUpload from './plugins/file-upload.plugin'; -import JsSearch from './plugins/js-search.plugin'; -import JsSortable from './plugins/js-sortable.plugin'; -import conditionalForm from './plugins/conditional-form.plugin'; -import columnResizer from './plugins/column-resizer.plugin'; -import scroll from './plugins/scroll.plugin'; -import confirm from './plugins/confirm.plugin'; -import sidenav from './plugins/sidenav.plugin'; - -/** - * Generate a jQuery plugin - * @param name [string] Plugin name - * @param className [object] Class of the plugin - * @param shortHand [bool] Generate a shorthand as $.pluginName - * - * @example - * import plugin from 'plugin'; - * - * class MyPlugin { - * constructor(element, options) { - * // ... - * } - * } - * - * MyPlugin.DEFAULTS = {}; - * - * plugin('myPlugin', MyPlugin); - * - * credit : https://gist.github.com/monkeymonk/c08cb040431f89f99928132ca221d647 - * - * import $ from 'jquery' will bind '$' var to jQuery var without '$' var conflicting with other library - * in final webpack output. - */ -function plugin(name, className, shortHand = false) { - // Add atk namespace to jQuery global space. - if (!$.atk) { - $.atk = {}; - } - - const pluginName = 'atk' + name; - const dataName = `__${pluginName}`; - - // add plugin to atk namespace. - $.atk[name] = className; - - // register plugin to jQuery fn prototype. - $.fn[pluginName] = function (option = {}, args = []) { - // Check if we are calling a plugin specific function: $(element).plugin('function', [arg1, arg2]); - if (typeof option === 'string') { - if (this.data(dataName) && typeof this.data(dataName)[option] === 'function') { - return this.data(dataName).call(option, args); - } - // return if trying to call a plugin method prior to instantiate it. - return; - } - - return this.each(function () { - const options = $.extend({}, className.DEFAULTS, typeof option === 'object' && option); - // create plugin using the constructor function store in atk namespace object - // and add a reference of it to this jQuery object data. - $(this).data(dataName, new $.atk[name](this, options)); - }); - }; - - // - Short hand - if (shortHand) { - $[pluginName] = (options) => $({})[pluginName](options); - } -} - -/** - * Create all jQuery plugins need for atk. - */ -(function () { - const atkJqPlugins = [ - { name: 'Spinner', plugin: spinner, sh: false }, - { name: 'ReloadView', plugin: reloadView, sh: false }, - { name: 'Ajaxec', plugin: ajaxec, sh: false }, - { name: 'CreateModal', plugin: createModal, sh: false }, - { name: 'Notify', plugin: notify, sh: true }, - { name: 'ServerEvent', plugin: serverEvent, sh: true }, - { name: 'FileUpload', plugin: fileUpload, sh: false }, - { name: 'JsSearch', plugin: JsSearch, sh: false }, - { name: 'JsSortable', plugin: JsSortable, sh: false }, - { name: 'ConditionalForm', plugin: conditionalForm, sh: true }, - { name: 'ColumnResizer', plugin: columnResizer, sh: false }, - { name: 'Scroll', plugin: scroll, sh: false }, - { name: 'Confirm', plugin: confirm, sh: true }, - { name: 'Sidenav', plugin: sidenav, sh: false }, - ]; - - atkJqPlugins.forEach((atkJqPlugin) => { - plugin(atkJqPlugin.name, atkJqPlugin.plugin, atkJqPlugin.sh); - }); -}()); - -export { plugin }; diff --git a/js/src/plugins/ajaxec.plugin.js b/js/src/plugins/ajaxec.plugin.js index 23d6bbdee9..024d26fef4 100644 --- a/js/src/plugins/ajaxec.plugin.js +++ b/js/src/plugins/ajaxec.plugin.js @@ -1,19 +1,18 @@ -/* eslint no-alert: "off" */ +import $ from 'external/jquery'; +import atk from 'atk'; +import AtkPlugin from './atk.plugin'; -import $ from 'jquery'; -import atkPlugin from './atk.plugin'; - -export default class ajaxec extends atkPlugin { +export default class AtkAjaxecPlugin extends AtkPlugin { main() { - if (!this.settings.uri) { - console.error('Trying to execute callback without url.'); + if (!this.settings.url) { + console.error('Trying to execute callback without URL'); return; } // Allow user to confirm if available. if (this.settings.confirm) { - if (window.confirm(this.settings.confirm)) { + if (window.confirm(this.settings.confirm)) { // eslint-disable-line no-alert this.doExecute(); } } else if (!this.$el.hasClass('loading')) { @@ -22,14 +21,14 @@ export default class ajaxec extends atkPlugin { } doExecute() { - const url = $.atk.getUrl(this.settings.uri); - const userConfig = this.settings.apiConfig ? this.settings.apiConfig : {}; + const url = atk.urlHelper.removeAllParams(this.settings.url); + const userConfig = this.settings.apiConfig ?? {}; - // uri_options is always use as data in a post request. - const data = this.settings.uri_options ? this.settings.uri_options : {}; + // urlOptions is always used as data in a POST request + const data = this.settings.urlOptions ?? {}; - // retrieve param from url. - let urlParam = $.atkGetQueryParam(this.settings.uri); + // retrieve param from URL + let urlParams = atk.urlHelper.parseParams(this.settings.url); // get store object. const store = atk.dataService.getStoreData(this.settings.storeName); @@ -42,21 +41,21 @@ export default class ajaxec extends atkPlugin { ...userConfig, }; - if (settings.method.toLowerCase() === 'get') { - // set data, store and add it to url param. - urlParam = Object.assign(urlParam, data, store); + if (settings.method.toUpperCase() === 'GET') { + // set data, store and add it to URL param. + urlParams = Object.assign(urlParams, data, store); } else { settings.data = Object.assign(data, store); } - settings.url = url + '?' + $.param(urlParam); + settings.url = url + '?' + $.param(urlParams); this.$el.api(settings); } } -ajaxec.DEFAULTS = { - uri: null, - uri_options: {}, +AtkAjaxecPlugin.DEFAULTS = { + url: null, + urlOptions: {}, confirm: null, apiConfig: null, storeName: null, diff --git a/js/src/plugins/atk.plugin.js b/js/src/plugins/atk.plugin.js index 75e444d858..e3ca824656 100644 --- a/js/src/plugins/atk.plugin.js +++ b/js/src/plugins/atk.plugin.js @@ -1,17 +1,13 @@ -import $ from 'jquery'; +import $ from 'external/jquery'; /** * Base implementation of jQuery plugin in Agile Toolkit. - * */ - -export default class atkPlugin { +export default class AtkPlugin { /** * Default plugin constructor * - * @param element - * @param options - * @returns {atkPlugin} + * @returns {AtkPlugin} */ constructor(element, options) { this.$el = $(element); @@ -31,8 +27,9 @@ export default class atkPlugin { * Call a plugin method via the initializer function. * Simply call the method like: $(selector).pluginName('method', [arg1, arg2]) * - * @param fn : string representing the method name to execute. - * @param args : array of arguments need for the method to execute. + * @param {string} fn string representing the method name to execute. + * @param {Array.<*>} args array of arguments need for the method to execute. + * * @returns {*} */ call(fn, args) { diff --git a/js/src/plugins/column-resizer.plugin.js b/js/src/plugins/column-resizer.plugin.js index b98199f9df..927dd01b54 100644 --- a/js/src/plugins/column-resizer.plugin.js +++ b/js/src/plugins/column-resizer.plugin.js @@ -1,61 +1,53 @@ -import $ from 'jquery'; +import $ from 'external/jquery'; import Resizer from 'column-resizer'; -import atkPlugin from './atk.plugin'; +import AtkPlugin from './atk.plugin'; /** * Enable table column to be resizable using drag. */ -export default class columnResizer extends atkPlugin { +export default class AtkColumnResizerPlugin extends AtkPlugin { main() { - // add on resize callback if url is supply. - if (this.settings.uri) { - this.settings.onResize = this.onResize.bind(this); - } - this.resizable = new Resizer(this.$el[0], ({ ...this.settings.atkDefaults, ...this.settings })); + this.settings.onResize = this.onResize.bind(this); + this.resizable = new Resizer(this.$el[0], { ...this.settings.atkDefaults, ...this.settings }); // reset padding class. this.$el.removeClass('grip-padding'); } /** - * Send widths to server via callback uri. + * Send widths to server via callback URL. * - * @param widths an Array of objects, each containing the column name and their size in pixels [{column: 'name', size: '135px'}] + * @param {Array.} widths example: [{ column: 'name', size: 135 }] */ sendWidths(widths) { this.$el.api({ on: 'now', - url: this.settings.uri, + url: this.settings.url, method: 'POST', data: { widths: JSON.stringify(widths) }, }); } - /** - * On resize callback when user finish dragging column for resizing. - * Calling this method via callback need to bind "this" set to this plugin. - * - * @param e the event. - */ - onResize(e) { - const columns = this.$el.find('th'); + onResize(event) { + if (this.settings.url) { + const columns = this.$el.find('th'); - const widths = []; - columns.each((idx, item) => { - widths.push({ column: $(item).data('column'), size: $(item).outerWidth() }); - }); + const widths = []; + columns.each((i, item) => { + widths.push({ column: $(item).data('column'), size: $(item).outerWidth() }); + }); - this.sendWidths(widths); + this.sendWidths(widths); + } } } -columnResizer.DEFAULTS = { +AtkColumnResizerPlugin.DEFAULTS = { atkDefaults: { + resizeMode: 'flex', liveDrag: true, - resizeMode: 'overflow', draggingClass: 'atk-column-dragging', - minWidth: 8, - // onResize: function(e) { e.path.filter(function(item) { return item.querySelector('table') }); } + serialize: false, }, - uri: null, + url: null, }; diff --git a/js/src/plugins/conditional-form.plugin.js b/js/src/plugins/conditional-form.plugin.js index 8711e721b0..12ab30dbc0 100644 --- a/js/src/plugins/conditional-form.plugin.js +++ b/js/src/plugins/conditional-form.plugin.js @@ -1,72 +1,69 @@ -import atkPlugin from './atk.plugin'; -import formService from '../services/form.service'; - -/* eslint-disable no-bitwise */ +import atk from 'atk'; +import AtkPlugin from './atk.plugin'; /** * Show or hide input field base on other input field condition. * Support all Fomantic-UI form validation rule. * Note on rule. FormService also add two more rule to Fomantic-UI existing ones: - * - notEmpty; - * - isVisible; - * - isEqual[number] for number comparaison. + * - notEmpty; + * - isVisible; + * - isEqual[number] for number comparison. * * Here is the phrasing of the rule. - * - Show "this field" if all condition are met. - * fieldRules is an array that contains items where each item describe the field to hide or show - * that depends on other field with their input value conditions. - * - * $form->js()->atkConditionalForm( - * [ 'fieldRules => - * [ - * 'fieldToShow' => ['field1' => 'notEmpty', 'field2' => 'number'] - * ] - * ]); - * Can be phrase this way: Display 'fieldToShow' if 'field1' is not empty AND field2 is a number. + * - Show "this field" if all condition are met. + * fieldRules is an array that contains items where each item describe the field to hide or show + * that depends on other field with their input value conditions. * - * Adding and array of field => rules for the same field will OR the condition for that field. - * $form->js()->atkConditionalForm( - * [ 'fieldRules => - * [ - * 'hair_cut' => [ - * ['race' => 'contains[poodle]', 'age' => 'integer[0..5]'], - * ['race' => 'isExactly[bichon]'] - * ] - * ] - * ]); - * Can be phrase this way: Display 'hair_cut' if 'race' contains 'poodle' AND 'age' is between 0 and 5 OR 'race' contains the exact word 'bichon'. + * $form->js()->atkConditionalForm( + * [ 'fieldRules => + * [ + * 'fieldToShow' => ['field1' => 'notEmpty', 'field2' => 'number'] + * ] + * ]); + * Can be phrase this way: Display 'fieldToShow' if 'field1' is not empty AND field2 is a number. * + * Adding and array of field => rules for the same field will OR the condition for that field. + * $form->js()->atkConditionalForm( + * [ 'fieldRules => + * [ + * 'haircut' => [ + * ['race' => 'contains[poodle]', 'age' => 'integer[0..5]'], + * ['race' => 'isExactly[bichon]'] + * ] + * ] + * ]); + * Can be phrase this way: Display 'haircut' if 'race' contains 'poodle' AND 'age' is between 0 and 5 OR 'race' contains the exact word 'bichon'. * - * Adding an array of conditions for the same field is also support. + * Adding an array of conditions for the same field is also support. * - * $form->js()->atkConditionalForm( - * [ 'fieldRules => - * [ - * 'ext' => ['phone' => ['number', 'minLength[7]']] - * ] - * ]); - * Can be phrase this way: Display 'ext' if phone is a number AND phone has at least 7 char. + * $form->js()->atkConditionalForm( + * [ 'fieldRules => + * [ + * 'ext' => ['phone' => ['number', 'minLength[7]']] + * ] + * ]); + * Can be phrase this way: Display 'ext' if phone is a number AND phone has at least 7 char. * - * See Fomantic-UI validation rule for more details: https://fomantic-ui.com/behaviors/form.html#validation-rules + * See Fomantic-UI validation rule for more details: https://fomantic-ui.com/behaviors/form.html#validation-rules */ -export default class conditionalForm extends atkPlugin { +export default class AtkConditionalFormPlugin extends AtkPlugin { main() { this.inputs = []; this.selector = this.settings.selector; if (!this.selector) { - this.selector = formService.getDefaultSelector(); + this.selector = atk.formService.getDefaultSelector(); } // add change listener to inputs according to selector this.$el.find(':checkbox') - .on('change', this, atk.debounce(this.onInputChange, 100, true)); + .on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true)); this.$el.find(':radio') - .on('change', this, atk.debounce(this.onInputChange, 100, true)); + .on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true)); this.$el.find('input[type="hidden"]') - .on('change', this, atk.debounce(this.onInputChange, 100, true)); + .on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true)); this.$el.find('input') - .on(this.settings.validateEvent, this, atk.debounce(this.onInputChange, 250)); + .on(this.settings.validateEvent, this, atk.createDebouncedFx(this.onInputChange, 250)); this.$el.find('select') - .on('change', this, atk.debounce(this.onInputChange, 100)); + .on('change', this, atk.createDebouncedFx(this.onInputChange, 100)); this.initialize(); } @@ -82,7 +79,9 @@ export default class conditionalForm extends atkPlugin { const tempRule = this.settings.fieldRules[ruleKey]; const temp = []; if (Array.isArray(tempRule)) { - tempRule.forEach((rule) => temp.push(rule)); + for (const rule of tempRule) { + temp.push(rule); + } } else { temp.push(tempRule); } @@ -96,11 +95,9 @@ export default class conditionalForm extends atkPlugin { /** * Field change handler. - * - * @param e */ onInputChange(e) { - // check rule when inputs has changed. + // check rule when inputs has changed. e.data.resetInputStatus(); e.data.applyRules(); e.data.setInputsState(); @@ -109,52 +106,51 @@ export default class conditionalForm extends atkPlugin { /** * Check each validation rule and apply proper visibility state to the * input where rules apply. - * */ applyRules() { - this.inputs.forEach((input, idx) => { - input.rules.forEach((rules) => { + for (const input of this.inputs) { + for (const rules of input.rules) { let isAndValid = true; const validateInputNames = Object.keys(rules); - validateInputNames.forEach((inputName) => { + for (const inputName of validateInputNames) { const validationRule = rules[inputName]; if (Array.isArray(validationRule)) { - validationRule.forEach((rule) => { - isAndValid &= formService.validateField(this.$el, inputName, rule); - }); + for (const rule of validationRule) { + isAndValid = isAndValid && atk.formService.validateField(this.$el, inputName, rule); + } } else { - isAndValid &= formService.validateField(this.$el, inputName, validationRule); + isAndValid = isAndValid && atk.formService.validateField(this.$el, inputName, validationRule); } - }); + } // Apply OR condition between rules. - input.state |= isAndValid; - }); - }); + input.state = input.state || isAndValid; + } + } } /** * Set all input state visibility to false. */ resetInputStatus() { - this.inputs.forEach((input) => { + for (const input of this.inputs) { input.state = false; - }); + } } /** * Set fields visibility according to their state. */ setInputsState() { - this.inputs.forEach((input) => { - const $input = formService.getField(this.$el, input.inputName); + for (const input of this.inputs) { + const $input = atk.formService.getField(this.$el, input.inputName); if ($input) { - const $container = formService.getContainer($input, this.selector); + const $container = atk.formService.getContainer($input, this.selector); if ($container) { $container.hide(); this.setInputState(input.state, $input, $container); } } - }); + } } setInputState(passed, field, fieldGroup) { @@ -169,7 +165,7 @@ export default class conditionalForm extends atkPlugin { } } -conditionalForm.DEFAULTS = { +AtkConditionalFormPlugin.DEFAULTS = { autoReset: true, validateEvent: 'keydown', selector: null, diff --git a/js/src/plugins/confirm.plugin.js b/js/src/plugins/confirm.plugin.js index b1d666a6d6..b6df046da4 100644 --- a/js/src/plugins/confirm.plugin.js +++ b/js/src/plugins/confirm.plugin.js @@ -1,5 +1,5 @@ -import $ from 'jquery'; -import atkPlugin from './atk.plugin'; +import $ from 'external/jquery'; +import AtkPlugin from './atk.plugin'; /** * A Fomantic-UI Modal dialog for confirming an action. @@ -11,9 +11,8 @@ import atkPlugin from './atk.plugin'; * Setting onApprove and onDeny function within modalOptions object will override * onApprove and onDeny current setting. */ -export default class confirm extends atkPlugin { +export default class AtkConfirmPlugin extends AtkPlugin { main() { - let context = this; const $m = $('