From a9bd46ed5a68fdb5a4c217cf1e4fcd1c9d814dad Mon Sep 17 00:00:00 2001 From: "James K." Date: Fri, 25 Jul 2025 10:54:35 -0400 Subject: [PATCH 01/83] Correcting a bug with incomplete quotes --- src/templates/admin/invKoForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/admin/invKoForm.php b/src/templates/admin/invKoForm.php index fe88f491..ea313319 100644 --- a/src/templates/admin/invKoForm.php +++ b/src/templates/admin/invKoForm.php @@ -315,7 +315,7 @@ function InvType(data) { this.excludeIf = ko.observable(data.excludeIf ?? []); this.hierarchical = ko.observable(data.hierarchical ?? false); this.importMeetings = ko.observable(data.importMeetings ?? false); - this.meetingGroupingMethod = ko.observable(data.meetingGroupingMethod ?? ); + this.meetingGroupingMethod = ko.observable(data.meetingGroupingMethod ?? ""); this.groupBy = ko.observable(data.groupBy ?? ""); this.leaderTypes = ko.observableArray(data.leaderTypes ?? []); this.hostTypes = ko.observableArray(data.hostTypes ?? []); From 6979021eb5c67bcd82e854571d3248e9ee64ad61 Mon Sep 17 00:00:00 2001 From: "James K." Date: Mon, 28 Jul 2025 20:19:12 -0400 Subject: [PATCH 02/83] Fix a bug where small group leaders aren't always being assigned tasks. Related to #146 and #175. --- src/python/WebApi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/python/WebApi.py b/src/python/WebApi.py index 939588b3..78407a44 100644 --- a/src/python/WebApi.py +++ b/src/python/WebApi.py @@ -793,7 +793,7 @@ def get_person_info_for_sync(person_obj): orgContactSql = ''' SELECT TOP 1 IntValue as contactId FROM OrganizationExtra WHERE OrganizationId = {0} AND Field = '{1}' UNION - SELECT TOP 1 MainLeaderId as contactId FROM Organizations WHERE OrganizationId = {0} + SELECT TOP 1 MainLeaderId as contactId FROM Organizations WHERE OrganizationId = {0} AND MainLeaderId IS NOT NULL UNION SELECT TOP 1 PeopleId as contactId FROM OrganizationMembers WHERE OrganizationId = {0} AND MemberTypeId in ({2}) '''.format(oid, sgContactEvName, memTypes) @@ -843,7 +843,7 @@ def get_person_info_for_sync(person_obj): orgContactSql = ''' SELECT TOP 1 IntValue as contactId FROM OrganizationExtra WHERE OrganizationId = {0} AND Field = '{1}' UNION - SELECT TOP 1 MainLeaderId as contactId FROM Organizations WHERE OrganizationId = {0} + SELECT TOP 1 MainLeaderId as contactId FROM Organizations WHERE OrganizationId = {0} AND MainLeaderId IS NOT NULL UNION SELECT TOP 1 PeopleId as contactId FROM OrganizationMembers WHERE OrganizationId = {0} AND MemberTypeId in ({2}) '''.format(oid, sgContactEvName, memTypes) From d3070137a12b6d72975a3f8718d31ef46f2d68b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:54:00 +0000 Subject: [PATCH 03/83] Initial plan From e7a594fcda8d2ac4fba4ca754700fc0e1c5c1cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:03:06 +0000 Subject: [PATCH 04/83] Add comprehensive unit testing infrastructure with PHPUnit Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com> --- .github/workflows/tests.yml | 58 ++++++ .gitignore | 5 + TESTING.md | 165 ++++++++++++++++ composer.json | 14 +- phpunit.xml | 24 +++ tests/TestCase.php | 35 ++++ tests/Unit/GeoTest.php | 140 +++++++++++++ tests/Unit/TouchPointWP_ExceptionTest.php | 150 ++++++++++++++ tests/Unit/UtilitiesTest.php | 227 ++++++++++++++++++++++ tests/bootstrap.php | 198 +++++++++++++++++++ 10 files changed, 1015 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml create mode 100644 TESTING.md create mode 100644 phpunit.xml create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/GeoTest.php create mode 100644 tests/Unit/TouchPointWP_ExceptionTest.php create mode 100644 tests/Unit/UtilitiesTest.php create mode 100644 tests/bootstrap.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..2b183123 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,58 @@ +name: Tests + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + test: + name: Run PHPUnit Tests + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['8.0', '8.1', '8.2', '8.3'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: json, zip, dom + coverage: xdebug + + - name: Validate composer.json + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php-${{ matrix.php-version }}- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: composer test + + - name: Generate coverage report (PHP 8.3 only) + if: matrix.php-version == '8.3' + run: ./vendor/bin/phpunit --coverage-clover coverage.xml + + - name: Upload coverage to artifact (PHP 8.3 only) + if: matrix.php-version == '8.3' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.xml diff --git a/.gitignore b/.gitignore index 12986275..1e372367 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ package-lock.json /build/ /touchpoint-wp.zip + +# Test artifacts +/.phpunit.cache/ +/coverage/ +.phpunit.result.cache diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..0727d63d --- /dev/null +++ b/TESTING.md @@ -0,0 +1,165 @@ +# Testing TouchPoint-WP + +This document describes how to run and write tests for the TouchPoint-WP plugin. + +## Running Tests + +### Prerequisites + +- PHP 8.0 or higher +- Composer + +### Installation + +First, install the development dependencies: + +```bash +composer install +``` + +### Running the Test Suite + +To run all tests: + +```bash +composer test +``` + +Or directly with PHPUnit: + +```bash +./vendor/bin/phpunit +``` + +### Running Specific Tests + +To run tests in a specific file: + +```bash +./vendor/bin/phpunit tests/Unit/GeoTest.php +``` + +To run a specific test method: + +```bash +./vendor/bin/phpunit --filter test_distance_calculation +``` + +### Code Coverage + +To generate a code coverage report: + +```bash +composer test-coverage +``` + +This will create an HTML coverage report in the `coverage/` directory. Open `coverage/index.html` in your browser to view the report. + +## Writing Tests + +### Test Structure + +Tests are organized in the `tests/` directory: + +- `tests/Unit/` - Unit tests for individual classes and methods +- `tests/Integration/` - Integration tests (if needed) +- `tests/TestCase.php` - Base test case class that all tests should extend +- `tests/bootstrap.php` - Bootstrap file that sets up the test environment + +### Creating a New Test + +1. Create a new test file in the appropriate directory (e.g., `tests/Unit/MyClassTest.php`) +2. Extend the `tp\TouchPointWP\Tests\TestCase` class +3. Add the `@covers` annotation to specify which class you're testing +4. Write test methods (must start with `test_` or use the `@test` annotation) + +Example: + +```php +myMethod(); + + $this->assertSame('expected', $result); + } +} +``` + +### Test Naming Conventions + +- Test files should be named `{ClassName}Test.php` +- Test methods should be named `test_{method_name}_{scenario}` (e.g., `test_distance_calculation_same_point`) +- Use descriptive names that explain what is being tested + +### Assertions + +PHPUnit provides many assertion methods. Common ones include: + +- `assertSame($expected, $actual)` - Strict equality check +- `assertEquals($expected, $actual)` - Loose equality check +- `assertTrue($condition)` - Check if condition is true +- `assertFalse($condition)` - Check if condition is false +- `assertNull($value)` - Check if value is null +- `assertInstanceOf($class, $object)` - Check object type +- `assertEqualsWithDelta($expected, $actual, $delta)` - Check numeric equality with tolerance + +See the [PHPUnit documentation](https://phpunit.readthedocs.io/) for more assertion methods. + +## Continuous Integration + +Tests are automatically run on every push and pull request via GitHub Actions. The test workflow: + +- Runs on PHP versions 8.0, 8.1, 8.2, and 8.3 +- Validates composer.json +- Installs dependencies +- Runs the complete test suite +- Generates code coverage report (PHP 8.3 only) + +You can view the test results in the "Actions" tab of the GitHub repository. + +## Best Practices + +1. **Write isolated tests** - Each test should be independent and not rely on other tests +2. **Test one thing per test** - Each test method should verify one specific behavior +3. **Use descriptive test names** - Test names should clearly explain what is being tested +4. **Mock external dependencies** - Use mocks or stubs for external services and WordPress functions +5. **Keep tests fast** - Unit tests should run quickly; avoid unnecessary setup or external calls +6. **Test edge cases** - Include tests for boundary conditions, error cases, and unusual inputs +7. **Maintain tests** - Update tests when you change code; failing tests should be fixed, not removed + +## Troubleshooting + +### Tests fail with "undefined constant" errors + +Make sure all required constants are defined in `tests/bootstrap.php`. + +### Tests fail with "undefined function" errors + +WordPress functions may need to be mocked in `tests/bootstrap.php` or in individual test files. + +### Can't run tests + +Ensure you have installed dev dependencies: + +```bash +composer install +``` + +If you've only installed production dependencies, add the dev dependencies: + +```bash +composer install --dev +``` diff --git a/composer.json b/composer.json index b0b86438..fd35a3d6 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,11 @@ "tp\\TouchPointWP\\Interfaces\\": "src/TouchPoint-WP/Interfaces" } }, + "autoload-dev": { + "psr-4": { + "tp\\TouchPointWP\\Tests\\": "tests/" + } + }, "require": { "php": ">=8.0", "composer/installers": "~1.0", @@ -36,11 +41,18 @@ }, "require-dev": { "pronamic/wp-documentor": "^1.3", - "skayo/phpdoc-md": "^0.2.0" + "skayo/phpdoc-md": "^0.2.0", + "phpunit/phpunit": "^9.5", + "mockery/mockery": "^1.5", + "yoast/phpunit-polyfills": "^1.0" }, "config": { "allow-plugins": { "composer/installers": true } + }, + "scripts": { + "test": "phpunit", + "test-coverage": "phpunit --coverage-html coverage" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..ec2d2a73 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,24 @@ + + + + + tests + + + + + src + + + src/TouchPoint-WP/Interfaces + + + + + + diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..92286cfa --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,35 @@ +assertNull($geo->lat); + $this->assertNull($geo->lng); + $this->assertNull($geo->human); + $this->assertNull($geo->type); + } + + /** + * Test that Geo object can be instantiated with specific values. + */ + public function test_geo_instantiation_with_values(): void + { + $lat = 40.7128; + $lng = -74.0060; + $human = 'New York, NY'; + $type = 'city'; + + $geo = new Geo($lat, $lng, $human, $type); + + $this->assertSame($lat, $geo->lat); + $this->assertSame($lng, $geo->lng); + $this->assertSame($human, $geo->human); + $this->assertSame($type, $geo->type); + } + + /** + * Test distance calculation between two points. + * Testing with known coordinates (New York to Los Angeles). + */ + public function test_distance_calculation(): void + { + // New York coordinates + $nyLat = 40.7128; + $nyLng = -74.0060; + + // Los Angeles coordinates + $laLat = 34.0522; + $laLng = -118.2437; + + $distance = Geo::distance($nyLat, $nyLng, $laLat, $laLng); + + // Expected distance is approximately 2451 miles + // Allow for some rounding variance + $this->assertEqualsWithDelta(2451, $distance, 10); + } + + /** + * Test distance calculation returns zero for same coordinates. + */ + public function test_distance_calculation_same_point(): void + { + $lat = 40.7128; + $lng = -74.0060; + + $distance = Geo::distance($lat, $lng, $lat, $lng); + + $this->assertSame(0.0, $distance); + } + + /** + * Test distance calculation with nearby points. + */ + public function test_distance_calculation_nearby_points(): void + { + // Two points very close together (approximately 1 mile apart) + $lat1 = 40.7128; + $lng1 = -74.0060; + $lat2 = 40.7260; + $lng2 = -74.0050; + + $distance = Geo::distance($lat1, $lng1, $lat2, $lng2); + + // Should be close to 1 mile + $this->assertGreaterThan(0, $distance); + $this->assertLessThan(2, $distance); + } + + /** + * Test distance calculation with international coordinates. + */ + public function test_distance_calculation_international(): void + { + // London coordinates + $londonLat = 51.5074; + $londonLng = -0.1278; + + // Paris coordinates + $parisLat = 48.8566; + $parisLng = 2.3522; + + $distance = Geo::distance($londonLat, $londonLng, $parisLat, $parisLng); + + // Expected distance is approximately 213 miles + $this->assertEqualsWithDelta(213, $distance, 10); + } + + /** + * Test that distance calculation handles negative coordinates (Southern/Western hemispheres). + */ + public function test_distance_calculation_negative_coordinates(): void + { + // Sydney, Australia + $sydneyLat = -33.8688; + $sydneyLng = 151.2093; + + // Melbourne, Australia + $melbourneLat = -37.8136; + $melbourneLng = 144.9631; + + $distance = Geo::distance($sydneyLat, $sydneyLng, $melbourneLat, $melbourneLng); + + // Expected distance is approximately 443 miles + $this->assertEqualsWithDelta(443, $distance, 10); + } +} diff --git a/tests/Unit/TouchPointWP_ExceptionTest.php b/tests/Unit/TouchPointWP_ExceptionTest.php new file mode 100644 index 00000000..520a1f53 --- /dev/null +++ b/tests/Unit/TouchPointWP_ExceptionTest.php @@ -0,0 +1,150 @@ +assertSame($message, $exception->getMessage()); + $this->assertSame(0, $exception->getCode()); + } + + /** + * Test that exception can be instantiated with message and code. + */ + public function test_exception_instantiation_with_code(): void + { + $message = 'Test error message'; + $code = 404; + $exception = new TouchPointWP_Exception($message, $code); + + $this->assertSame($message, $exception->getMessage()); + $this->assertSame($code, $exception->getCode()); + } + + /** + * Test that exception can be thrown and caught. + */ + public function test_exception_can_be_thrown_and_caught(): void + { + $message = 'Test error message'; + + try { + throw new TouchPointWP_Exception($message); + $this->fail('Exception should have been thrown'); + } catch (TouchPointWP_Exception $e) { + $this->assertSame($message, $e->getMessage()); + } + } + + /** + * Test that exception can be created with previous exception. + */ + public function test_exception_with_previous_exception(): void + { + $previousMessage = 'Previous error'; + $message = 'Current error'; + + $previous = new \Exception($previousMessage); + $exception = new TouchPointWP_Exception($message, 0, $previous); + + $this->assertSame($message, $exception->getMessage()); + $this->assertNotNull($exception->getPrevious()); + $this->assertSame($previousMessage, $exception->getPrevious()->getMessage()); + } + + /** + * Test toJson method returns valid JSON. + */ + public function test_to_json_returns_valid_json(): void + { + $message = 'Test error message'; + $code = 500; + $exception = new TouchPointWP_Exception($message, $code); + + $json = $exception->toJson(); + $this->assertIsString($json); + + $decoded = json_decode($json, true); + $this->assertIsArray($decoded); + $this->assertArrayHasKey('error', $decoded); + $this->assertArrayHasKey('status', $decoded['error']); + $this->assertArrayHasKey('code', $decoded['error']); + $this->assertArrayHasKey('message', $decoded['error']); + $this->assertArrayHasKey('location', $decoded['error']); + + $this->assertSame('failure', $decoded['error']['status']); + $this->assertSame($code, $decoded['error']['code']); + $this->assertSame($message, $decoded['error']['message']); + } + + /** + * Test toWpError method returns WP_Error object. + */ + public function test_to_wp_error_returns_wp_error(): void + { + $message = 'Test error message'; + $code = 404; + $exception = new TouchPointWP_Exception($message, $code); + + $wpError = $exception->toWpError(); + + // WP_Error is mocked in our test environment, but we can check the structure + $this->assertInstanceOf(\WP_Error::class, $wpError); + } + + /** + * Test exception extends standard PHP Exception. + */ + public function test_exception_extends_exception(): void + { + $exception = new TouchPointWP_Exception('test'); + $this->assertInstanceOf(\Exception::class, $exception); + } + + /** + * Test exception has file and line information. + */ + public function test_exception_has_file_and_line_info(): void + { + $exception = new TouchPointWP_Exception('test'); + + $this->assertIsString($exception->getFile()); + $this->assertIsInt($exception->getLine()); + $this->assertGreaterThan(0, $exception->getLine()); + } + + /** + * Test exception has trace information. + */ + public function test_exception_has_trace_info(): void + { + $exception = new TouchPointWP_Exception('test'); + + $trace = $exception->getTrace(); + $this->assertIsArray($trace); + + $traceString = $exception->getTraceAsString(); + $this->assertIsString($traceString); + } +} diff --git a/tests/Unit/UtilitiesTest.php b/tests/Unit/UtilitiesTest.php new file mode 100644 index 00000000..83cf506c --- /dev/null +++ b/tests/Unit/UtilitiesTest.php @@ -0,0 +1,227 @@ +getProperty($propertyName); + $property->setAccessible(true); + $property->setValue(null, null); + } + } + + /** + * Test toFloatOrNull returns null for non-numeric values. + */ + public function test_to_float_or_null_returns_null_for_non_numeric(): void + { + $this->assertNull(Utilities::toFloatOrNull('not a number')); + $this->assertNull(Utilities::toFloatOrNull('abc')); + $this->assertNull(Utilities::toFloatOrNull([])); + $this->assertNull(Utilities::toFloatOrNull(null)); + } + + /** + * Test toFloatOrNull converts numeric strings to float. + */ + public function test_to_float_or_null_converts_numeric_strings(): void + { + $this->assertSame(123.0, Utilities::toFloatOrNull('123')); + $this->assertSame(123.45, Utilities::toFloatOrNull('123.45')); + $this->assertSame(-45.67, Utilities::toFloatOrNull('-45.67')); + } + + /** + * Test toFloatOrNull converts integers to float. + */ + public function test_to_float_or_null_converts_integers(): void + { + $this->assertSame(123.0, Utilities::toFloatOrNull(123)); + $this->assertSame(0.0, Utilities::toFloatOrNull(0)); + $this->assertSame(-456.0, Utilities::toFloatOrNull(-456)); + } + + /** + * Test toFloatOrNull with rounding. + */ + public function test_to_float_or_null_with_rounding(): void + { + $this->assertSame(123.46, Utilities::toFloatOrNull(123.456, 2)); + $this->assertSame(123.5, Utilities::toFloatOrNull(123.456, 1)); + $this->assertSame(123.0, Utilities::toFloatOrNull(123.456, 0)); + } + + /** + * Test dateTimeNow returns DateTimeImmutable. + */ + public function test_date_time_now_returns_datetime_immutable(): void + { + $now = Utilities::dateTimeNow(); + $this->assertInstanceOf(DateTimeImmutable::class, $now); + } + + /** + * Test dateTimeNow caches the result. + */ + public function test_date_time_now_caches_result(): void + { + $first = Utilities::dateTimeNow(); + $second = Utilities::dateTimeNow(); + + // Should be the exact same instance due to caching + $this->assertSame($first, $second); + } + + /** + * Test dateTimeTodayAtMidnight returns midnight. + */ + public function test_date_time_today_at_midnight(): void + { + $midnight = Utilities::dateTimeTodayAtMidnight(); + + $this->assertInstanceOf(DateTimeImmutable::class, $midnight); + $this->assertSame('00:00', $midnight->format('H:i')); + } + + /** + * Test dateTimeNowPlus1D is approximately one day in the future. + */ + public function test_date_time_now_plus_1d(): void + { + $now = Utilities::dateTimeNow(); + $tomorrow = Utilities::dateTimeNowPlus1D(); + + $diff = $tomorrow->getTimestamp() - $now->getTimestamp(); + + // Should be approximately 86400 seconds (1 day) + $this->assertEqualsWithDelta(86400, $diff, 1); + } + + /** + * Test dateTimeNowPlus90D is approximately 90 days in the future. + */ + public function test_date_time_now_plus_90d(): void + { + $now = Utilities::dateTimeNow(); + $future = Utilities::dateTimeNowPlus90D(); + + $diff = $future->getTimestamp() - $now->getTimestamp(); + + // Should be approximately 7776000 seconds (90 days) + $this->assertEqualsWithDelta(7776000, $diff, 1); + } + + /** + * Test dateTimeNowPlus1Y is approximately one year in the future. + */ + public function test_date_time_now_plus_1y(): void + { + $now = Utilities::dateTimeNow(); + $nextYear = Utilities::dateTimeNowPlus1Y(); + + $diff = $nextYear->getTimestamp() - $now->getTimestamp(); + + // Should be approximately 31536000 seconds (365 days) + // Allow for leap years + $this->assertEqualsWithDelta(31536000, $diff, 86400); + } + + /** + * Test dateTimeNowMinus1D is approximately one day in the past. + */ + public function test_date_time_now_minus_1d(): void + { + $now = Utilities::dateTimeNow(); + $yesterday = Utilities::dateTimeNowMinus1D(); + + $diff = $now->getTimestamp() - $yesterday->getTimestamp(); + + // Should be approximately 86400 seconds (1 day) + $this->assertEqualsWithDelta(86400, $diff, 1); + } + + /** + * Test utcTimeZone returns UTC timezone. + */ + public function test_utc_time_zone(): void + { + $utc = Utilities::utcTimeZone(); + + $this->assertInstanceOf(DateTimeZone::class, $utc); + $this->assertSame('UTC', $utc->getName()); + } + + /** + * Test utcTimeZone caches the result. + */ + public function test_utc_time_zone_caches_result(): void + { + $first = Utilities::utcTimeZone(); + $second = Utilities::utcTimeZone(); + + // Should be the exact same instance due to caching + $this->assertSame($first, $second); + } + + /** + * Test getPluralDayOfWeekNameForNumber_noI18n returns correct day names. + */ + public function test_get_plural_day_of_week_name_for_number_no_i18n(): void + { + $this->assertSame('Sundays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(0)); + $this->assertSame('Mondays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(1)); + $this->assertSame('Tuesdays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(2)); + $this->assertSame('Wednesdays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(3)); + $this->assertSame('Thursdays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(4)); + $this->assertSame('Fridays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(5)); + $this->assertSame('Saturdays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(6)); + } + + /** + * Test getPluralDayOfWeekNameForNumber_noI18n handles modulo correctly. + */ + public function test_get_plural_day_of_week_name_handles_modulo(): void + { + // Test that numbers > 6 wrap around + $this->assertSame('Sundays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(7)); + $this->assertSame('Mondays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(8)); + $this->assertSame('Sundays', Utilities::getPluralDayOfWeekNameForNumber_noI18n(14)); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..bfb1e862 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,198 @@ +errors[$code][] = $message; + + if (!empty($data)) { + $this->error_data[$code] = $data; + } + } + + public function get_error_code() { + $codes = array_keys($this->errors); + return empty($codes) ? '' : $codes[0]; + } + + public function get_error_message($code = '') { + if (empty($code)) { + $code = $this->get_error_code(); + } + return isset($this->errors[$code]) ? $this->errors[$code][0] : ''; + } + } +} + +if (!defined('HOUR_IN_SECONDS')) { + define('HOUR_IN_SECONDS', 3600); +} + +if (!defined('DAY_IN_SECONDS')) { + define('DAY_IN_SECONDS', 86400); +} + +if (!defined('WEEK_IN_SECONDS')) { + define('WEEK_IN_SECONDS', 604800); +} + +if (!defined('MONTH_IN_SECONDS')) { + define('MONTH_IN_SECONDS', 2592000); +} + +if (!defined('YEAR_IN_SECONDS')) { + define('YEAR_IN_SECONDS', 31536000); +} + +// Autoload Yoast PHPUnit Polyfills +require_once dirname(__DIR__) . '/vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php'; From 3c89a58a5174d97f345b90f71e8256dc1e2a7790 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:05:05 +0000 Subject: [PATCH 05/83] Add comprehensive tests for StringableArray utility class Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com> --- tests/Unit/Utilities/StringableArrayTest.php | 228 +++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 tests/Unit/Utilities/StringableArrayTest.php diff --git a/tests/Unit/Utilities/StringableArrayTest.php b/tests/Unit/Utilities/StringableArrayTest.php new file mode 100644 index 00000000..05cf1fae --- /dev/null +++ b/tests/Unit/Utilities/StringableArrayTest.php @@ -0,0 +1,228 @@ +assertInstanceOf(StringableArray::class, $array); + $this->assertSame(0, $array->count()); + } + + /** + * Test that StringableArray can be instantiated with custom separator. + */ + public function test_instantiation_with_custom_separator(): void + { + $separator = ', '; + $array = new StringableArray($separator); + + $this->assertInstanceOf(StringableArray::class, $array); + } + + /** + * Test that StringableArray can be instantiated with initial data. + */ + public function test_instantiation_with_initial_data(): void + { + $data = ['apple', 'banana', 'cherry']; + $array = new StringableArray(', ', $data); + + $this->assertSame(3, $array->count()); + } + + /** + * Test count method returns correct count. + */ + public function test_count_returns_correct_count(): void + { + $data = ['one', 'two', 'three']; + $array = new StringableArray(', ', $data); + + $this->assertSame(3, $array->count()); + $this->assertCount(3, $array); + } + + /** + * Test append adds items to the end. + */ + public function test_append_adds_items_to_end(): void + { + $array = new StringableArray(', ', ['first']); + $array->append('second'); + $array->append('third'); + + $this->assertSame(3, $array->count()); + $this->assertSame('first, second, third', $array->join()); + } + + /** + * Test prepend adds items to the beginning. + */ + public function test_prepend_adds_items_to_beginning(): void + { + $array = new StringableArray(', ', ['second']); + $array->prepend('first'); + + $this->assertSame(2, $array->count()); + $this->assertSame('first, second', $array->join()); + } + + /** + * Test prepend with key. + */ + public function test_prepend_with_key(): void + { + $array = new StringableArray(', ', ['b' => 'second']); + $array->prepend('first', 'a'); + + $this->assertSame(2, $array->count()); + $this->assertSame('first, second', $array->join()); + } + + /** + * Test contains returns true for existing items. + */ + public function test_contains_returns_true_for_existing_items(): void + { + $array = new StringableArray(', ', ['apple', 'banana', 'cherry']); + + $this->assertTrue($array->contains('apple')); + $this->assertTrue($array->contains('banana')); + $this->assertTrue($array->contains('cherry')); + } + + /** + * Test contains returns false for non-existing items. + */ + public function test_contains_returns_false_for_non_existing_items(): void + { + $array = new StringableArray(', ', ['apple', 'banana', 'cherry']); + + $this->assertFalse($array->contains('orange')); + $this->assertFalse($array->contains('grape')); + } + + /** + * Test join with default separator. + */ + public function test_join_with_default_separator(): void + { + $array = new StringableArray(', ', ['apple', 'banana', 'cherry']); + + $this->assertSame('apple, banana, cherry', $array->join()); + } + + /** + * Test join with custom separator. + */ + public function test_join_with_custom_separator(): void + { + $array = new StringableArray(', ', ['apple', 'banana', 'cherry']); + + $this->assertSame('apple | banana | cherry', $array->join(' | ')); + } + + /** + * Test toString uses default separator. + */ + public function test_to_string_uses_default_separator(): void + { + $array = new StringableArray(' - ', ['one', 'two', 'three']); + + $this->assertSame('one - two - three', (string)$array); + } + + /** + * Test empty array joins to empty string. + */ + public function test_empty_array_joins_to_empty_string(): void + { + $array = new StringableArray(', '); + + $this->assertSame('', $array->join()); + $this->assertSame('', (string)$array); + } + + /** + * Test single item array. + */ + public function test_single_item_array(): void + { + $array = new StringableArray(', ', ['only']); + + $this->assertSame('only', $array->join()); + $this->assertSame(1, $array->count()); + } + + /** + * Test array with numeric values. + */ + public function test_array_with_numeric_values(): void + { + $array = new StringableArray(', ', [1, 2, 3, 4, 5]); + + $this->assertSame('1, 2, 3, 4, 5', $array->join()); + } + + /** + * Test array operations don't affect original separator. + */ + public function test_operations_preserve_original_separator(): void + { + $array = new StringableArray(' | ', ['a', 'b']); + + // Join with different separator + $this->assertSame('a, b', $array->join(', ')); + + // Original separator should still work + $this->assertSame('a | b', $array->join()); + $this->assertSame('a | b', (string)$array); + } + + /** + * Test StringableArray extends ArrayObject. + */ + public function test_extends_array_object(): void + { + $array = new StringableArray(); + + $this->assertInstanceOf(\ArrayObject::class, $array); + } + + /** + * Test array access operations. + */ + public function test_array_access_operations(): void + { + $array = new StringableArray(', ', ['a', 'b', 'c']); + + // Access + $this->assertSame('a', $array[0]); + $this->assertSame('b', $array[1]); + $this->assertSame('c', $array[2]); + + // Modification + $array[1] = 'modified'; + $this->assertSame('a, modified, c', $array->join()); + } +} From 114602168add0aa5817009fa531b5403876cffe9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:06:25 +0000 Subject: [PATCH 06/83] Fix: Add explicit permissions to GitHub Actions workflow for security Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com> --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b183123..5867eda4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,6 +8,9 @@ on: branches: - "*" +permissions: + contents: read + jobs: test: name: Run PHPUnit Tests From 3aa075e7f505c52fd0a9e794503178bb3b487c53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:13:59 +0000 Subject: [PATCH 07/83] Replace manual WordPress mocks with Brain Monkey and add integration tests Co-authored-by: jkrrv <1238391+jkrrv@users.noreply.github.com> --- TESTING.md | 109 ++++++++- composer.json | 3 +- .../ExceptionUtilitiesIntegrationTest.php | 224 ++++++++++++++++++ .../GeoUtilitiesIntegrationTest.php | 193 +++++++++++++++ tests/TestCase.php | 60 ++++- tests/bootstrap.php | 158 +----------- tests/mocks/WP_Error.php | 102 ++++++++ 7 files changed, 684 insertions(+), 165 deletions(-) create mode 100644 tests/Integration/ExceptionUtilitiesIntegrationTest.php create mode 100644 tests/Integration/GeoUtilitiesIntegrationTest.php create mode 100644 tests/mocks/WP_Error.php diff --git a/TESTING.md b/TESTING.md index 0727d63d..c6b22157 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2,6 +2,14 @@ This document describes how to run and write tests for the TouchPoint-WP plugin. +## Test Infrastructure + +This project uses: +- **PHPUnit 9.6** for testing framework +- **Brain Monkey** for WordPress function and filter mocking +- **Mockery** for general mocking capabilities +- **Yoast PHPUnit Polyfills** for PHP 8.0+ compatibility + ## Running Tests ### Prerequisites @@ -19,7 +27,7 @@ composer install ### Running the Test Suite -To run all tests: +To run all tests (unit and integration): ```bash composer test @@ -33,6 +41,18 @@ Or directly with PHPUnit: ### Running Specific Tests +To run only unit tests: + +```bash +./vendor/bin/phpunit tests/Unit +``` + +To run only integration tests: + +```bash +./vendor/bin/phpunit tests/Integration +``` + To run tests in a specific file: ```bash @@ -62,24 +82,25 @@ This will create an HTML coverage report in the `coverage/` directory. Open `cov Tests are organized in the `tests/` directory: - `tests/Unit/` - Unit tests for individual classes and methods -- `tests/Integration/` - Integration tests (if needed) -- `tests/TestCase.php` - Base test case class that all tests should extend +- `tests/Integration/` - Integration tests that test multiple components working together +- `tests/TestCase.php` - Base test case class with Brain Monkey integration - `tests/bootstrap.php` - Bootstrap file that sets up the test environment +- `tests/mocks/` - Mock implementations of WordPress classes -### Creating a New Test +### Test Types -1. Create a new test file in the appropriate directory (e.g., `tests/Unit/MyClassTest.php`) -2. Extend the `tp\TouchPointWP\Tests\TestCase` class -3. Add the `@covers` annotation to specify which class you're testing -4. Write test methods (must start with `test_` or use the `@test` annotation) +#### Unit Tests + +Unit tests focus on testing individual methods and classes in isolation. They use Brain Monkey to mock WordPress functions. -Example: +Example unit test: ```php justReturn('test_value'); + $instance = new MyClass(); $result = $instance->myMethod(); @@ -98,6 +122,73 @@ class MyClassTest extends TestCase } ``` +#### Integration Tests + +Integration tests verify that multiple components work together correctly. + +Example integration test: + +```php +processWithB($classB); + + $this->assertTrue($result); + } +} +``` + +### Mocking WordPress Functions with Brain Monkey + +Brain Monkey provides elegant WordPress function mocking: + +```php +use Brain\Monkey; + +// Simple return value +Monkey\Functions\when('get_option')->justReturn('value'); + +// Return argument unchanged (useful for escaping functions) +Monkey\Functions\when('esc_html')->returnArg(); + +// Custom callback +Monkey\Functions\when('apply_filters')->alias(function($tag, $value) { + return $value; +}); + +// Expect a function to be called +Monkey\Functions\expect('wp_enqueue_script') + ->once() + ->with('my-script', 'path/to/script.js'); +``` + +### Creating a New Test + +1. Create a new test file in the appropriate directory: + - `tests/Unit/` for unit tests + - `tests/Integration/` for integration tests +2. Extend the `tp\TouchPointWP\Tests\TestCase` class +3. Add the `@covers` annotation to specify which class(es) you're testing +4. Write test methods (must start with `test_` or use the `@test` annotation) +5. Use Brain Monkey to mock WordPress functions as needed + ### Test Naming Conventions - Test files should be named `{ClassName}Test.php` diff --git a/composer.json b/composer.json index fd35a3d6..696973aa 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "skayo/phpdoc-md": "^0.2.0", "phpunit/phpunit": "^9.5", "mockery/mockery": "^1.5", - "yoast/phpunit-polyfills": "^1.0" + "yoast/phpunit-polyfills": "^1.0", + "brain/monkey": "^2.6" }, "config": { "allow-plugins": { diff --git a/tests/Integration/ExceptionUtilitiesIntegrationTest.php b/tests/Integration/ExceptionUtilitiesIntegrationTest.php new file mode 100644 index 00000000..42f22901 --- /dev/null +++ b/tests/Integration/ExceptionUtilitiesIntegrationTest.php @@ -0,0 +1,224 @@ +assertSame('Test exception with timestamp', $e->getMessage()); + $this->assertSame(500, $e->getCode()); + + // Verify timestamps (should be same due to caching) + $this->assertSame($beforeException, $afterException); + } + } + + /** + * Test exception JSON output includes proper error structure. + */ + public function test_exception_json_format_validation(): void + { + $exception = new TouchPointWP_Exception('Data validation failed', 400); + $json = $exception->toJson(); + + // Verify JSON is valid + $this->assertIsString($json); + $decoded = json_decode($json, true); + $this->assertIsArray($decoded); + + // Verify structure + $this->assertArrayHasKey('error', $decoded); + $this->assertArrayHasKey('status', $decoded['error']); + $this->assertArrayHasKey('code', $decoded['error']); + $this->assertArrayHasKey('message', $decoded['error']); + + // Verify values + $this->assertSame('failure', $decoded['error']['status']); + $this->assertSame(400, $decoded['error']['code']); + $this->assertSame('Data validation failed', $decoded['error']['message']); + } + + /** + * Test exception chaining with timezone conversions. + */ + public function test_exception_chaining_with_timezone_info(): void + { + $utcZone = Utilities::utcTimeZone(); + $this->assertSame('UTC', $utcZone->getName()); + + $originalException = new \Exception('Database connection failed'); + $wrappedException = new TouchPointWP_Exception( + 'Unable to fetch data', + 503, + $originalException + ); + + $this->assertSame('Unable to fetch data', $wrappedException->getMessage()); + $this->assertSame(503, $wrappedException->getCode()); + $this->assertNotNull($wrappedException->getPrevious()); + $this->assertSame('Database connection failed', $wrappedException->getPrevious()->getMessage()); + } + + /** + * Test exception handling in a workflow with multiple utility calls. + */ + public function test_exception_in_data_processing_workflow(): void + { + $startTime = Utilities::dateTimeNow(); + $errors = []; + + // Simulate data processing with potential errors + $dataPoints = [ + ['value' => '123.45', 'valid' => true], + ['value' => 'invalid', 'valid' => false], + ['value' => '67.89', 'valid' => true], + ]; + + foreach ($dataPoints as $index => $data) { + try { + if (!$data['valid']) { + throw new TouchPointWP_Exception( + "Invalid data at index {$index}", + 400 + ); + } + + $converted = Utilities::toFloatOrNull($data['value']); + $this->assertIsFloat($converted); + } catch (TouchPointWP_Exception $e) { + $errors[] = [ + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'json' => $e->toJson(), + ]; + } + } + + $endTime = Utilities::dateTimeNow(); + + // Verify we caught exactly one error + $this->assertCount(1, $errors); + $this->assertSame('Invalid data at index 1', $errors[0]['message']); + $this->assertSame(400, $errors[0]['code']); + + // Verify timestamps + $this->assertSame($startTime, $endTime); + } + + /** + * Test WP_Error conversion with utility date formatting. + */ + public function test_wp_error_conversion_with_utilities(): void + { + $exception = new TouchPointWP_Exception('Resource not found', 404); + $wpError = $exception->toWpError(); + + $this->assertInstanceOf(\WP_Error::class, $wpError); + $this->assertSame(404, $wpError->get_error_code()); + $this->assertSame('Resource not found', $wpError->get_error_message()); + + // Add timestamp context using utilities + $timestamp = Utilities::dateTimeNow()->getTimestamp(); + $this->assertIsInt($timestamp); + $this->assertGreaterThan(0, $timestamp); + } + + /** + * Test exception with float conversion for numeric error codes. + */ + public function test_exception_with_numeric_utilities(): void + { + // Test with various error scenarios + $testCases = [ + ['input' => '500', 'expected' => 500.0], + ['input' => '404.5', 'expected' => 404.5], + ['input' => 'invalid', 'expected' => null], + ]; + + $exceptionsLogged = []; + + foreach ($testCases as $case) { + $converted = Utilities::toFloatOrNull($case['input']); + $this->assertSame($case['expected'], $converted); + + if ($converted === null) { + try { + throw new TouchPointWP_Exception( + "Invalid numeric value: {$case['input']}", + 422 + ); + } catch (TouchPointWP_Exception $e) { + $exceptionsLogged[] = $e; + } + } + } + + $this->assertCount(1, $exceptionsLogged); + $this->assertStringContainsString('Invalid numeric value: invalid', $exceptionsLogged[0]->getMessage()); + } + + /** + * Test error handling with date range validation. + */ + public function test_exception_with_date_range_validation(): void + { + $today = Utilities::dateTimeTodayAtMidnight(); + $tomorrow = Utilities::dateTimeNowPlus1D(); + $yesterday = Utilities::dateTimeNowMinus1D(); + + // Simulate date validation + $validationErrors = []; + + // Test with invalid date comparison + if ($tomorrow < $today) { + try { + throw new TouchPointWP_Exception('Invalid date range: end before start', 400); + } catch (TouchPointWP_Exception $e) { + $validationErrors[] = $e; + } + } + + // Test with past date + if ($yesterday > $today) { + try { + throw new TouchPointWP_Exception('Cannot schedule in the past', 400); + } catch (TouchPointWP_Exception $e) { + $validationErrors[] = $e; + } + } + + // Should have no validation errors with correct date logic + $this->assertCount(0, $validationErrors); + + // Verify date relationships + $this->assertGreaterThan($tomorrow->getTimestamp(), Utilities::dateTimeNowPlus90D()->getTimestamp()); + $this->assertGreaterThan($yesterday->getTimestamp(), $today->getTimestamp()); + } +} diff --git a/tests/Integration/GeoUtilitiesIntegrationTest.php b/tests/Integration/GeoUtilitiesIntegrationTest.php new file mode 100644 index 00000000..9b93c136 --- /dev/null +++ b/tests/Integration/GeoUtilitiesIntegrationTest.php @@ -0,0 +1,193 @@ +assertInstanceOf(\DateTimeImmutable::class, $now); + + // Use Geo to calculate distance between two points + $distance = Geo::distance(40.7128, -74.0060, 34.0522, -118.2437); + + // Verify the distance calculation is reasonable (NY to LA) + $this->assertGreaterThan(2400, $distance); + $this->assertLessThan(2500, $distance); + + // Verify datetime is available for logging/tracking purposes + $this->assertNotNull($now->getTimestamp()); + } + + /** + * Test creating Geo objects with timezone-aware datetimes from Utilities. + */ + public function test_geo_object_creation_with_timezone_utilities(): void + { + $utcZone = Utilities::utcTimeZone(); + $this->assertInstanceOf(\DateTimeZone::class, $utcZone); + $this->assertSame('UTC', $utcZone->getName()); + + // Create a Geo object representing a location + $location = new Geo(51.5074, -0.1278, 'London, UK', 'city'); + + // Verify the Geo object was created correctly + $this->assertSame(51.5074, $location->lat); + $this->assertSame(-0.1278, $location->lng); + $this->assertSame('London, UK', $location->human); + $this->assertSame('city', $location->type); + + // Get current time in UTC for potential timestamp logging + $currentTime = Utilities::dateTimeNow()->setTimezone($utcZone); + $this->assertInstanceOf(\DateTimeImmutable::class, $currentTime); + } + + /** + * Test calculating distances between multiple points over time. + */ + public function test_multiple_distance_calculations_with_time_tracking(): void + { + $startTime = Utilities::dateTimeNow(); + + // Calculate distances between multiple city pairs + $distances = [ + 'NYC-LA' => Geo::distance(40.7128, -74.0060, 34.0522, -118.2437), + 'London-Paris' => Geo::distance(51.5074, -0.1278, 48.8566, 2.3522), + 'Tokyo-Sydney' => Geo::distance(35.6762, 139.6503, -33.8688, 151.2093), + ]; + + $endTime = Utilities::dateTimeNow(); + + // Verify all distances were calculated + $this->assertCount(3, $distances); + $this->assertArrayHasKey('NYC-LA', $distances); + $this->assertArrayHasKey('London-Paris', $distances); + $this->assertArrayHasKey('Tokyo-Sydney', $distances); + + // Verify distances are reasonable + $this->assertGreaterThan(2000, $distances['NYC-LA']); + $this->assertGreaterThan(200, $distances['London-Paris']); + $this->assertGreaterThan(4800, $distances['Tokyo-Sydney']); + + // Verify timing works (should be same instance due to caching) + $this->assertSame($startTime, $endTime); + } + + /** + * Test converting numeric strings to floats with Geo coordinates. + */ + public function test_utilities_float_conversion_with_geo_coordinates(): void + { + // Convert string coordinates to floats + $lat = Utilities::toFloatOrNull('40.7128', 4); + $lng = Utilities::toFloatOrNull('-74.0060', 4); + + $this->assertSame(40.7128, $lat); + $this->assertSame(-74.0060, $lng); + + // Create Geo object with converted coordinates + $location = new Geo($lat, $lng, 'New York', 'city'); + + // Calculate distance to another location + $distance = Geo::distance( + $location->lat, + $location->lng, + 34.0522, + -118.2437 + ); + + $this->assertGreaterThan(2000, $distance); + } + + /** + * Test day of week utilities integration with geographic location tracking. + */ + public function test_day_of_week_utilities_with_geo_locations(): void + { + // Get day names + $monday = Utilities::getPluralDayOfWeekNameForNumber_noI18n(1); + $this->assertSame('Mondays', $monday); + + // Create locations for a hypothetical event schedule + $locations = [ + new Geo(40.7128, -74.0060, 'NYC Office', 'office'), + new Geo(34.0522, -118.2437, 'LA Office', 'office'), + new Geo(51.5074, -0.1278, 'London Office', 'office'), + ]; + + $this->assertCount(3, $locations); + + // Verify distance between first and last location + $distance = Geo::distance( + $locations[0]->lat, + $locations[0]->lng, + $locations[2]->lat, + $locations[2]->lng + ); + + // NYC to London is approximately 3450 miles + $this->assertGreaterThan(3400, $distance); + $this->assertLessThan(3500, $distance); + } + + /** + * Test date range calculations with geographic data. + */ + public function test_date_range_with_geographic_tracking(): void + { + $today = Utilities::dateTimeTodayAtMidnight(); + $tomorrow = Utilities::dateTimeNowPlus1D(); + $nextWeek = Utilities::dateTimeNowPlus90D(); + + // Verify date progression + $this->assertInstanceOf(\DateTimeImmutable::class, $today); + $this->assertInstanceOf(\DateTimeImmutable::class, $tomorrow); + $this->assertInstanceOf(\DateTimeImmutable::class, $nextWeek); + + // Create a route with multiple stops + $route = [ + ['location' => new Geo(40.7128, -74.0060), 'day' => 0], + ['location' => new Geo(41.8781, -87.6298), 'day' => 1], // Chicago + ['location' => new Geo(34.0522, -118.2437), 'day' => 3], // LA + ]; + + $this->assertCount(3, $route); + + // Calculate total distance of route + $totalDistance = 0; + for ($i = 0; $i < count($route) - 1; $i++) { + $totalDistance += Geo::distance( + $route[$i]['location']->lat, + $route[$i]['location']->lng, + $route[$i + 1]['location']->lat, + $route[$i + 1]['location']->lng + ); + } + + // Total distance from NYC -> Chicago -> LA should be roughly 2400-2600 miles + $this->assertGreaterThan(2300, $totalDistance); + $this->assertLessThan(2700, $totalDistance); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 92286cfa..cfd5e338 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,29 +7,85 @@ namespace tp\TouchPointWP\Tests; +use Brain\Monkey; +use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Yoast\PHPUnitPolyfills\TestCases\TestCase as PolyfillsTestCase; /** * Base test case class that all TouchPoint-WP tests should extend. * Provides common setup and utility methods for testing. + * Uses Brain Monkey for WordPress function mocking. */ abstract class TestCase extends PolyfillsTestCase { + use MockeryPHPUnitIntegration; + /** * Set up before each test. + * Initializes Brain Monkey for WordPress function mocking. */ protected function set_up(): void { parent::set_up(); - // Additional setup can be added here + Monkey\setUp(); + + // Set up common WordPress function defaults + $this->setUpWordPressFunctions(); } /** * Tear down after each test. + * Tears down Brain Monkey. */ protected function tear_down(): void { + Monkey\tearDown(); parent::tear_down(); - // Additional teardown can be added here + } + + /** + * Set up common WordPress function defaults. + * Can be overridden in individual tests as needed. + */ + protected function setUpWordPressFunctions(): void + { + // Mock current_datetime to return a consistent DateTimeImmutable + Monkey\Functions\when('current_datetime')->justReturn( + new \DateTimeImmutable('2025-11-12 21:00:00', new \DateTimeZone('UTC')) + ); + + // Mock is_admin to return false by default + Monkey\Functions\when('is_admin')->justReturn(false); + + // Mock current_user_can to return false by default + Monkey\Functions\when('current_user_can')->justReturn(false); + + // Mock get_option to return default value + Monkey\Functions\when('get_option')->alias(function ($option, $default = false) { + return $default; + }); + + // Mock translation functions to return the text unchanged + Monkey\Functions\when('__')->returnArg(); + Monkey\Functions\when('_e')->returnArg(); + Monkey\Functions\when('_x')->returnArg(); + + // Mock escaping functions + Monkey\Functions\when('esc_html')->alias(function ($text) { + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + }); + Monkey\Functions\when('esc_attr')->alias(function ($text) { + return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + }); + Monkey\Functions\when('esc_url')->returnArg(); + + // Mock sanitization functions + Monkey\Functions\when('sanitize_text_field')->alias(function ($str) { + return trim(strip_tags($str)); + }); + Monkey\Functions\when('wp_kses_post')->returnArg(); + + // Mock apply_filters to return the value unchanged + Monkey\Functions\when('apply_filters')->returnArg(2); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index bfb1e862..a47b35c5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -21,159 +21,7 @@ define('WPINC', 'wp-includes'); } -// Mock WordPress functions that are commonly used but not critical for unit tests -// These can be overridden in individual test files when needed -if (!function_exists('current_datetime')) { - function current_datetime() { - return new DateTimeImmutable('now', new DateTimeZone(date_default_timezone_get())); - } -} - -if (!function_exists('is_admin')) { - function is_admin() { - return false; // In test environment, not in admin - } -} - -if (!function_exists('current_user_can')) { - function current_user_can($capability) { - return false; // In test environment, user has no capabilities - } -} - -if (!function_exists('get_option')) { - function get_option($option, $default = false) { - return $default; - } -} - -if (!function_exists('update_option')) { - function update_option($option, $value, $autoload = null) { - return true; - } -} - -if (!function_exists('delete_option')) { - function delete_option($option) { - return true; - } -} - -if (!function_exists('add_action')) { - function add_action($hook, $callback, $priority = 10, $accepted_args = 1) { - return true; - } -} - -if (!function_exists('add_filter')) { - function add_filter($hook, $callback, $priority = 10, $accepted_args = 1) { - return true; - } -} - -if (!function_exists('apply_filters')) { - function apply_filters($tag, $value, ...$args) { - return $value; - } -} - -if (!function_exists('do_action')) { - function do_action($tag, ...$args) { - return true; - } -} - -if (!function_exists('__')) { - function __($text, $domain = 'default') { - return $text; - } -} - -if (!function_exists('_e')) { - function _e($text, $domain = 'default') { - echo $text; - } -} - -if (!function_exists('esc_html')) { - function esc_html($text) { - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); - } -} - -if (!function_exists('esc_attr')) { - function esc_attr($text) { - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); - } -} - -if (!function_exists('esc_url')) { - function esc_url($url) { - return $url; - } -} - -if (!function_exists('wp_kses_post')) { - function wp_kses_post($data) { - return $data; - } -} - -if (!function_exists('sanitize_text_field')) { - function sanitize_text_field($str) { - return trim(strip_tags($str)); - } -} - -if (!function_exists('wp_parse_args')) { - function wp_parse_args($args, $defaults = []) { - if (is_object($args)) { - $parsed_args = get_object_vars($args); - } elseif (is_array($args)) { - $parsed_args = &$args; - } else { - parse_str($args, $parsed_args); - } - - if (is_array($defaults)) { - return array_merge($defaults, $parsed_args); - } - return $parsed_args; - } -} - -// Mock WP_Error class for testing -if (!class_exists('WP_Error')) { - class WP_Error { - public $errors = []; - public $error_data = []; - - public function __construct($code = '', $message = '', $data = '') { - if (empty($code)) { - return; - } - - $this->errors[$code][] = $message; - - if (!empty($data)) { - $this->error_data[$code] = $data; - } - } - - public function get_error_code() { - $codes = array_keys($this->errors); - return empty($codes) ? '' : $codes[0]; - } - - public function get_error_message($code = '') { - if (empty($code)) { - $code = $this->get_error_code(); - } - return isset($this->errors[$code]) ? $this->errors[$code][0] : ''; - } - } -} - +// Define WordPress time constants if (!defined('HOUR_IN_SECONDS')) { define('HOUR_IN_SECONDS', 3600); } @@ -194,5 +42,9 @@ public function get_error_message($code = '') { define('YEAR_IN_SECONDS', 31536000); } +// Load WordPress class mocks +require_once __DIR__ . '/mocks/WP_Error.php'; + // Autoload Yoast PHPUnit Polyfills require_once dirname(__DIR__) . '/vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php'; + diff --git a/tests/mocks/WP_Error.php b/tests/mocks/WP_Error.php new file mode 100644 index 00000000..03ef3358 --- /dev/null +++ b/tests/mocks/WP_Error.php @@ -0,0 +1,102 @@ +errors[$code][] = $message; + + if (!empty($data)) { + $this->error_data[$code] = $data; + } + } + + /** + * Retrieve first error code available. + * + * @return string|int Empty string if no error codes are available. + */ + public function get_error_code() + { + $codes = array_keys($this->errors); + return empty($codes) ? '' : $codes[0]; + } + + /** + * Retrieve first error message available. + * + * @param string|int $code Error code to retrieve message for. + * @return string Empty string if no error messages are available. + */ + public function get_error_message($code = '') + { + if (empty($code)) { + $code = $this->get_error_code(); + } + return isset($this->errors[$code]) ? $this->errors[$code][0] : ''; + } + + /** + * Retrieve error data for error code. + * + * @param string|int $code Error code. + * @return mixed Null if $code is invalid. + */ + public function get_error_data($code = '') + { + if (empty($code)) { + $code = $this->get_error_code(); + } + return isset($this->error_data[$code]) ? $this->error_data[$code] : null; + } + + /** + * Add an error. + * + * @param string|int $code Error code. + * @param string $message Error message. + * @param mixed $data Error data. + */ + public function add($code, $message, $data = '') + { + $this->errors[$code][] = $message; + if (!empty($data)) { + $this->error_data[$code] = $data; + } + } + } +} From 0715d0f3a03b56855a060fef9d82942d0436390e Mon Sep 17 00:00:00 2001 From: "James K." Date: Thu, 5 Jun 2025 01:03:58 -0400 Subject: [PATCH 08/83] Making global declaration of i18n functions safer. --- assets/js/base-defer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base-defer.js b/assets/js/base-defer.js index 36b3d814..ea1d6000 100644 --- a/assets/js/base-defer.js +++ b/assets/js/base-defer.js @@ -1,7 +1,7 @@ "use strict"; // noinspection JSUnresolvedVariable -const { __, _x, _n, _nx, sprintf } = wp.i18n; +const { __, _x, _n, _nx, sprintf } = typeof __ === "undefined" ? wp.i18n : { __, _x, _n, _nx, sprintf }; function utilInit() { tpvm._utils.stringArrayToListString = function(strings) { From b9e1400657437ef73f0f34641d2474e452ff478e Mon Sep 17 00:00:00 2001 From: "James K." Date: Thu, 5 Jun 2025 12:45:49 -0400 Subject: [PATCH 09/83] Finally correcting JS scoping --- assets/js/base-defer.js | 2551 +++++++++++---------- assets/js/meeting-defer.js | 2 +- assets/js/partner-defer.js | 271 +-- src/TouchPoint-WP/Involvement.php | 4 +- src/TouchPoint-WP/Meeting.php | 2 +- src/TouchPoint-WP/Partner.php | 4 +- src/TouchPoint-WP/Person.php | 2 +- src/TouchPoint-WP/TouchPointWP.php | 7 +- src/js-partials/involvement-map-inline.js | 2 +- src/js-partials/partner-map-inline.js | 2 +- 10 files changed, 1433 insertions(+), 1414 deletions(-) diff --git a/assets/js/base-defer.js b/assets/js/base-defer.js index ea1d6000..cff806d6 100644 --- a/assets/js/base-defer.js +++ b/assets/js/base-defer.js @@ -1,1548 +1,1563 @@ "use strict"; +(function() { + // noinspection JSUnresolvedVariable -const { __, _x, _n, _nx, sprintf } = typeof __ === "undefined" ? wp.i18n : { __, _x, _n, _nx, sprintf }; - -function utilInit() { - tpvm._utils.stringArrayToListString = function(strings) { - let concat = strings.join(''), - comma = ', ', - and = ' & ', - useOxford = false, - last, str; - if (concat.indexOf(', ') !== -1) { - comma = '; '; - useOxford = true; - } - if (concat.indexOf(' & ') !== -1) { - and = ' ' + __('and', 'TouchPoint-WP') + ' '; - useOxford = true; - } - - last = strings.pop(); - str = strings.join(comma); - if (strings.length > 0) { - if (useOxford) - str += comma.trim(); - str += and; - } - str += last; - return str; - } + const {__, sprintf} = wp.i18n; + + function utilInit() { + tpvm._utils.stringArrayToListString = function (strings) { + let concat = strings.join(''), + comma = ', ', + and = ' & ', + useOxford = false, + last, str; + if (concat.indexOf(', ') !== -1) { + comma = '; '; + useOxford = true; + } + if (concat.indexOf(' & ') !== -1) { + and = ' ' + __('and', 'TouchPoint-WP') + ' '; + useOxford = true; + } - /** - * - * @param {string} action The name of the action function, minus the word "action" - * @param {object} object The object to which the action belongs. - */ - tpvm._utils.registerAction = function(action, object) { - if (typeof object[action + "Action"] === "function") { - let sc = object.shortClass; - if (typeof sc !== "string") { - console.warn(`Action '${action}' cannot be registered because the short class name is missing.`) - return; + last = strings.pop(); + str = strings.join(comma); + if (strings.length > 0) { + if (useOxford) + str += comma.trim(); + str += and; } - let actionLC = action.toLowerCase(); - if (!tpvm._actions.hasOwnProperty(actionLC)) { - tpvm._actions[actionLC] = []; + str += last; + return str; + } + + /** + * + * @param {string} action The name of the action function, minus the word "action" + * @param {object} object The object to which the action belongs. + */ + tpvm._utils.registerAction = function (action, object) { + if (typeof object[action + "Action"] === "function") { + let sc = object.shortClass; + if (typeof sc !== "string") { + console.warn(`Action '${action}' cannot be registered because the short class name is missing.`) + return; + } + let actionLC = action.toLowerCase(); + if (!tpvm._actions.hasOwnProperty(actionLC)) { + tpvm._actions[actionLC] = []; + } + tpvm._actions[actionLC].push({ + action: () => object[action + "Action"](), + uid: sc + object.id + }); } - tpvm._actions[actionLC].push({ - action: () => object[action + "Action"](), - uid: sc + object.id - }); } - } - tpvm._utils.applyHashForAction = function(action, object) { - // Make sure a function exists - if (typeof object[action + "Action"] !== "function") { - return; - } + tpvm._utils.applyHashForAction = function (action, object) { + // Make sure a function exists + if (typeof object[action + "Action"] !== "function") { + return; + } - // Figure out the needed hash - action = action.toLowerCase() - if (tpvm._actions[action].length === 1) { - window.location.hash = "tp-" + action; - } else if (tpvm._actions[action].length > 1) { - window.location.hash = "tp-" + action + "-" + object.shortClass + object.id; + // Figure out the needed hash + action = action.toLowerCase() + if (tpvm._actions[action].length === 1) { + window.location.hash = "tp-" + action; + } else if (tpvm._actions[action].length > 1) { + window.location.hash = "tp-" + action + "-" + object.shortClass + object.id; + } } - } - tpvm._utils.clearHash = function() { - if (!!window.history) { - window.history.pushState("", "", `${window.location.pathname}${window.location.search}`) - } else { - window.location.hash = ""; + tpvm._utils.clearHash = function () { + if (!!window.history) { + window.history.pushState("", "", `${window.location.pathname}${window.location.search}`) + } else { + window.location.hash = ""; + } } - } - /** - * - * @param {?string} limitToAction - */ - tpvm._utils.handleHash = function(limitToAction = null) { - if (window.location.hash.substring(1, 4) !== "tp-") { - return; - } + /** + * + * @param {?string} limitToAction + */ + tpvm._utils.handleHash = function (limitToAction = null) { + if (window.location.hash.substring(1, 4) !== "tp-") { + return; + } - let [action, identifier] = window.location.hash.toLowerCase().substring(4).split('-', 2); + let [action, identifier] = window.location.hash.toLowerCase().substring(4).split('-', 2); - if (tpvm._actions[action] === undefined || (limitToAction !== null && action !== limitToAction.toLowerCase())) { - return; - } - if (tpvm._actions[action].length === 1 && identifier === undefined) { - tpvm._actions[action][0].action(); - return; - } + if (tpvm._actions[action] === undefined || (limitToAction !== null && action !== limitToAction.toLowerCase())) { + return; + } + if (tpvm._actions[action].length === 1 && identifier === undefined) { + tpvm._actions[action][0].action(); + return; + } - let obj = tpvm._actions[action].find((t) => t.uid === identifier); - if (obj !== undefined && typeof obj.action === "function") { - obj.action(); + let obj = tpvm._actions[action].find((t) => t.uid === identifier); + if (obj !== undefined && typeof obj.action === "function") { + obj.action(); + } } - } - tpvm.addEventListener("load", tpvm._utils.handleHash); + tpvm.addEventListener("load", tpvm._utils.handleHash); - tpvm._utils.defaultSwalClasses = function() { - return { - container: 'tp-swal-container' + tpvm._utils.defaultSwalClasses = function () { + return { + container: 'tp-swal-container' + } } - } - tpvm._utils.arrayAdd = function (a, b) { - if (a.length !== b.length) { - console.error("Array lengths do not match."); - return; - } - for (const ai in a) { - a[ai] += b[ai]; + tpvm._utils.arrayAdd = function (a, b) { + if (a.length !== b.length) { + console.error("Array lengths do not match."); + return; + } + for (const ai in a) { + a[ai] += b[ai]; + } + return a; + } + + tpvm._utils.averageColor = function (arr) { + let components = [0, 0, 0, 0], + useAlpha = false, + denominator = 0; + for (const ai in arr) { + arr[ai] = arr[ai].replace(';', '').trim(); + if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 4) { // #abc + components = tpvm._utils.arrayAdd(components, [ + parseInt(arr[ai][1] + arr[ai][1], 16), + parseInt(arr[ai][2] + arr[ai][2], 16), + parseInt(arr[ai][3] + arr[ai][3], 16), + 255]); + denominator++; + } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 5) { // #abcd + components = tpvm._utils.arrayAdd(components, [ + parseInt(arr[ai][1] + arr[ai][1], 16), + parseInt(arr[ai][2] + arr[ai][2], 16), + parseInt(arr[ai][3] + arr[ai][3], 16), + parseInt(arr[ai][4] + arr[ai][4], 16)]); + useAlpha = true; + denominator++; + } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 7) { // #aabbcc + components = tpvm._utils.arrayAdd(components, [ + parseInt(arr[ai][1] + arr[ai][2], 16), + parseInt(arr[ai][3] + arr[ai][4], 16), + parseInt(arr[ai][5] + arr[ai][6], 16), + 255]); + denominator++; + } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 9) { // #aabbccdd + components = tpvm._utils.arrayAdd(components, [ + parseInt(arr[ai][1] + arr[ai][2], 16), + parseInt(arr[ai][3] + arr[ai][4], 16), + parseInt(arr[ai][5] + arr[ai][6], 16), + parseInt(arr[ai][7] + arr[ai][8], 16)]); + useAlpha = true; + denominator++; + } else { + console.error("Can't calculate the color for " + arr[ai]); + } + } + if (!useAlpha) { + components.pop(); + } + for (const ci in components) { + components[ci] = Math.round(components[ci] / denominator).toString(16); // convert to hex + components[ci] = ("00" + components[ci]).slice(-2); // pad, just in case there's only one digit. + } + return "#" + components.join(''); } - return a; - } - tpvm._utils.averageColor = function (arr) { - let components = [0, 0, 0, 0], - useAlpha = false, - denominator = 0; - for (const ai in arr) { - arr[ai] = arr[ai].replace(';', '').trim(); - if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 4) { // #abc - components = tpvm._utils.arrayAdd(components, [ - parseInt(arr[ai][1] + arr[ai][1], 16), - parseInt(arr[ai][2] + arr[ai][2], 16), - parseInt(arr[ai][3] + arr[ai][3], 16), - 255]); - denominator++; - } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 5) { // #abcd - components = tpvm._utils.arrayAdd(components, [ - parseInt(arr[ai][1] + arr[ai][1], 16), - parseInt(arr[ai][2] + arr[ai][2], 16), - parseInt(arr[ai][3] + arr[ai][3], 16), - parseInt(arr[ai][4] + arr[ai][4], 16)]); - useAlpha = true; - denominator++; - } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 7) { // #aabbcc - components = tpvm._utils.arrayAdd(components, [ - parseInt(arr[ai][1] + arr[ai][2], 16), - parseInt(arr[ai][3] + arr[ai][4], 16), - parseInt(arr[ai][5] + arr[ai][6], 16), - 255]); - denominator++; - } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 9) { // #aabbccdd - components = tpvm._utils.arrayAdd(components, [ - parseInt(arr[ai][1] + arr[ai][2], 16), - parseInt(arr[ai][3] + arr[ai][4], 16), - parseInt(arr[ai][5] + arr[ai][6], 16), - parseInt(arr[ai][7] + arr[ai][8], 16)]); - useAlpha = true; - denominator++; - } else { - console.error("Can't calculate the color for " + arr[ai]); + tpvm._utils.ga = function (command, hitType, category, action, label = null, value = null) { + if (typeof ga === "function") { + ga(command, hitType, category, action, label, value); + } + if (typeof gtag === "function") { + gtag(hitType, action, { + 'event_category': category, + 'event_label': label, + 'value': value + }); } } - if (!useAlpha) { - components.pop(); - } - for (const ci in components) { - components[ci] = Math.round(components[ci] / denominator).toString(16); // convert to hex - components[ci] = ("00" + components[ci]).slice(-2); // pad, just in case there's only one digit. - } - return "#" + components.join(''); } - tpvm._utils.ga = function(command, hitType, category, action, label = null, value = null) { - if (typeof ga === "function") { - ga(command, hitType, category, action, label, value); + utilInit(); + + class TP_DataGeo { + static loc = { + "lat": null, + "lng": null, + "type": null, + "human": __('Loading...', 'TouchPoint-WP') + }; + + get shortClass() { + return "geo"; } - if (typeof gtag === "function") { - gtag(hitType, action, { - 'event_category': category, - 'event_label': label, - 'value': value - }); + + static init() { + tpvm.trigger('dataGeo_class_loaded'); } - } -} -utilInit(); - -class TP_DataGeo { - static loc = { - "lat": null, - "lng": null, - "type": null, - "human": __('Loading...', 'TouchPoint-WP') - }; - - get shortClass() { - return "geo"; - } - static init() { - tpvm.trigger('dataGeo_class_loaded'); - } + static geoByNavigator(then = null, error = null) { + navigator.geolocation.getCurrentPosition(geo, err); - static geoByNavigator(then = null, error = null) { - navigator.geolocation.getCurrentPosition(geo, err); + function geo(pos) { + TP_DataGeo.loc = { + "lat": pos.coords.latitude, + "lng": pos.coords.longitude, + "type": "nav", + "permission": null, + "human": __('Your Location', 'TouchPoint-WP') + } - function geo(pos) { - TP_DataGeo.loc = { - "lat": pos.coords.latitude, - "lng": pos.coords.longitude, - "type": "nav", - "permission": null, - "human": __('Your Location', 'TouchPoint-WP') - } + if (then !== null) { + then(TP_DataGeo.loc) + } - if (then !== null) { - then(TP_DataGeo.loc) + tpvm.trigger("dataGeo_located", TP_DataGeo.loc) } - tpvm.trigger("dataGeo_located", TP_DataGeo.loc) - } + function err(e) { + let userFacingMessage; - function err(e) { - let userFacingMessage; + if (error !== null) { + error(e) + } - if (error !== null) { - error(e) - } + console.error(e); - console.error(e); + switch (e.code) { + case e.PERMISSION_DENIED: + userFacingMessage = __("User denied the request for Geolocation.", 'TouchPoint-WP'); + break; - switch(e.code) { - case e.PERMISSION_DENIED: - userFacingMessage = __("User denied the request for Geolocation.", 'TouchPoint-WP'); - break; + case e.POSITION_UNAVAILABLE: + userFacingMessage = __("Location information is unavailable.", 'TouchPoint-WP'); + break; - case e.POSITION_UNAVAILABLE: - userFacingMessage = __("Location information is unavailable.", 'TouchPoint-WP'); - break; + case e.TIMEOUT: + userFacingMessage = __("The request to get user location timed out.", 'TouchPoint-WP'); + break; - case e.TIMEOUT: - userFacingMessage = __("The request to get user location timed out.", 'TouchPoint-WP'); - break; + default: + userFacingMessage = __("An unknown error occurred.", 'TouchPoint-WP'); + break; + } - default: - userFacingMessage = __("An unknown error occurred.", 'TouchPoint-WP'); - break; + tpvm.trigger("dataGeo_error", userFacingMessage) } - - tpvm.trigger("dataGeo_error", userFacingMessage) } - } - /** - * Get the user's location. - * - * @param then function Callback for when the location is available. - * @param error function Callback for an error. (Error data structure may vary.) - * @param type string Type of fetching to use. "nav", "ip" or "both" - */ - static getLocation(then, error, type = "both") { - if (type === "both") { - type = ["nav", "ip"]; - } else { - type = [type]; - } - - tpvm.addEventListener("dataGeo_located", then); - - // navigator is preferred if available and allowed. - if (navigator.geolocation && navigator.permissions && type.indexOf("nav") > -1) { - navigator.permissions.query({name: 'geolocation'}).then( - function(PermissionStatus) { - TP_DataGeo.loc.permission = PermissionStatus.state; - if (PermissionStatus.state === 'granted') { - return TP_DataGeo.geoByNavigator(null, error); - } else { - // Fallback to Server - if (type.indexOf("ip") > -1) { - return TP_DataGeo.geoByServer(null, error); + /** + * Get the user's location. + * + * @param then function Callback for when the location is available. + * @param error function Callback for an error. (Error data structure may vary.) + * @param type string Type of fetching to use. "nav", "ip" or "both" + */ + static getLocation(then, error, type = "both") { + if (type === "both") { + type = ["nav", "ip"]; + } else { + type = [type]; + } + + tpvm.addEventListener("dataGeo_located", then); + + // navigator is preferred if available and allowed. + if (navigator.geolocation && navigator.permissions && type.indexOf("nav") > -1) { + navigator.permissions.query({name: 'geolocation'}).then( + function (PermissionStatus) { + TP_DataGeo.loc.permission = PermissionStatus.state; + if (PermissionStatus.state === 'granted') { + return TP_DataGeo.geoByNavigator(null, error); } else { - error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')}); + // Fallback to Server + if (type.indexOf("ip") > -1) { + return TP_DataGeo.geoByServer(null, error); + } else { + error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')}); + } } } - } - ) - } else { - // Fallback to Server - if (type.indexOf("ip") > -1) { - return TP_DataGeo.geoByServer(null, error); + ) } else { - error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')}); + // Fallback to Server + if (type.indexOf("ip") > -1) { + return TP_DataGeo.geoByServer(null, error); + } else { + error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')}); + } } } - } - static geoByServer(then, error) { - tpvm.getData('geolocate').then(function (responseData) { - if (responseData.hasOwnProperty("error")) { - error(responseData.error) - tpvm.trigger("dataGeo_error", responseData.error) - } else { - for (const di in responseData) { - if (responseData.hasOwnProperty(di)) - TP_DataGeo.loc[di] = responseData[di]; - } + static geoByServer(then, error) { + tpvm.getData('geolocate').then(function (responseData) { + if (responseData.hasOwnProperty("error")) { + error(responseData.error) + tpvm.trigger("dataGeo_error", responseData.error) + } else { + for (const di in responseData) { + if (responseData.hasOwnProperty(di)) + TP_DataGeo.loc[di] = responseData[di]; + } - if (then !== null) { - then(TP_DataGeo.loc) - } + if (then !== null) { + then(TP_DataGeo.loc) + } - tpvm.trigger("dataGeo_located", TP_DataGeo.loc) + tpvm.trigger("dataGeo_located", TP_DataGeo.loc) + } + }, error); + } + } + tpvm.TP_DataGeo = TP_DataGeo; + TP_DataGeo.init(); + + class TP_MapMarker { + /** + * + * @type {TP_Mappable[]} + */ + items = []; + + color = "#000"; + + geoStr = ""; + + /** + * @type {google.maps.Marker} + */ + gMkr = null; + + constructor(options) { + if (!options.hasOwnProperty('icon')) { + options.icon = { + path: "M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z", // from FontAwesome + fillColor: options.color ?? "#000", + fillOpacity: .85, + anchor: new google.maps.Point(172.268, 501.67), + strokeWeight: 1, + scale: 0.04, + labelOrigin: new google.maps.Point(190, 198) + } } - }, error); - } -} -TP_DataGeo.init(); - -class TP_MapMarker -{ - /** - * - * @type {TP_Mappable[]} - */ - items = []; - - color = "#000"; - - geoStr = ""; - - /** - * @type {google.maps.Marker} - */ - gMkr = null; - - constructor(options) { - if (!options.hasOwnProperty('icon')) { - options.icon = { - path: "M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z", // from FontAwesome - fillColor: options.color ?? "#000", - fillOpacity: .85, - anchor: new google.maps.Point(172.268, 501.67), - strokeWeight: 1, - scale: 0.04, - labelOrigin: new google.maps.Point(190, 198) - } - } - this.gMkr = new google.maps.Marker(options); - let that = this; - this.gMkr.addListener("click", () => that.handleClick()); - } - - get visibleItems() { - return this.items.filter((i) => i._visible); - } + this.gMkr = new google.maps.Marker(options); + let that = this; + this.gMkr.addListener("click", () => that.handleClick()); + } - get visible() { - return this.visibleItems.length > 0 - } + get visibleItems() { + return this.items.filter((i) => i._visible); + } - get inBounds() { - let map = this.gMkr.getMap(); - if (!map) { // if map failed to render for some reason, this prevents entries from being hidden. - return true; + get visible() { + return this.visibleItems.length > 0 } - return map.getBounds().contains(this.gMkr.getPosition()); - } - get useIcon() { - let icon = this.visibleItems.find((i) => i.useIcon !== false) - if (icon === undefined) { - return false; + get inBounds() { + let map = this.gMkr.getMap(); + if (!map) { // if map failed to render for some reason, this prevents entries from being hidden. + return true; + } + return map.getBounds().contains(this.gMkr.getPosition()); } - return icon.useIcon; - } - updateLabel(highlighted = false) { - if (this.gMkr === null) { - return; + get useIcon() { + let icon = this.visibleItems.find((i) => i.useIcon !== false) + if (icon === undefined) { + return false; + } + return icon.useIcon; } - let icon = this.gMkr.getIcon(); + updateLabel(highlighted = false) { + if (this.gMkr === null) { + return; + } - // Update icon color - this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color)) - if (icon !== undefined && icon.hasOwnProperty("fillColor")) { - icon.fillColor = this.color; - this.gMkr.setIcon(icon); - } + let icon = this.gMkr.getIcon(); - // Update visibility - this.gMkr.setVisible(this.visibleItems.length > 0); + // Update icon color + this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color)) + if (icon !== undefined && icon.hasOwnProperty("fillColor")) { + icon.fillColor = this.color; + this.gMkr.setIcon(icon); + } - // Update title - this.gMkr.setTitle(tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name))) + // Update visibility + this.gMkr.setVisible(this.visibleItems.length > 0); - // Update label proper - if (highlighted) { - this.gMkr.setLabel(null); // Remove label if highlighted, because labels don't animate. - } else { - this.gMkr.setLabel(this.getLabelContent()); - } - } + // Update title + this.gMkr.setTitle(tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name))) - getLabelContent() { - let label = null; - if (this.visibleItems.length > 1) { - label = { - text: this.visibleItems.length.toString(), - color: "#000000", - fontSize: "100%" + // Update label proper + if (highlighted) { + this.gMkr.setLabel(null); // Remove label if highlighted, because labels don't animate. + } else { + this.gMkr.setLabel(this.getLabelContent()); } - } else if (this.useIcon !== false) { // icon for secure partners - label = this.useIcon; } - return label; - } - // noinspection JSUnusedGlobalSymbols Used dynamically from markers. - handleClick() { - if (this.gMkr === null) { - return; + getLabelContent() { + let label = null; + if (this.visibleItems.length > 1) { + label = { + text: this.visibleItems.length.toString(), + color: "#000000", + fontSize: "100%" + } + } else if (this.useIcon !== false) { // icon for secure partners + label = this.useIcon; + } + return label; } - tpvm._utils.clearHash(); + // noinspection JSUnusedGlobalSymbols Used dynamically from markers. + handleClick() { + if (this.gMkr === null) { + return; + } - const mp = this.gMkr.getMap(); - TP_MapMarker.smoothZoom(mp, this.gMkr.getPosition()).then(() => 1) + tpvm._utils.clearHash(); - tpvm._utils.ga('send', 'event', this.items[0].itemTypeName, 'mapMarker click', this.gMkr.getTitle()); - } + const mp = this.gMkr.getMap(); + TP_MapMarker.smoothZoom(mp, this.gMkr.getPosition()).then(() => 1) - /** - * Smoothly zoom in (or out) on the given map. By default, zooms in to the max level allowed. - * - * @param {google.maps.Map} map The Google Maps map - * @param {google.maps.LatLng} position The position to move to center - * @param {number, undefined} zoomTo Google Maps zoom level, or undefined for maxZoom. - */ - static async smoothZoom(map, position = null, zoomTo = undefined) { - if (zoomTo === undefined || zoomTo > map.maxZoom) { - zoomTo = map.maxZoom; - } - - if (map.getZoom() !== zoomTo) { - let z = google.maps.event.addListener(map, 'zoom_changed', () => { - google.maps.event.removeListener(z); - setTimeout(() => this.smoothZoom(map, position, zoomTo), 150); - }); - if (map.getZoom() < zoomTo) { // zoom in - map.setZoom(map.getZoom() + 1); - } else { // zoom out - map.setZoom(map.getZoom() - 1); + tpvm._utils.ga('send', 'event', this.items[0].itemTypeName, 'mapMarker click', this.gMkr.getTitle()); + } + + /** + * Smoothly zoom in (or out) on the given map. By default, zooms in to the max level allowed. + * + * @param {google.maps.Map} map The Google Maps map + * @param {google.maps.LatLng} position The position to move to center + * @param {number, undefined} zoomTo Google Maps zoom level, or undefined for maxZoom. + */ + static async smoothZoom(map, position = null, zoomTo = undefined) { + if (zoomTo === undefined || zoomTo > map.maxZoom) { + zoomTo = map.maxZoom; } - if (position !== null) { - let oldPos = map.getCenter(), - newPos = new google.maps.LatLng((oldPos.lat() + position.lat() * 2) / 3, (oldPos.lng() + position.lng() * 2) / 3); - map.panTo(newPos); + + if (map.getZoom() !== zoomTo) { + let z = google.maps.event.addListener(map, 'zoom_changed', () => { + google.maps.event.removeListener(z); + setTimeout(() => this.smoothZoom(map, position, zoomTo), 150); + }); + if (map.getZoom() < zoomTo) { // zoom in + map.setZoom(map.getZoom() + 1); + } else { // zoom out + map.setZoom(map.getZoom() - 1); + } + if (position !== null) { + let oldPos = map.getCenter(), + newPos = new google.maps.LatLng((oldPos.lat() + position.lat() * 2) / 3, (oldPos.lng() + position.lng() * 2) / 3); + map.panTo(newPos); + } + } else { + map.panTo(position); } - } else { - map.panTo(position); } } -} + tpvm.TP_MapMarker = TP_MapMarker; -class TP_Mappable { - name = ""; - post_id = 0; - _id = null; // For situations where the ID needs to happen early in the instantiation chain. + class TP_Mappable { + name = ""; + post_id = 0; + _id = null; // For situations where the ID needs to happen early in the instantiation chain. - geo = {}; + geo = {}; - color = "#000"; + color = "#000"; - /** - * @type {TP_Mappable[]} - */ - static items = []; - static itemsWithoutMarkers = []; + /** + * @type {TP_Mappable[]} + */ + static items = []; + static itemsWithoutMarkers = []; - _visible = true; + _visible = true; - /** - * All markers on all maps. - * - * @type {TP_MapMarker[]} - */ - static markers = []; + /** + * All markers on all maps. + * + * @type {TP_MapMarker[]} + */ + static markers = []; - /** - * Markers for this specific object. - * - * @type {TP_MapMarker[]} - */ - markers = []; + /** + * Markers for this specific object. + * + * @type {TP_MapMarker[]} + */ + markers = []; - constructor(obj, id = null) { - this._id = id; + constructor(obj, id = null) { + this._id = id; - if (obj.geo !== undefined && obj.geo !== null && obj.geo.lat !== null && obj.geo.lng !== null) { - obj.geo.lat = Math.round(obj.geo.lat * 1000) / 1000; - obj.geo.lng = Math.round(obj.geo.lng * 1000) / 1000; - } + if (obj.geo !== undefined && obj.geo !== null && obj.geo.lat !== null && obj.geo.lng !== null) { + obj.geo.lat = Math.round(obj.geo.lat * 1000) / 1000; + obj.geo.lng = Math.round(obj.geo.lng * 1000) / 1000; + } - this.geo = [obj.geo] ?? []; + this.geo = [obj.geo] ?? []; - this.name = obj.name.replace("&", "&"); - this.post_id = obj.post_id; + this.name = obj.name.replace("&", "&"); + this.post_id = obj.post_id; - if (obj.post_id === undefined) { - this.post_id = 0; - } + if (obj.post_id === undefined) { + this.post_id = 0; + } - if (obj.hasOwnProperty('color')) { - this.color = obj.color; - } + if (obj.hasOwnProperty('color')) { + this.color = obj.color; + } - for (const ei in this.connectedElements) { - if (!this.connectedElements.hasOwnProperty(ei)) continue; + for (const ei in this.connectedElements) { + if (!this.connectedElements.hasOwnProperty(ei)) continue; - let mappable = this; - this.connectedElements[ei].addEventListener('mouseenter', function(e){e.stopPropagation(); mappable.toggleHighlighted(true);}); - this.connectedElements[ei].addEventListener('mouseleave', function(e){e.stopPropagation(); mappable.toggleHighlighted(false);}); + let mappable = this; + this.connectedElements[ei].addEventListener('mouseenter', function (e) { + e.stopPropagation(); + mappable.toggleHighlighted(true); + }); + this.connectedElements[ei].addEventListener('mouseleave', function (e) { + e.stopPropagation(); + mappable.toggleHighlighted(false); + }); - let ce = this.connectedElements[ei], - actionBtns = Array.from(ce.querySelectorAll('[data-tp-action]')); - if (ce.hasAttribute('data-tp-action')) { - // if there's a sole button, it should be added to the list so it works, too. - actionBtns.push(ce); - } + let ce = this.connectedElements[ei], + actionBtns = Array.from(ce.querySelectorAll('[data-tp-action]')); + if (ce.hasAttribute('data-tp-action')) { + // if there's a sole button, it should be added to the list so it works, too. + actionBtns.push(ce); + } - for (const ai in actionBtns) { - if (!actionBtns.hasOwnProperty(ai)) continue; - const action = actionBtns[ai].getAttribute('data-tp-action'); - if (typeof mappable[action + "Action"] === "function") { - tpvm._utils.registerAction(action, mappable) - actionBtns[ai].addEventListener('click', function (e) { - e.stopPropagation(); - mappable[action + "Action"](); - }); + for (const ai in actionBtns) { + if (!actionBtns.hasOwnProperty(ai)) continue; + const action = actionBtns[ai].getAttribute('data-tp-action'); + if (typeof mappable[action + "Action"] === "function") { + tpvm._utils.registerAction(action, mappable) + actionBtns[ai].addEventListener('click', function (e) { + e.stopPropagation(); + mappable[action + "Action"](); + }); + } } } + + TP_Mappable.items.push(this); } - TP_Mappable.items.push(this); - } + /** + * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id. + * + * @return {int} + */ + get id() { + return this.post_id; + } - /** - * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id. - * - * @return {int} - */ - get id() { - return this.post_id; - } + get shortClass() { + return "mpbl"; + } - get shortClass() { - return "mpbl"; - } + static initMap(containerElt, mapOptions, list) { + google.maps.visualRefresh = true; + const map = new google.maps.Map(containerElt, mapOptions), + bounds = new google.maps.LatLngBounds(); - static initMap(containerElt, mapOptions, list) { - google.maps.visualRefresh = true; - const map = new google.maps.Map(containerElt, mapOptions), - bounds = new google.maps.LatLngBounds(); + for (const ii in list) { + if (!list.hasOwnProperty(ii)) continue; - for (const ii in list) { - if (!list.hasOwnProperty(ii)) continue; + // skip items that aren't locatable. + let hasMarkers = false; + for (const gi in list[ii].geo) { + if (list[ii].geo[gi] === null || list[ii].geo[gi].lat === null || list[ii].geo[gi].lng === null) + continue; - // skip items that aren't locatable. - let hasMarkers = false; - for (const gi in list[ii].geo) { - if (list[ii].geo[gi] === null || list[ii].geo[gi].lat === null || list[ii].geo[gi].lng === null) - continue; + const item = list[ii], + geoStr = "" + item.geo[gi].lat + "," + item.geo[gi].lng; + let mkr = this.markers.find((m) => m.gMkr.getMap() === map && m.geoStr === geoStr); - const item = list[ii], - geoStr = "" + item.geo[gi].lat + "," + item.geo[gi].lng; - let mkr = this.markers.find((m) => m.gMkr.getMap() === map && m.geoStr === geoStr); - - // If there isn't already a marker for the item on the right map, create one. - if (mkr === undefined) { - mkr = new TP_MapMarker({ - position: item.geo[gi], - color: item.color, - map: map, - animation: google.maps.Animation.DROP, - }); - mkr.geoStr = geoStr; + // If there isn't already a marker for the item on the right map, create one. + if (mkr === undefined) { + mkr = new TP_MapMarker({ + position: item.geo[gi], + color: item.color, + map: map, + animation: google.maps.Animation.DROP, + }); + mkr.geoStr = geoStr; - // Add to collection of all markers - this.markers.push(mkr); - } + // Add to collection of all markers + this.markers.push(mkr); + } - bounds.extend(mkr.gMkr.getPosition()); + bounds.extend(mkr.gMkr.getPosition()); - // If the marker doesn't already have a reference to this item, add one. - if (!mkr.items.includes(item)) { - mkr.items.push(item); - } + // If the marker doesn't already have a reference to this item, add one. + if (!mkr.items.includes(item)) { + mkr.items.push(item); + } - // If the item doesn't already have a reference to this marker, add one. - if (!item.markers.includes(mkr)) { - item.markers.push(mkr); - } + // If the item doesn't already have a reference to this marker, add one. + if (!item.markers.includes(mkr)) { + item.markers.push(mkr); + } - hasMarkers = true; + hasMarkers = true; - mkr.updateLabel(); - } - if (!hasMarkers) { - this.itemsWithoutMarkers.push(this); + mkr.updateLabel(); + } + if (!hasMarkers) { + this.itemsWithoutMarkers.push(this); + } } - } - map.fitBounds(bounds); + map.fitBounds(bounds); - map.addListener('bounds_changed', this.handleZoom); + map.addListener('bounds_changed', this.handleZoom); - // Add Map Reset links - let elts = document.getElementsByClassName("TouchPointWP-map-resetLink"); - for (const ei in elts) { - if (! elts.hasOwnProperty(ei)) continue; - elts[ei].addEventListener("click", (e) => { - tpvm._utils.clearHash(); - e.preventDefault(); - map.fitBounds(bounds); - }); + // Add Map Reset links + let elts = document.getElementsByClassName("TouchPointWP-map-resetLink"); + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) continue; + elts[ei].addEventListener("click", (e) => { + tpvm._utils.clearHash(); + e.preventDefault(); + map.fitBounds(bounds); + }); + } } - } - // noinspection JSUnusedGlobalSymbols Used dynamically from warning text. - /** - * - * @param {google.maps.Map} map - */ - static resetMap(map) { - console.log("reset " + map.getMapTypeId()) - } + // noinspection JSUnusedGlobalSymbols Used dynamically from warning text. + /** + * + * @param {google.maps.Map} map + */ + static resetMap(map) { + console.log("reset " + map.getMapTypeId()) + } - /** - * Currently, this will apply visibility to ALL mappable items, even if they're on a different map. - */ - static handleZoom() { - if (TP_Mappable.items.length > 1) { // Don't hide details on Single pages - for (const ii in TP_Mappable.items) { - TP_Mappable.items[ii].applyVisibilityToConnectedElements(); + /** + * Currently, this will apply visibility to ALL mappable items, even if they're on a different map. + */ + static handleZoom() { + if (TP_Mappable.items.length > 1) { // Don't hide details on Single pages + for (const ii in TP_Mappable.items) { + TP_Mappable.items[ii].applyVisibilityToConnectedElements(); + } + TP_Mappable.updateFilterWarnings(); } - TP_Mappable.updateFilterWarnings(); } - } - static updateFilterWarnings() { - let elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleOnly"), - includesBoth = TP_Mappable.mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds; - for (const ei in elts) { - if (!elts.hasOwnProperty(ei)) - continue; - elts[ei].style.display = (TP_Mappable.mapExcludesSomeVisibleMarkers && !includesBoth) ? "" : "none"; - } + static updateFilterWarnings() { + let elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleOnly"), + includesBoth = TP_Mappable.mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds; + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) + continue; + elts[ei].style.display = (TP_Mappable.mapExcludesSomeVisibleMarkers && !includesBoth) ? "" : "none"; + } - elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleAndInvisible"); - for (const ei in elts) { - if (!elts.hasOwnProperty(ei)) - continue; - elts[ei].style.display = includesBoth ? "" : "none"; - } + elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleAndInvisible"); + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) + continue; + elts[ei].style.display = includesBoth ? "" : "none"; + } - elts = document.getElementsByClassName("TouchPointWP-map-warning-zoomOrReset"); - for (const ei in elts) { - if (!elts.hasOwnProperty(ei)) - continue; - elts[ei].style.display = TP_Mappable.mapExcludesSomeVisibleMarkers ? "" : "none"; + elts = document.getElementsByClassName("TouchPointWP-map-warning-zoomOrReset"); + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) + continue; + elts[ei].style.display = TP_Mappable.mapExcludesSomeVisibleMarkers ? "" : "none"; + } } - } - updateMarkerLabels() { - for (const mi in this.markers) { - this.markers[mi].updateLabel(); + updateMarkerLabels() { + for (const mi in this.markers) { + this.markers[mi].updateLabel(); + } } - } - // noinspection JSUnusedGlobalSymbols Used dynamically from btns. - showOnMapAction() { - tpvm._utils.ga('send', 'event', this.itemTypeName, 'showOnMap btn click', this.name); + // noinspection JSUnusedGlobalSymbols Used dynamically from btns. + showOnMapAction() { + tpvm._utils.ga('send', 'event', this.itemTypeName, 'showOnMap btn click', this.name); + + tpvm._utils.applyHashForAction("showOnMap", this); + + // One marker (probably typical) + if (this.markers.length === 1) { + let mp = this.markers[0].gMkr.getMap(), + el = mp.getDiv(), + rect = el.getBoundingClientRect(), + viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight), + mpWithinView = !(rect.bottom < 0 || rect.top - viewHeight >= 0); + TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.getPosition()).then(() => 1); + if (!mpWithinView) { + window.scroll({ + top: rect.top, + left: rect.left, + behavior: 'smooth' + }) + } + return; + } - tpvm._utils.applyHashForAction("showOnMap", this); + // No Markers + if (this.markers.length === 0) { + console.warn("\"Show on Map\" was called on a Mappable item that doesn't have markers.") + return; + } - // One marker (probably typical) - if (this.markers.length === 1) { - let mp = this.markers[0].gMkr.getMap(), - el = mp.getDiv(), - rect = el.getBoundingClientRect(), - viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight), - mpWithinView = !(rect.bottom < 0 || rect.top - viewHeight >= 0); - TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.getPosition()).then(() => 1); - if (!mpWithinView) { - window.scroll({ - top: rect.top, - left: rect.left, - behavior: 'smooth' - }) + // More than one marker + console.warn("\"Show on Map\" for Mappable items with multiple markers is not fully supported.") + // Hide all non-matching markers. There isn't really a way to get them back, but that's why this isn't fully supported. + for (const mi in TP_Mappable.markers) { + for (const ii in TP_Mappable.markers[mi].items) { + TP_Mappable.markers[mi].items[ii].toggleVisibility(TP_Mappable.markers[mi].items[ii] === this); + } } - return; } - // No Markers - if (this.markers.length === 0) { - console.warn("\"Show on Map\" was called on a Mappable item that doesn't have markers.") - return; + get itemTypeName() { + return this.constructor.name; } - // More than one marker - console.warn("\"Show on Map\" for Mappable items with multiple markers is not fully supported.") - // Hide all non-matching markers. There isn't really a way to get them back, but that's why this isn't fully supported. - for (const mi in TP_Mappable.markers) { - for (const ii in TP_Mappable.markers[mi].items) { - TP_Mappable.markers[mi].items[ii].toggleVisibility(TP_Mappable.markers[mi].items[ii] === this); - } + get visible() { + return this._visible && (this.markers.some((m) => m.visible) || this.markers.length === 0); } - } - - get itemTypeName() { - return this.constructor.name; - } - get visible() { - return this._visible && (this.markers.some((m) => m.visible) || this.markers.length === 0); - } - - get inBounds() { - return this.markers.some((m) => m.inBounds); - } - - static get mapExcludesSomeVisibleMarkers() { - return this.markers.some((m) => m.visible && !m.inBounds); - } + get inBounds() { + return this.markers.some((m) => m.inBounds); + } - static get mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds() { - return this.items.some((i) => i.visible && i.markers.some((mk) => mk.inBounds) && i.markers.some((mk) => !mk.inBounds)) - } + static get mapExcludesSomeVisibleMarkers() { + return this.markers.some((m) => m.visible && !m.inBounds); + } - get useIcon() { - return false; - } + static get mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds() { + return this.items.some((i) => i.visible && i.markers.some((mk) => mk.inBounds) && i.markers.some((mk) => !mk.inBounds)) + } - get highlightable() { - return true; - } + get useIcon() { + return false; + } - toggleVisibility(vis = null) { - if (vis === null) { - this._visible = !this._visible - } else { - this._visible = !!vis; + get highlightable() { + return true; } - this._visible = vis; - this.updateMarkerLabels(); + toggleVisibility(vis = null) { + if (vis === null) { + this._visible = !this._visible + } else { + this._visible = !!vis; + } - this.applyVisibilityToConnectedElements(); + this._visible = vis; + this.updateMarkerLabels(); - return this._visible; - } + this.applyVisibilityToConnectedElements(); - get connectedElements() { - const clsName = this.constructor.name.toLowerCase().replace("_", "-"); - const sPath = '[data-' + clsName + '="' + this.post_id + '"]' - return document.querySelectorAll(sPath); - } + return this._visible; + } - applyVisibilityToConnectedElements() { - let elts = this.connectedElements; - for (const ei in elts) { - if (!elts.hasOwnProperty(ei)) - continue; - elts[ei].style.display = (this.visible && (this.inBounds || !TP_Mappable.mapExcludesSomeVisibleMarkers)) ? "" : "none"; + get connectedElements() { + const clsName = this.constructor.name.toLowerCase().replace("_", "-"); + const sPath = '[data-' + clsName + '="' + this.post_id + '"]' + return document.querySelectorAll(sPath); } - } - toggleHighlighted(hl) { - this.highlighted = !!hl; + applyVisibilityToConnectedElements() { + let elts = this.connectedElements; + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) + continue; + elts[ei].style.display = (this.visible && (this.inBounds || !TP_Mappable.mapExcludesSomeVisibleMarkers)) ? "" : "none"; + } + } + + toggleHighlighted(hl) { + this.highlighted = !!hl; - if (!this.highlightable) - this.highlighted = false; + if (!this.highlightable) + this.highlighted = false; - if (this.highlighted) { - let item = this; - for (let mi in this.markers) { - const mk = item.markers[mi]; - if (TP_Mappable.items.length > 1) { - if (mk.gMkr.getAnimation() !== google.maps.Animation.BOUNCE) { - mk.gMkr.setAnimation(google.maps.Animation.BOUNCE); + if (this.highlighted) { + let item = this; + for (let mi in this.markers) { + const mk = item.markers[mi]; + if (TP_Mappable.items.length > 1) { + if (mk.gMkr.getAnimation() !== google.maps.Animation.BOUNCE) { + mk.gMkr.setAnimation(google.maps.Animation.BOUNCE); + } } + mk.updateLabel(this.highlighted) } - mk.updateLabel(this.highlighted) - } - } else { - for (const mi in this.markers) { - let mk = this.markers[mi]; - if (mk.gMkr.getAnimation() !== null) { - mk.gMkr.setAnimation(null) + } else { + for (const mi in this.markers) { + let mk = this.markers[mi]; + if (mk.gMkr.getAnimation() !== null) { + mk.gMkr.setAnimation(null) + } + mk.updateLabel(this.highlighted) } - mk.updateLabel(this.highlighted) } } } -} + tpvm.TP_Mappable = TP_Mappable; -class TP_Involvement extends TP_Mappable { - invId = ""; - invType = "involvement"; // overwritten by constructor + class TP_Involvement extends tpvm.TP_Mappable { + invId = ""; + invType = "involvement"; // overwritten by constructor - attributes = {}; + attributes = {}; - static currentFilters = {}; + static currentFilters = {}; - static actions = ['join', 'contact']; + static actions = ['join', 'contact']; - constructor(obj) { - super(obj, obj.invId); + constructor(obj) { + super(obj, obj.invId); - this.invId = obj.invId; - this.invType = obj.invType; + this.invId = obj.invId; + this.invType = obj.invType; - this.attributes = obj.attributes ?? null; + this.attributes = obj.attributes ?? null; - tpvm.involvements[this.invId] = this; - } + tpvm.involvements[this.invId] = this; + } - get id() { - return parseInt(this._id); - } + get id() { + return parseInt(this._id); + } - get shortClass() { - return "i"; - } + get shortClass() { + return "i"; + } - // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation. - static fromObjArray(invArr) { - let ret = []; - for (const i in invArr) { - if (!invArr.hasOwnProperty(i)) continue; + // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation. + static fromObjArray(invArr) { + let ret = []; + for (const i in invArr) { + if (!invArr.hasOwnProperty(i)) continue; - if (typeof invArr[i].invId === "undefined") { - continue; + if (typeof invArr[i].invId === "undefined") { + continue; + } + + if (typeof tpvm.involvements[invArr[i].invId] === "undefined") { + ret.push(new this(invArr[i])); + } else { + ret.push(tpvm.involvements[invArr[i].invId]); + } } + tpvm.trigger("Involvement_fromObjArray"); + return ret; + }; - if (typeof tpvm.involvements[invArr[i].invId] === "undefined") { - ret.push(new this(invArr[i])); - } else { - ret.push(tpvm.involvements[invArr[i].invId]); + // noinspection JSUnusedGlobalSymbols Called by inline. + static initFilters() { + const filtOptions = document.querySelectorAll("[data-involvement-filter]"); + for (const ei in filtOptions) { + if (!filtOptions.hasOwnProperty(ei)) continue; + filtOptions[ei].addEventListener('change', this.applyFilters.bind(this, "Involvement")) } } - tpvm.trigger("Involvement_fromObjArray"); - return ret; - }; - // noinspection JSUnusedGlobalSymbols Called by inline. - static initFilters() { - const filtOptions = document.querySelectorAll("[data-involvement-filter]"); - for (const ei in filtOptions) { - if (!filtOptions.hasOwnProperty(ei)) continue; - filtOptions[ei].addEventListener('change', this.applyFilters.bind(this, "Involvement")) + static applyFilters(invType, ev = null) { + if (ev !== null) { + let attr = ev.target.getAttribute("data-involvement-filter"), + val = ev.target.value; + if (attr !== null) { + if (val === "") { + delete this.currentFilters[attr]; + } else { + this.currentFilters[attr] = val; + } + } + } + + groupLoop: + for (const ii in tpvm.involvements) { + if (!tpvm.involvements.hasOwnProperty(ii)) continue; + const group = tpvm.involvements[ii]; + for (const ai in this.currentFilters) { + if (!this.currentFilters.hasOwnProperty(ai)) continue; + + if (!group.attributes.hasOwnProperty(ai) || + group.attributes[ai] === null || + (!Array.isArray(group.attributes[ai]) && + group.attributes[ai].slug !== this.currentFilters[ai] && + group.attributes[ai] !== this.currentFilters[ai] + ) || ( + Array.isArray(group.attributes[ai]) && + group.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined + ) + ) { + group.toggleVisibility(false) + continue groupLoop; + } + } + group.toggleVisibility(true) + } + TP_Mappable.updateFilterWarnings(); } - } - static applyFilters(invType, ev = null) { - if (ev !== null) { - let attr = ev.target.getAttribute("data-involvement-filter"), - val = ev.target.value; - if (attr !== null) { - if (val === "") { - delete this.currentFilters[attr]; - } else { - this.currentFilters[attr] = val; + static init() { + tpvm.trigger('Involvement_class_loaded'); + } + + async doJoin(people, showConfirm = true) { + let inv = this; + showConfirm = !!showConfirm; + + tpvm._utils.ga('send', 'event', inv.invType, 'join complete', inv.name); + + let res = await tpvm.postData('inv/join', {invId: inv.invId, people: people, invType: inv.invType}); + if (res.success.length > 0) { + if (showConfirm) { + Swal.fire({ + icon: 'success', + // translators: %s is the name of an involvement, like a particular small group + title: sprintf(__('Added to %s', 'TouchPoint-WP'), inv.name), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); + } + } else { + console.error(res); + if (showConfirm) { + Swal.fire({ + icon: 'error', + title: __('Something strange happened.', 'TouchPoint-WP'), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); } } } - groupLoop: - for (const ii in tpvm.involvements) { - if (!tpvm.involvements.hasOwnProperty(ii)) continue; - const group = tpvm.involvements[ii]; - for (const ai in this.currentFilters) { - if (!this.currentFilters.hasOwnProperty(ai)) continue; - - if (!group.attributes.hasOwnProperty(ai) || - group.attributes[ai] === null || - ( !Array.isArray(group.attributes[ai]) && - group.attributes[ai].slug !== this.currentFilters[ai] && - group.attributes[ai] !== this.currentFilters[ai] - ) || ( - Array.isArray(group.attributes[ai]) && - group.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined - ) - ) { - group.toggleVisibility(false) - continue groupLoop; - } + async doInvContact(fromPerson, message, showConfirm = true) { + let inv = this; + showConfirm = !!showConfirm; + + tpvm._utils.ga('send', 'event', inv.invType, 'contact complete', inv.name); + + let res = await tpvm.postData('inv/contact', { + invId: inv.invId, + fromPerson: fromPerson, + message: message, + invType: inv.invType, + fromEmail: tpvm.userEmail + }); + if (res.success.length > 0) { + if (showConfirm) { + Swal.fire({ + icon: 'success', + title: __('Your message has been sent.', 'TouchPoint-WP'), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); + } + } else { + console.error(res); + if (showConfirm) { + Swal.fire({ + icon: 'error', + title: __('Something strange happened.', 'TouchPoint-WP'), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); } - group.toggleVisibility(true) } - TP_Mappable.updateFilterWarnings(); - } + } - static init() { - tpvm.trigger('Involvement_class_loaded'); - } + // noinspection JSUnusedGlobalSymbols Used dynamically from btns. + joinAction() { + let inv = this, + // translators: %s is the name of an Involvement + title = sprintf(__('Join %s', 'TouchPoint-WP'), inv.name); - async doJoin(people, showConfirm = true) { - let inv = this; - showConfirm = !!showConfirm; + tpvm._utils.ga('send', 'event', inv.invType, 'join btn click', inv.name); - tpvm._utils.ga('send', 'event', inv.invType, 'join complete', inv.name); + tpvm._utils.applyHashForAction("join", this); - let res = await tpvm.postData('inv/join', {invId: inv.invId, people: people, invType: inv.invType}); - if (res.success.length > 0) { - if (showConfirm) { - Swal.fire({ - icon: 'success', - // translators: %s is the name of an involvement, like a particular small group - title: sprintf(__('Added to %s', 'TouchPoint-WP'), inv.name), - timer: 3000, - customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') - }); - } - } else { - console.error(res); - if (showConfirm) { - Swal.fire({ - icon: 'error', - title: __('Something strange happened.', 'TouchPoint-WP'), - timer: 3000, + TP_Person.DoInformalAuth(title).then( + (res) => joinUi(inv, res).then(tpvm._utils.clearHash), + () => tpvm._utils.clearHash() + ) + + function joinUi(inv, people) { + tpvm._utils.ga('send', 'event', inv.invType, 'join userIdentified', inv.name); + + return Swal.fire({ + title: title, + html: `

${__('Who is joining the group?', 'TouchPoint-WP')}

` + TP_Person.peopleArrayToCheckboxes(people), customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') + showConfirmButton: true, + showCancelButton: true, + confirmButtonText: __('Join', 'TouchPoint-WP'), + cancelButtonText: __('Cancel', 'TouchPoint-WP'), + focusConfirm: false, + preConfirm: () => { + let form = document.getElementById('tp_people_list_checkboxes'), + inputs = form.querySelectorAll("input"), + data = []; + for (const ii in inputs) { + if (!inputs.hasOwnProperty(ii) || !inputs[ii].checked) continue; + data.push(tpvm.people[inputs[ii].value]); + } + + if (data.length < 1) { + let prompt = document.getElementById('swal-tp-text'); + prompt.innerText = __("Select who should be added to the group.", 'TouchPoint-WP'); + prompt.classList.add('error') + return false; + } + + Swal.showLoading(); + + return inv.doJoin(data, true); + } }); } } - } - async doInvContact(fromPerson, message, showConfirm = true) { - let inv = this; - showConfirm = !!showConfirm; - - tpvm._utils.ga('send', 'event', inv.invType, 'contact complete', inv.name); - - let res = await tpvm.postData('inv/contact', { - invId: inv.invId, - fromPerson: fromPerson, - message: message, - invType: inv.invType, - fromEmail: tpvm.userEmail - }); - if (res.success.length > 0) { - if (showConfirm) { - Swal.fire({ - icon: 'success', - title: __('Your message has been sent.', 'TouchPoint-WP'), - timer: 3000, - customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') - }); - } - } else { - console.error(res); - if (showConfirm) { - Swal.fire({ - icon: 'error', - title: __('Something strange happened.', 'TouchPoint-WP'), - timer: 3000, - customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') - }); - } + get itemTypeName() { + return this.invType; } - } - // noinspection JSUnusedGlobalSymbols Used dynamically from btns. - joinAction() { - let inv = this, - // translators: %s is the name of an Involvement - title = sprintf(__('Join %s', 'TouchPoint-WP'), inv.name); - - tpvm._utils.ga('send', 'event', inv.invType, 'join btn click', inv.name); - - tpvm._utils.applyHashForAction("join", this); - - TP_Person.DoInformalAuth(title).then( - (res) => joinUi(inv, res).then(tpvm._utils.clearHash), - () => tpvm._utils.clearHash() - ) - - function joinUi(inv, people) { - tpvm._utils.ga('send', 'event', inv.invType, 'join userIdentified', inv.name); - - return Swal.fire({ - title: title, - html: `

${__('Who is joining the group?', 'TouchPoint-WP')}

` + TP_Person.peopleArrayToCheckboxes(people), - customClass: tpvm._utils.defaultSwalClasses(), - showConfirmButton: true, - showCancelButton: true, - confirmButtonText: __('Join', 'TouchPoint-WP'), - cancelButtonText: __('Cancel', 'TouchPoint-WP'), - focusConfirm: false, - preConfirm: () => { - let form = document.getElementById('tp_people_list_checkboxes'), - inputs = form.querySelectorAll("input"), - data = []; - for (const ii in inputs) { - if (!inputs.hasOwnProperty(ii) || !inputs[ii].checked) continue; - data.push(tpvm.people[inputs[ii].value]); - } + // noinspection JSUnusedGlobalSymbols Used dynamically from btns. + contactAction() { + let inv = this, + // translators: %s is the name of an involvement. This is a heading for a modal. + title = sprintf(__("Contact the Leaders of %s", 'TouchPoint-WP'), inv.name); - if (data.length < 1) { - let prompt = document.getElementById('swal-tp-text'); - prompt.innerText = __("Select who should be added to the group.", 'TouchPoint-WP'); - prompt.classList.add('error') - return false; - } + tpvm._utils.ga('send', 'event', inv.invType, 'contact btn click', inv.name); - Swal.showLoading(); + tpvm._utils.applyHashForAction("contact", this); - return inv.doJoin(data, true); - } - }); - } - } + TP_Person.DoInformalAuth(title).then( + (res) => contactUi(inv, res).then(tpvm._utils.clearHash), + () => tpvm._utils.clearHash() + ) - get itemTypeName() { - return this.invType; - } + function contactUi(inv, people) { + tpvm._utils.ga('send', 'event', inv.invType, 'contact userIdentified', inv.name); + + return Swal.fire({ + title: title, + html: '
' + + `
` + TP_Person.peopleArrayToSelect(people, "tp_inv_contact_fromPid", "fromPid") + '
' + + `
` + + '
', + customClass: tpvm._utils.defaultSwalClasses(), + showConfirmButton: true, + showCancelButton: true, + confirmButtonText: __('Send', 'TouchPoint-WP'), + cancelButtonText: __('Cancel', 'TouchPoint-WP'), + focusConfirm: false, + preConfirm: () => { + let form = document.getElementById('tp_inv_contact_form'), + fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)], + message = form.getElementsByTagName('textarea')[0].value; + + if (message.length < 5) { + let prompt = document.getElementById('swal-tp-text'); + prompt.innerText = __("Please provide a message.", 'TouchPoint-WP'); + prompt.classList.add('error') + return false; + } - // noinspection JSUnusedGlobalSymbols Used dynamically from btns. - contactAction() { - let inv = this, - // translators: %s is the name of an involvement. This is a heading for a modal. - title = sprintf(__("Contact the Leaders of %s", 'TouchPoint-WP'), inv.name); - - tpvm._utils.ga('send', 'event', inv.invType, 'contact btn click', inv.name); - - tpvm._utils.applyHashForAction("contact", this); - - TP_Person.DoInformalAuth(title).then( - (res) => contactUi(inv, res).then(tpvm._utils.clearHash), - () => tpvm._utils.clearHash() - ) - - function contactUi(inv, people) { - tpvm._utils.ga('send', 'event', inv.invType, 'contact userIdentified', inv.name); - - return Swal.fire({ - title: title, - html: '
' + - `
` + TP_Person.peopleArrayToSelect(people, "tp_inv_contact_fromPid", "fromPid") + '
' + - `
` + - '
', - customClass: tpvm._utils.defaultSwalClasses(), - showConfirmButton: true, - showCancelButton: true, - confirmButtonText: __('Send', 'TouchPoint-WP'), - cancelButtonText: __('Cancel', 'TouchPoint-WP'), - focusConfirm: false, - preConfirm: () => { - let form = document.getElementById('tp_inv_contact_form'), - fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)], - message = form.getElementsByTagName('textarea')[0].value; - - if (message.length < 5) { - let prompt = document.getElementById('swal-tp-text'); - prompt.innerText = __("Please provide a message.", 'TouchPoint-WP'); - prompt.classList.add('error') - return false; + Swal.showLoading(); + + return inv.doInvContact(fromPerson, message, true); } + }); + } + } - Swal.showLoading(); + static initMap(mapDivId) { + let mapOptions = { + mapTypeId: google.maps.MapTypeId.ROADMAP, + linksControl: false, + maxZoom: 15, + minZoom: 2, + panControl: false, + addressControl: false, + enableCloseButton: false, + mapTypeControl: false, + zoomControl: false, + gestureHandling: 'greedy', + styles: [ + { + featureType: "poi", //points of interest + stylers: [ + {visibility: 'off'} + ] + }, + { + featureType: "road", + stylers: [ + {visibility: 'on'} + ] + }, + { + featureType: "transit", + stylers: [ + {visibility: 'on'} + ] + } + ], + zoom: 15, + center: {lat: 0, lng: 0}, // gets overwritten by bounds later. + streetViewControl: false, + fullscreenControl: false, + disableDefaultUI: true + }; - return inv.doInvContact(fromPerson, message, true); - } - }); + super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.involvements) } - } - static initMap(mapDivId) { - let mapOptions = { - mapTypeId: google.maps.MapTypeId.ROADMAP, - linksControl: false, - maxZoom: 15, - minZoom: 2, - panControl: false, - addressControl: false, - enableCloseButton: false, - mapTypeControl: false, - zoomControl: false, - gestureHandling: 'greedy', - styles: [ - { - featureType: "poi", //points of interest - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "road", - stylers: [ - {visibility: 'on'} - ] - }, - { - featureType: "transit", - stylers: [ - {visibility: 'on'} - ] - } - ], - zoom: 15, - center: {lat: 0, lng: 0}, // gets overwritten by bounds later. - streetViewControl: false, - fullscreenControl: false, - disableDefaultUI: true - }; + static initNearby(targetId, type, count) { + if (window.location.pathname.substring(0, 10) === "/wp-admin/") + return; - super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.involvements) - } + let target = document.getElementById(targetId); + if (!target) // make sure element actually exists (it may not if shortcode was within a tease) + return; - static initNearby(targetId, type, count) { - if (window.location.pathname.substring(0, 10) === "/wp-admin/") - return; - - let target = document.getElementById(targetId); - if (!target) // make sure element actually exists (it may not if shortcode was within a tease) - return; - - tpvm._invNear.nearby = ko.observableArray([]); - tpvm._invNear.labelStr = ko.observable(__("Loading...", 'TouchPoint-WP')); - ko.applyBindings(tpvm._invNear, target); - - // continue to next action for either success or failure. - TP_DataGeo.getLocation(getNearbyInvolvements, getNearbyInvolvements, 'nav'); - - function getNearbyInvolvements() { - tpvm.getData('inv/nearby', { - lat: TP_DataGeo.loc.lat, - lng: TP_DataGeo.loc.lng, - type: type, - limit: count, - locale: tpvm.locale - }).then(handleNearbyLoaded); - } - - function handleNearbyLoaded(response) { - tpvm._invNear.nearby(response.invList); - if (response.error !== undefined) { - if (response.geo === false) { - if (navigator.geolocation && location.protocol === 'https:') { - tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP') + "
" + __("Click here to use your actual location.", 'TouchPoint-WP') + ""); + tpvm._invNear.nearby = ko.observableArray([]); + tpvm._invNear.labelStr = ko.observable(__("Loading...", 'TouchPoint-WP')); + ko.applyBindings(tpvm._invNear, target); + + // continue to next action for either success or failure. + TP_DataGeo.getLocation(getNearbyInvolvements, getNearbyInvolvements, 'nav'); + + function getNearbyInvolvements() { + tpvm.getData('inv/nearby', { + lat: TP_DataGeo.loc.lat, + lng: TP_DataGeo.loc.lng, + type: type, + limit: count, + locale: tpvm.locale + }).then(handleNearbyLoaded); + } + + function handleNearbyLoaded(response) { + tpvm._invNear.nearby(response.invList); + if (response.error !== undefined) { + if (response.geo === false) { + if (navigator.geolocation && location.protocol === 'https:') { + tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP') + "
" + __("Click here to use your actual location.", 'TouchPoint-WP') + ""); + } else { + tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP')); + } } else { - tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP')); + tpvm._invNear.labelStr(response.error); } + } else if (response.geo?.human !== undefined) { + let label = response.geo.human; + if (response.geo.type === "ip" && navigator.geolocation && location.protocol === 'https:') { + label += "
" + __("Click here to use your actual location.", 'TouchPoint-WP') + ""; + } + tpvm._invNear.labelStr(label); } else { - tpvm._invNear.labelStr(response.error); - } - } else if (response.geo?.human !== undefined) { - let label = response.geo.human; - if (response.geo.type === "ip" && navigator.geolocation && location.protocol === 'https:') { - label += "
" + __("Click here to use your actual location.", 'TouchPoint-WP') + ""; + tpvm._invNear.labelStr(__("Your Location", 'TouchPoint-WP')); } - tpvm._invNear.labelStr(label); - } else { - tpvm._invNear.labelStr(__("Your Location", 'TouchPoint-WP')); + setTimeout(getNearbyInvolvements, 600000); // 10 minutes } - setTimeout(getNearbyInvolvements, 600000); // 10 minutes } } -} -TP_Involvement.init(); + tpvm.TP_Involvement = TP_Involvement; + TP_Involvement.init(); -class TP_Person { - peopleId; - familyId; - displayName; + class TP_Person { + peopleId; + familyId; + displayName; - static actions = ['join', 'contact']; + static actions = ['join', 'contact']; - constructor(peopleId) { - peopleId = Number(peopleId); - this.peopleId = peopleId; + constructor(peopleId) { + peopleId = Number(peopleId); + this.peopleId = peopleId; - for (const ei in this.connectedElements) { - if (!this.connectedElements.hasOwnProperty(ei)) continue; + for (const ei in this.connectedElements) { + if (!this.connectedElements.hasOwnProperty(ei)) continue; - let psn = this; + let psn = this; - let actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]') - for (const ai in actionBtns) { - if (!actionBtns.hasOwnProperty(ai)) continue; - const action = actionBtns[ai].getAttribute('data-tp-action'); - if (TP_Person.actions.includes(action)) { - tpvm._utils.registerAction(action, psn) - actionBtns[ai].addEventListener('click', function (e) { - e.stopPropagation(); - psn[action + "Action"](); - }); + let actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]') + for (const ai in actionBtns) { + if (!actionBtns.hasOwnProperty(ai)) continue; + const action = actionBtns[ai].getAttribute('data-tp-action'); + if (TP_Person.actions.includes(action)) { + tpvm._utils.registerAction(action, psn) + actionBtns[ai].addEventListener('click', function (e) { + e.stopPropagation(); + psn[action + "Action"](); + }); + } } } + + tpvm.people[peopleId] = this; } - tpvm.people[peopleId] = this; - } + /** + * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id. + * + * @return {int} + */ + get id() { + return this.peopleId; + } - /** - * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id. - * - * @return {int} - */ - get id() { - return this.peopleId; - } + get shortClass() { + return "p"; + } - get shortClass() { - return "p"; - } + static fromObj(obj) { + let person; + if (tpvm.people[obj.peopleId] !== undefined) { + person = tpvm.people[obj.peopleId] + } else { + person = new TP_Person(obj.peopleId); + } + for (const a in obj) { + if (!obj.hasOwnProperty(a) || a === 'peopleId') continue; - static fromObj(obj) { - let person; - if (tpvm.people[obj.peopleId] !== undefined) { - person = tpvm.people[obj.peopleId] - } else { - person = new TP_Person(obj.peopleId); + person[a] = obj[a]; + } + return person; } - for (const a in obj) { - if (!obj.hasOwnProperty(a) || a === 'peopleId') continue; - person[a] = obj[a]; - } - return person; - } + static fromObjArray(peopleArray) { + let ret = []; - static fromObjArray(peopleArray) { - let ret = []; + for (const pi in peopleArray) { + if (!peopleArray.hasOwnProperty(pi)) continue; + ret.push(tpvm.TP_Person.fromObj(peopleArray[pi])); + } + tpvm.trigger("Person_fromObjArray"); - for (const pi in peopleArray) { - if (!peopleArray.hasOwnProperty(pi)) continue; - ret.push(TP_Person.fromObj(peopleArray[pi])); + return ret; } - tpvm.trigger("Person_fromObjArray"); - return ret; - } + // noinspection JSUnusedGlobalSymbols Called in Person.php + static identByFamily(primaryFamIds = [], secondaryFamIds = []) { + tpvm._plausibleUsers = Object.entries(tpvm.people).filter(([, p]) => primaryFamIds.indexOf(p.familyId) > -1).map(([, p]) => p); + tpvm._secondaryUsers = Object.entries(tpvm.people).filter(([, p]) => secondaryFamIds.indexOf(p.familyId) > -1).map(([, p]) => p); + } - // noinspection JSUnusedGlobalSymbols Called in Person.php - static identByFamily(primaryFamIds = [], secondaryFamIds = []) { - tpvm._plausibleUsers = Object.entries(tpvm.people).filter(([,p]) => primaryFamIds.indexOf(p.familyId) > -1).map(([,p]) => p); - tpvm._secondaryUsers = Object.entries(tpvm.people).filter(([,p]) => secondaryFamIds.indexOf(p.familyId) > -1).map(([,p]) => p); - } + static init() { + tpvm.trigger('Person_class_loaded'); + } - static init() { - tpvm.trigger('Person_class_loaded'); - } + get connectedElements() { + const sPath = '[data-tp-person="' + this.peopleId + '"]' + return document.querySelectorAll(sPath); + } - get connectedElements() { - const sPath = '[data-tp-person="' + this.peopleId + '"]' - return document.querySelectorAll(sPath); - } + static mergePeopleArrays(a, b) { + return [...new Set([...a, ...b])] + } - static mergePeopleArrays(a, b) { - return [...new Set([...a, ...b])] - } + /** + * Take an array of TP_Person objects and make a list of checkboxes out of them. + * + * @param array TP_Person[] + */ + static peopleArrayToCheckboxes(array) { + let out = "
" - /** - * Take an array of TP_Person objects and make a list of checkboxes out of them. - * - * @param array TP_Person[] - */ - static peopleArrayToCheckboxes(array) { - let out = "
" + for (const pi in array) { + if (!array.hasOwnProperty(pi)) continue; + let p = array[pi]; - for (const pi in array) { - if (!array.hasOwnProperty(pi)) continue; - let p = array[pi]; + out += '' + out += '' + } - out += '' - out += '' + return out + "
" } - return out + "" - } + /** + * Take an array of TP_Person objects and make a list of radio buttons out of them. + * + * @param options object - an object that is a key-value map of data-value: translated-label + * @param array TP_Person[] + * @param secondaryArray TP_Person[] + */ + static peopleArrayToRadio(options, array, secondaryArray = null) { + let out = "
" - /** - * Take an array of TP_Person objects and make a list of radio buttons out of them. - * - * @param options object - an object that is a key-value map of data-value: translated-label - * @param array TP_Person[] - * @param secondaryArray TP_Person[] - */ - static peopleArrayToRadio(options, array, secondaryArray = null) { - let out = "
" - - // headers - out += ""; - for (const oi in options) { - if (!options.hasOwnProperty(oi)) continue; - out += `` - } - out += ``; - - // people -- primary array - for (const pi in array) { - if (!array.hasOwnProperty(pi)) continue; - let p = array[pi]; - - out += '' + // headers + out += ""; for (const oi in options) { if (!options.hasOwnProperty(oi)) continue; - out += `` + out += `` } - out += `` - out += `` - } + out += ``; - // people -- secondary array - if (secondaryArray !== null && secondaryArray.length > 0) { - out += ``; - out += ``; - for (const pi in secondaryArray) { - if (!secondaryArray.hasOwnProperty(pi)) continue; - let p = secondaryArray[pi]; + // people -- primary array + for (const pi in array) { + if (!array.hasOwnProperty(pi)) continue; + let p = array[pi]; out += '' for (const oi in options) { if (!options.hasOwnProperty(oi)) continue; out += `` } - out += ``; - out += ``; + out += `` + out += `` } - } - - return out + "
${options[oi]}
${options[oi]}${__('clear', 'TouchPoint-WP')}${p.goesBy} ${p.lastName}
${__('Other Relatives...', 'TouchPoint-WP')}
" - } - static clearRadio(name) { - let elts = document.getElementsByName(name); - for (const ei in elts) { - if (!elts.hasOwnProperty(ei)) continue; - elts[ei].checked = false; - } - } - - // noinspection JSUnusedGlobalSymbols Used dynamically from btns. - contactAction() { - let psn = this, - // translators: %s is a person's name. This is a heading for a contact modal. - title = sprintf(__('Contact %s', 'TouchPoint-WP'), psn.displayName); - - tpvm._utils.ga('send', 'event', 'Person', 'contact btn click', psn.peopleId); - - tpvm._utils.applyHashForAction("contact", this); - - TP_Person.DoInformalAuth(title).then( - (res) => contactUi(psn, res).then(tpvm._utils.clearHash), - () => tpvm._utils.clearHash() - ) - - function contactUi(psn, people) { - tpvm._utils.ga('send', 'event', 'Person', 'contact userIdentified', psn.peopleId); - - return Swal.fire({ - title: title, - html: '
' + - `
` + TP_Person.peopleArrayToSelect(people, "tp_person_contact_fromPid", "fromPid") + '
' + - `
` + - '
', - customClass: tpvm._utils.defaultSwalClasses(), - showConfirmButton: true, - showCancelButton: true, - confirmButtonText: __('Send', 'TouchPoint-WP'), - cancelButtonText: __('Cancel', 'TouchPoint-WP'), - focusConfirm: false, - preConfirm: () => { - let form = document.getElementById('tp_person_contact_form'), - fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)], - message = form.getElementsByTagName('textarea')[0].value; - - if (message.length < 5) { - let prompt = document.getElementById('swal2-title'); - prompt.innerText = __("Please provide a message.", 'TouchPoint-WP'); - prompt.classList.add('error') - return false; + // people -- secondary array + if (secondaryArray !== null && secondaryArray.length > 0) { + out += `${__('Other Relatives...', 'TouchPoint-WP')}`; + out += ``; + for (const pi in secondaryArray) { + if (!secondaryArray.hasOwnProperty(pi)) continue; + let p = secondaryArray[pi]; + + out += '' + for (const oi in options) { + if (!options.hasOwnProperty(oi)) continue; + out += `` } - - Swal.showLoading(); - - return psn.doPersonContact(fromPerson, message, true); + out += `${__('clear', 'TouchPoint-WP')}`; + out += `${p.goesBy} ${p.lastName}`; } - }); + } + + return out + "" } - } - async doPersonContact(fromPerson, message, showConfirm = true) { - let psn = this; - showConfirm = !!showConfirm; - - tpvm._utils.ga('send', 'event', 'Person', 'contact complete', psn.peopleId); - - let res = await tpvm.postData('person/contact', { - toId: psn.peopleId, - fromPerson: fromPerson, - message: message, - fromEmail: tpvm.userEmail - }); - if (res.success.length > 0) { - if (showConfirm) { - Swal.fire({ - icon: 'success', - title: __('Your message has been sent.', 'TouchPoint-WP'), - timer: 3000, - customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') - }); - } - } else { - console.error(res); - if (showConfirm) { - Swal.fire({ - icon: 'error', - title: __('Something strange happened.', 'TouchPoint-WP'), - timer: 3000, - customClass: tpvm._utils.defaultSwalClasses(), - confirmButtonText: __('OK', 'TouchPoint-WP') - }); + static clearRadio(name) { + let elts = document.getElementsByName(name); + for (const ei in elts) { + if (!elts.hasOwnProperty(ei)) continue; + elts[ei].checked = false; } } - } - /** - * - * @param array TP_Person[] - * @param id string - * @param name string - */ - static peopleArrayToSelect(array, id, name) { - let out = `" - } + TP_Person.DoInformalAuth(title).then( + (res) => contactUi(psn, res).then(tpvm._utils.clearHash), + () => tpvm._utils.clearHash() + ) - static async DoInformalAuth(title, forceAsk = false) { - return new Promise(function (resolve, reject) { - if (tpvm._plausibleUsers.length > 0 && !forceAsk) { - resolve(tpvm._plausibleUsers); - } else { - Swal.fire({ - html: `

${__('Tell us about yourself.', 'TouchPoint-WP')}

` + - '
' + - `
` + - `
` + - '
', + function contactUi(psn, people) { + tpvm._utils.ga('send', 'event', 'Person', 'contact userIdentified', psn.peopleId); + + return Swal.fire({ + title: title, + html: '
' + + `
` + TP_Person.peopleArrayToSelect(people, "tp_person_contact_fromPid", "fromPid") + '
' + + `
` + + '
', customClass: tpvm._utils.defaultSwalClasses(), showConfirmButton: true, showCancelButton: true, - title: title, - confirmButtonText: __('Next', 'TouchPoint-WP'), + confirmButtonText: __('Send', 'TouchPoint-WP'), cancelButtonText: __('Cancel', 'TouchPoint-WP'), focusConfirm: false, - didOpen: () => { - document.getElementById('tp_ident_form').addEventListener('submit', (e) => { - Swal.clickConfirm(); - e.preventDefault(); - }) - }, - preConfirm: async () => { - let form = document.getElementById('tp_ident_form'), - inputs = form.querySelectorAll("input"), - data = {}; - form.checkValidity() - for (const ii in inputs) { - if (!inputs.hasOwnProperty(ii)) continue; - if (!inputs[ii].reportValidity()) { - return false; - } - let name = inputs[ii].name.replace("tp_ident_", ""); - if (name.length > 0) // removes entry generated by submit input - data[name] = inputs[ii].value; + preConfirm: () => { + let form = document.getElementById('tp_person_contact_form'), + fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)], + message = form.getElementsByTagName('textarea')[0].value; + + if (message.length < 5) { + let prompt = document.getElementById('swal2-title'); + prompt.innerText = __("Please provide a message.", 'TouchPoint-WP'); + prompt.classList.add('error') + return false; } - tpvm.userEmail = data['email'] ?? null; Swal.showLoading(); - let result = await tpvm.postData('person/ident', data); - - // Handle an error if it happened. - if (result.hasOwnProperty('error')) { - Swal.hideLoading(); - Swal.update({ - icon: 'error', - title: __("Something went wrong.", 'TouchPoint-WP'), - text: result.error, - timer: 3000 - }); - return false; - } + return psn.doPersonContact(fromPerson, message, true); + } + }); + } + } - if (result.people.length > 0) { - return result; - } else { - Swal.hideLoading(); - Swal.update({ - html: `

${__("Our system doesn't recognize you,
so we need a little more info.", 'TouchPoint-WP')}

` + - '
' + - '
' + - '
' + - '
' + - '
' + - // '
' + - '
' + - '
' - }); + async doPersonContact(fromPerson, message, showConfirm = true) { + let psn = this; + showConfirm = !!showConfirm; + + tpvm._utils.ga('send', 'event', 'Person', 'contact complete', psn.peopleId); + + let res = await tpvm.postData('person/contact', { + toId: psn.peopleId, + fromPerson: fromPerson, + message: message, + fromEmail: tpvm.userEmail + }); + if (res.success.length > 0) { + if (showConfirm) { + Swal.fire({ + icon: 'success', + title: __('Your message has been sent.', 'TouchPoint-WP'), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); + } + } else { + console.error(res); + if (showConfirm) { + Swal.fire({ + icon: 'error', + title: __('Something strange happened.', 'TouchPoint-WP'), + timer: 3000, + customClass: tpvm._utils.defaultSwalClasses(), + confirmButtonText: __('OK', 'TouchPoint-WP') + }); + } + } + } + + /** + * + * @param array TP_Person[] + * @param id string + * @param name string + */ + static peopleArrayToSelect(array, id, name) { + let out = `" + } + + static async DoInformalAuth(title, forceAsk = false) { + return new Promise(function (resolve, reject) { + if (tpvm._plausibleUsers.length > 0 && !forceAsk) { + resolve(tpvm._plausibleUsers); + } else { + Swal.fire({ + html: `

${__('Tell us about yourself.', 'TouchPoint-WP')}

` + + '
' + + `
` + + `
` + + '
', + customClass: tpvm._utils.defaultSwalClasses(), + showConfirmButton: true, + showCancelButton: true, + title: title, + confirmButtonText: __('Next', 'TouchPoint-WP'), + cancelButtonText: __('Cancel', 'TouchPoint-WP'), + focusConfirm: false, + didOpen: () => { document.getElementById('tp_ident_form').addEventListener('submit', (e) => { Swal.clickConfirm(); e.preventDefault(); - }); - return false; + }) + }, + preConfirm: async () => { + let form = document.getElementById('tp_ident_form'), + inputs = form.querySelectorAll("input"), + data = {}; + form.checkValidity() + for (const ii in inputs) { + if (!inputs.hasOwnProperty(ii)) continue; + if (!inputs[ii].reportValidity()) { + return false; + } + let name = inputs[ii].name.replace("tp_ident_", ""); + if (name.length > 0) // removes entry generated by submit input + data[name] = inputs[ii].value; + } + tpvm.userEmail = data['email'] ?? null; + + Swal.showLoading(); + + let result = await tpvm.postData('person/ident', data); + + // Handle an error if it happened. + if (result.hasOwnProperty('error')) { + Swal.hideLoading(); + Swal.update({ + icon: 'error', + title: __("Something went wrong.", 'TouchPoint-WP'), + text: result.error, + timer: 3000 + }); + return false; + } + + if (result.people.length > 0) { + return result; + } else { + Swal.hideLoading(); + Swal.update({ + html: `

${__("Our system doesn't recognize you,
so we need a little more info.", 'TouchPoint-WP')}

` + + '
' + + '
' + + '
' + + '
' + + '
' + + // '
' + + '
' + + '
' + }); + document.getElementById('tp_ident_form').addEventListener('submit', (e) => { + Swal.clickConfirm(); + e.preventDefault(); + }); + return false; + } + } + }).then((result) => { + if (result.value) { + let ps = TP_Person.fromObjArray(result.value.people); + tpvm._plausibleUsers = TP_Person.mergePeopleArrays(tpvm._plausibleUsers, ps.filter((p) => result.value.primaryFam.indexOf(p.familyId) > -1)); + tpvm._secondaryUsers = TP_Person.mergePeopleArrays(tpvm._secondaryUsers, ps.filter((p) => result.value.primaryFam.indexOf(p.familyId) === -1)); } - } - }).then((result) => { - if (result.value) { - let ps = TP_Person.fromObjArray(result.value.people); - tpvm._plausibleUsers = TP_Person.mergePeopleArrays(tpvm._plausibleUsers, ps.filter((p) => result.value.primaryFam.indexOf(p.familyId) > -1)); - tpvm._secondaryUsers = TP_Person.mergePeopleArrays(tpvm._secondaryUsers, ps.filter((p) => result.value.primaryFam.indexOf(p.familyId) === -1)); - } - if (result.isDismissed) { - reject(false); - } else { - resolve(tpvm._plausibleUsers); - } - }); - } - }); + if (result.isDismissed) { + reject(false); + } else { + resolve(tpvm._plausibleUsers); + } + }); + } + }); + } } -} -TP_Person.init(); \ No newline at end of file + tpvm.TP_Person = TP_Person; + TP_Person.init(); + +})(); \ No newline at end of file diff --git a/assets/js/meeting-defer.js b/assets/js/meeting-defer.js index 5e0d383a..c21c6489 100644 --- a/assets/js/meeting-defer.js +++ b/assets/js/meeting-defer.js @@ -22,7 +22,7 @@ class TP_Meeting { this.location = obj.location; this.capacity = obj.capacity; - this.inv = TP_Involvement.fromObjArray([{name: obj.invName, invId: obj.invId}])[0]; + this.inv = tpvm.TP_Involvement.fromObjArray([{name: obj.invName, invId: obj.invId}])[0]; for (const ei in this.connectedElements) { if (!this.connectedElements.hasOwnProperty(ei)) continue; diff --git a/assets/js/partner-defer.js b/assets/js/partner-defer.js index c9793122..f27604cf 100644 --- a/assets/js/partner-defer.js +++ b/assets/js/partner-defer.js @@ -1,162 +1,165 @@ -class TP_Partner extends TP_Mappable { - attributes = {}; +(function() { + class TP_Partner extends tpvm.TP_Mappable { + attributes = {}; - static currentFilters = {}; + static currentFilters = {}; - static actions = []; + static actions = []; - constructor(obj) { - super(obj); + constructor(obj) { + super(obj); - this.attributes = obj.attributes ?? null; + this.attributes = obj.attributes ?? null; - tpvm.partners[this.post_id] = this; - } + tpvm.partners[this.post_id] = this; + } - get shortClass() { - return "gp"; - } + get shortClass() { + return "gp"; + } - // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation. - static fromObjArray(pArr) { - let ret = []; - for (const p in pArr) { - if (!pArr.hasOwnProperty(p)) continue; + // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation. + static fromObjArray(pArr) { + let ret = []; + for (const p in pArr) { + if (!pArr.hasOwnProperty(p)) continue; - let pid = 0; - if (typeof pArr[p].post_id !== "undefined") { - pid = pArr[p].post_id; - } + let pid = 0; + if (typeof pArr[p].post_id !== "undefined") { + pid = pArr[p].post_id; + } - if (typeof tpvm.partners[pid] === "undefined") { - ret.push(new this(pArr[p])); - } else { - // Partner exists, and probably needs something added to it. (This should only happen for secure partners.) - if (pArr[p].geo !== undefined) { - tpvm.partners[pid].geo.push(pArr[p].geo) + if (typeof tpvm.partners[pid] === "undefined") { + ret.push(new this(pArr[p])); + } else { + // Partner exists, and probably needs something added to it. (This should only happen for secure partners.) + if (pArr[p].geo !== undefined) { + tpvm.partners[pid].geo.push(pArr[p].geo) + } + ret.push(tpvm.partners[pid]); } - ret.push(tpvm.partners[pid]); } - } - tpvm.trigger("Partner_fromObjArray"); - return ret; - }; - - get useIcon() { - if (this.post_id === 0) { - return { - text: " ", - fontFamily: "\"Font Awesome 6 Free\", FontAwesome", - color: "#00000088", - fontSize: "90%", - className: "fa fa-solid fa-lock" + tpvm.trigger("Partner_fromObjArray"); + return ret; + }; + + get useIcon() { + if (this.post_id === 0) { + return { + text: " ", + fontFamily: "\"Font Awesome 6 Free\", FontAwesome", + color: "#00000088", + fontSize: "90%", + className: "fa fa-solid fa-lock" + } } + return false; } - return false; - } - - get highlightable() { - return this.post_id !== 0; - } - // noinspection JSUnusedGlobalSymbols Called by inline. - static initFilters() { - const filtOptions = document.querySelectorAll("[data-partner-filter]"); - for (const ei in filtOptions) { - if (!filtOptions.hasOwnProperty(ei)) continue; - filtOptions[ei].addEventListener('change', this.applyFilters.bind(this)) + get highlightable() { + return this.post_id !== 0; } - } - static applyFilters(ev = null) { - if (ev !== null) { - let attr = ev.target.getAttribute("data-partner-filter"), - val = ev.target.value; - if (attr !== null) { - if (val === "") { - delete this.currentFilters[attr]; - } else { - this.currentFilters[attr] = val; - } + // noinspection JSUnusedGlobalSymbols Called by inline. + static initFilters() { + const filtOptions = document.querySelectorAll("[data-partner-filter]"); + for (const ei in filtOptions) { + if (!filtOptions.hasOwnProperty(ei)) continue; + filtOptions[ei].addEventListener('change', this.applyFilters.bind(this)) } } - itemLoop: - for (const ii in tpvm.partners) { - if (!tpvm.partners.hasOwnProperty(ii)) continue; - const item = tpvm.partners[ii]; - for (const ai in this.currentFilters) { - if (!this.currentFilters.hasOwnProperty(ai)) continue; - - if (!item.attributes.hasOwnProperty(ai) || - item.attributes[ai] === null || - ( !Array.isArray(item.attributes[ai]) && - item.attributes[ai].slug !== this.currentFilters[ai] && - item.attributes[ai] !== this.currentFilters[ai] - ) || ( - Array.isArray(item.attributes[ai]) && - item.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined - ) - ) { - item.toggleVisibility(false) - continue itemLoop; + static applyFilters(ev = null) { + if (ev !== null) { + let attr = ev.target.getAttribute("data-partner-filter"), + val = ev.target.value; + if (attr !== null) { + if (val === "") { + delete this.currentFilters[attr]; + } else { + this.currentFilters[attr] = val; } } - item.toggleVisibility(true) } - TP_Mappable.updateFilterWarnings(); - } - get visibility() { - return this._visible; - } + itemLoop: + for (const ii in tpvm.partners) { + if (!tpvm.partners.hasOwnProperty(ii)) continue; + const item = tpvm.partners[ii]; + for (const ai in this.currentFilters) { + if (!this.currentFilters.hasOwnProperty(ai)) continue; + + if (!item.attributes.hasOwnProperty(ai) || + item.attributes[ai] === null || + (!Array.isArray(item.attributes[ai]) && + item.attributes[ai].slug !== this.currentFilters[ai] && + item.attributes[ai] !== this.currentFilters[ai] + ) || ( + Array.isArray(item.attributes[ai]) && + item.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined + ) + ) { + item.toggleVisibility(false) + continue itemLoop; + } + } + item.toggleVisibility(true) + } + TP_Mappable.updateFilterWarnings(); + } - static init() { - tpvm.trigger('Partner_class_loaded'); - } + get visibility() { + return this._visible; + } - static initMap(mapDivId) { - let mapOptions = { - mapTypeId: google.maps.MapTypeId.HYBRID, - linksControl: false, - maxZoom: 10, - minZoom: 2, - panControl: false, - addressControl: false, - enableCloseButton: false, - mapTypeControl: false, - zoomControl: false, - gestureHandling: 'greedy', - styles: [ - { - featureType: "poi", //points of interest - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "road", - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "transit", - stylers: [ - {visibility: 'off'} - ] - } - ], - zoom: 6, - center: {lat: 0, lng: 0}, // gets overwritten by bounds later. - streetViewControl: false, - fullscreenControl: false, - disableDefaultUI: true - }; + static init() { + tpvm.trigger('Partner_class_loaded'); + } - super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.partners) - } + static initMap(mapDivId) { + let mapOptions = { + mapTypeId: google.maps.MapTypeId.HYBRID, + linksControl: false, + maxZoom: 10, + minZoom: 2, + panControl: false, + addressControl: false, + enableCloseButton: false, + mapTypeControl: false, + zoomControl: false, + gestureHandling: 'greedy', + styles: [ + { + featureType: "poi", //points of interest + stylers: [ + {visibility: 'off'} + ] + }, + { + featureType: "road", + stylers: [ + {visibility: 'off'} + ] + }, + { + featureType: "transit", + stylers: [ + {visibility: 'off'} + ] + } + ], + zoom: 6, + center: {lat: 0, lng: 0}, // gets overwritten by bounds later. + streetViewControl: false, + fullscreenControl: false, + disableDefaultUI: true + }; + + super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.partners) + } -} -TP_Partner.init(); \ No newline at end of file + } + tpvm.TP_Partner = TP_Partner; + TP_Partner.init(); +})(); \ No newline at end of file diff --git a/src/TouchPoint-WP/Involvement.php b/src/TouchPoint-WP/Involvement.php index 41e4d5f8..2219f7c8 100644 --- a/src/TouchPoint-WP/Involvement.php +++ b/src/TouchPoint-WP/Involvement.php @@ -1396,7 +1396,7 @@ public static function filterShortcode($params = []): string // language=javascript " tpvm.addEventListener('Involvement_fromObjArray', function() { - TP_Involvement.initFilters(); + tpvm.TP_Involvement.initFilters(); });" ); self::$filterJsAdded = true; @@ -4010,7 +4010,7 @@ public static function getJsInstantiationString(): string $listStr = json_encode($queue); return "\ttpvm.addEventListener('Involvement_class_loaded', function() { - TP_Involvement.fromObjArray($listStr);\n\t});\n"; + tpvm.TP_Involvement.fromObjArray($listStr);\n\t});\n"; } public function getTouchPointId(): int diff --git a/src/TouchPoint-WP/Meeting.php b/src/TouchPoint-WP/Meeting.php index 5cdd5d55..48cb21b2 100644 --- a/src/TouchPoint-WP/Meeting.php +++ b/src/TouchPoint-WP/Meeting.php @@ -677,7 +677,7 @@ public static function getJsInstantiationString(): string return ""; // TODO someday, probably. // return "\ttpvm.addEventListener('Involvement_class_loaded', function() { -// TP_Involvement.fromObjArray($listStr);\n\t});\n"; +// tpvm.TP_Involvement.fromObjArray($listStr);\n\t});\n"; } /** diff --git a/src/TouchPoint-WP/Partner.php b/src/TouchPoint-WP/Partner.php index 6fd42d1a..cd4e350b 100644 --- a/src/TouchPoint-WP/Partner.php +++ b/src/TouchPoint-WP/Partner.php @@ -795,7 +795,7 @@ public static function filterShortcode($params = []): string // language=javascript " tpvm.addEventListener('Partner_fromObjArray', function() { - TP_Partner.initFilters(); + tpvm.TP_Partner.initFilters(); });" ); self::$filterJsAdded = true; @@ -1380,7 +1380,7 @@ public static function getJsInstantiationString(): string $listStr = json_encode($queue); return "\ttpvm.addEventListener('Partner_class_loaded', function() { - TP_Partner.fromObjArray($listStr);\n\t});\n"; + tpvm.TP_Partner.fromObjArray($listStr);\n\t});\n"; } /** diff --git a/src/TouchPoint-WP/Person.php b/src/TouchPoint-WP/Person.php index c6855aa9..a980e0bb 100644 --- a/src/TouchPoint-WP/Person.php +++ b/src/TouchPoint-WP/Person.php @@ -1318,7 +1318,7 @@ public static function getJsInstantiationString(): string $listStr = json_encode($queue); $out = "\ttpvm.addOrTriggerEventListener('Person_class_loaded', function() {\n"; - $out .= "\t\tTP_Person.fromObjArray($listStr);\n"; + $out .= "\t\ttpvm.TP_Person.fromObjArray($listStr);\n"; // TODO restore, better. // if (self::$_enqueueUsersForJsInstantiation) { diff --git a/src/TouchPoint-WP/TouchPointWP.php b/src/TouchPoint-WP/TouchPointWP.php index 385478ae..81931f9f 100644 --- a/src/TouchPoint-WP/TouchPointWP.php +++ b/src/TouchPoint-WP/TouchPointWP.php @@ -935,7 +935,8 @@ public function registerScriptsAndStyles(): void ); wp_set_script_translations( self::SHORTCODE_PREFIX . 'base-defer', - 'TouchPoint-WP', $this->getJsLocalizationDir() + 'TouchPoint-WP', + $this->getJsLocalizationDir() ); wp_register_script( @@ -973,7 +974,7 @@ public function registerScriptsAndStyles(): void wp_register_script( TouchPointWP::SHORTCODE_PREFIX . "googleMaps", sprintf( - "https://maps.googleapis.com/maps/api/js?key=%s&v=3&libraries=geometry&language=$lang", + "https://maps.googleapis.com/maps/api/js?key=%s&v=3&loading=async&libraries=geometry&language=$lang", TouchPointWP::instance()->settings->google_maps_api_key ), [TouchPointWP::SHORTCODE_PREFIX . "base-defer"], @@ -1085,7 +1086,7 @@ public static function requireStyle(?string $name = null): void public function filterByTag(?string $tag, ?string $handle): string { if (!str_contains($tag, ' async') && - strpos($handle, '-async') > 0 + strpos($handle, '-async') ) { $tag = str_replace(' src=', ' async="async" src=', $tag); } diff --git a/src/js-partials/involvement-map-inline.js b/src/js-partials/involvement-map-inline.js index c7c6a05c..6c3d1427 100644 --- a/src/js-partials/involvement-map-inline.js +++ b/src/js-partials/involvement-map-inline.js @@ -1,3 +1,3 @@ tpvm.addEventListener('Involvement_fromObjArray', function() { - TP_Involvement.initMap('{$mapDivId}'); + tpvm.TP_Involvement.initMap('{$mapDivId}'); }); \ No newline at end of file diff --git a/src/js-partials/partner-map-inline.js b/src/js-partials/partner-map-inline.js index a3d4e524..4dcf8985 100644 --- a/src/js-partials/partner-map-inline.js +++ b/src/js-partials/partner-map-inline.js @@ -1,3 +1,3 @@ tpvm.addEventListener('Partner_fromObjArray', function() { - TP_Partner.initMap('{$mapDivId}'); + tpvm.TP_Partner.initMap('{$mapDivId}'); }); \ No newline at end of file From 76332707facb30e5a06e1af09250dd5352af9f41 Mon Sep 17 00:00:00 2001 From: "James K." Date: Thu, 5 Jun 2025 18:11:44 -0400 Subject: [PATCH 10/83] Reworking usage of Google Maps library to account for API changes. Closes #220. --- assets/js/base-defer.js | 134 ++++++++------------ assets/js/partner-defer.js | 35 ++--- assets/template/partials-template-style.css | 8 ++ src/TouchPoint-WP/TouchPointWP.php | 2 +- 4 files changed, 69 insertions(+), 110 deletions(-) diff --git a/assets/js/base-defer.js b/assets/js/base-defer.js index cff806d6..13030a6a 100644 --- a/assets/js/base-defer.js +++ b/assets/js/base-defer.js @@ -331,25 +331,36 @@ geoStr = ""; /** - * @type {google.maps.Marker} + * @type {google.maps.marker.AdvancedMarkerElement} */ gMkr = null; + map = null; constructor(options) { - if (!options.hasOwnProperty('icon')) { - options.icon = { - path: "M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z", // from FontAwesome - fillColor: options.color ?? "#000", - fillOpacity: .85, - anchor: new google.maps.Point(172.268, 501.67), - strokeWeight: 1, - scale: 0.04, - labelOrigin: new google.maps.Point(190, 198) - } + this.color = options.color ?? "#000"; + this.map = options.map; + + if (options.hasOwnProperty('color')) { + delete options.color; // can't be passed to AdvancedMarkerElement + } + + if (!options.hasOwnProperty('content')) { + const label = document.createElement('div'); + label.classList.add("map-marker-label"); + const pin = new google.maps.marker.PinElement({ + glyph: label, + glyphColor: "#000", + background: this.color, + borderColor: "#000", + scale: .65, + }); + options.content = pin.element; } - this.gMkr = new google.maps.Marker(options); + + this.gMkr = new google.maps.marker.AdvancedMarkerElement(options); let that = this; - this.gMkr.addListener("click", () => that.handleClick()); + this.gMkr.addListener("gmp-click", () => that.handleClick()); + this.gMkr.content.classList.add("tp-map-marker"); } get visibleItems() { @@ -361,11 +372,11 @@ } get inBounds() { - let map = this.gMkr.getMap(); + let map = this.gMkr.map; if (!map) { // if map failed to render for some reason, this prevents entries from being hidden. return true; } - return map.getBounds().contains(this.gMkr.getPosition()); + return map.getBounds().contains(this.gMkr.position); } get useIcon() { @@ -381,41 +392,31 @@ return; } - let icon = this.gMkr.getIcon(); + let icon = this.gMkr.content; // Update icon color - this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color)) + this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color)); if (icon !== undefined && icon.hasOwnProperty("fillColor")) { icon.fillColor = this.color; - this.gMkr.setIcon(icon); + this.gMkr.content.style.backgroundColor = this.color; // Update AdvancedMarkerElement content style } // Update visibility - this.gMkr.setVisible(this.visibleItems.length > 0); + this.gMkr.map = (this.visibleItems.length > 0 ? this.map : null); // Update title - this.gMkr.setTitle(tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name))) + this.gMkr.title = tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name)); - // Update label proper - if (highlighted) { - this.gMkr.setLabel(null); // Remove label if highlighted, because labels don't animate. - } else { - this.gMkr.setLabel(this.getLabelContent()); - } + this.gMkr.content.getElementsByTagName('div')[0].innerHTML = this.getLabelContent() || ""; // Set label content } getLabelContent() { - let label = null; if (this.visibleItems.length > 1) { - label = { - text: this.visibleItems.length.toString(), - color: "#000000", - fontSize: "100%" - } + return this.visibleItems.length.toString(); } else if (this.useIcon !== false) { // icon for secure partners - label = this.useIcon; + return this.useIcon; } - return label; + return null; } // noinspection JSUnusedGlobalSymbols Used dynamically from markers. @@ -426,8 +427,8 @@ tpvm._utils.clearHash(); - const mp = this.gMkr.getMap(); - TP_MapMarker.smoothZoom(mp, this.gMkr.getPosition()).then(() => 1) + const mp = this.gMkr.map; + TP_MapMarker.smoothZoom(mp, this.gMkr.position).then(() => 1) tpvm._utils.ga('send', 'event', this.items[0].itemTypeName, 'mapMarker click', this.gMkr.getTitle()); } @@ -451,12 +452,12 @@ }); if (map.getZoom() < zoomTo) { // zoom in map.setZoom(map.getZoom() + 1); - } else { // zoom out + } else if (map.getZoom() > zoomTo) { // zoom out map.setZoom(map.getZoom() - 1); } if (position !== null) { let oldPos = map.getCenter(), - newPos = new google.maps.LatLng((oldPos.lat() + position.lat() * 2) / 3, (oldPos.lng() + position.lng() * 2) / 3); + newPos = new google.maps.LatLng((oldPos.lat() + position.lat * 2) / 3, (oldPos.lng() + position.lng * 2) / 3); map.panTo(newPos); } } else { @@ -583,7 +584,7 @@ const item = list[ii], geoStr = "" + item.geo[gi].lat + "," + item.geo[gi].lng; - let mkr = this.markers.find((m) => m.gMkr.getMap() === map && m.geoStr === geoStr); + let mkr = this.markers.find((m) => m.gMkr.map === map && m.geoStr === geoStr); // If there isn't already a marker for the item on the right map, create one. if (mkr === undefined) { @@ -591,7 +592,6 @@ position: item.geo[gi], color: item.color, map: map, - animation: google.maps.Animation.DROP, }); mkr.geoStr = geoStr; @@ -599,7 +599,7 @@ this.markers.push(mkr); } - bounds.extend(mkr.gMkr.getPosition()); + bounds.extend(mkr.gMkr.position); // If the marker doesn't already have a reference to this item, add one. if (!mkr.items.includes(item)) { @@ -621,6 +621,7 @@ } map.fitBounds(bounds); + let originalZoom = map.getZoom(); map.addListener('bounds_changed', this.handleZoom); @@ -636,15 +637,6 @@ } } - // noinspection JSUnusedGlobalSymbols Used dynamically from warning text. - /** - * - * @param {google.maps.Map} map - */ - static resetMap(map) { - console.log("reset " + map.getMapTypeId()) - } - /** * Currently, this will apply visibility to ALL mappable items, even if they're on a different map. */ @@ -695,12 +687,12 @@ // One marker (probably typical) if (this.markers.length === 1) { - let mp = this.markers[0].gMkr.getMap(), + let mp = this.markers[0].gMkr.map, el = mp.getDiv(), rect = el.getBoundingClientRect(), viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight), mpWithinView = !(rect.bottom < 0 || rect.top - viewHeight >= 0); - TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.getPosition()).then(() => 1); + TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.position).then(() => 1); if (!mpWithinView) { window.scroll({ top: rect.top, @@ -796,8 +788,8 @@ for (let mi in this.markers) { const mk = item.markers[mi]; if (TP_Mappable.items.length > 1) { - if (mk.gMkr.getAnimation() !== google.maps.Animation.BOUNCE) { - mk.gMkr.setAnimation(google.maps.Animation.BOUNCE); + if (!mk.gMkr.content.classList.contains("highlighted-marker")) { + mk.gMkr.content.classList.add("highlighted-marker"); } } mk.updateLabel(this.highlighted) @@ -805,8 +797,8 @@ } else { for (const mi in this.markers) { let mk = this.markers[mi]; - if (mk.gMkr.getAnimation() !== null) { - mk.gMkr.setAnimation(null) + if (mk.gMkr.content.classList.contains("highlighted-marker")) { + mk.gMkr.content.classList.remove("highlighted-marker"); } mk.updateLabel(this.highlighted) } @@ -1036,10 +1028,6 @@ } } - get itemTypeName() { - return this.invType; - } - // noinspection JSUnusedGlobalSymbols Used dynamically from btns. contactAction() { let inv = this, @@ -1093,8 +1081,9 @@ static initMap(mapDivId) { let mapOptions = { mapTypeId: google.maps.MapTypeId.ROADMAP, + mapId: "f0fb8ca5f6beff5288b80a8d", linksControl: false, - maxZoom: 15, + maxZoom: 16, minZoom: 2, panControl: false, addressControl: false, @@ -1102,26 +1091,6 @@ mapTypeControl: false, zoomControl: false, gestureHandling: 'greedy', - styles: [ - { - featureType: "poi", //points of interest - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "road", - stylers: [ - {visibility: 'on'} - ] - }, - { - featureType: "transit", - stylers: [ - {visibility: 'on'} - ] - } - ], zoom: 15, center: {lat: 0, lng: 0}, // gets overwritten by bounds later. streetViewControl: false, @@ -1560,4 +1529,5 @@ tpvm.TP_Person = TP_Person; TP_Person.init(); -})(); \ No newline at end of file +})(); + diff --git a/assets/js/partner-defer.js b/assets/js/partner-defer.js index f27604cf..90d55264 100644 --- a/assets/js/partner-defer.js +++ b/assets/js/partner-defer.js @@ -44,19 +44,19 @@ return ret; }; + // noinspection JSUnusedGlobalSymbols get useIcon() { if (this.post_id === 0) { - return { - text: " ", - fontFamily: "\"Font Awesome 6 Free\", FontAwesome", - color: "#00000088", - fontSize: "90%", - className: "fa fa-solid fa-lock" - } + return ""; } return false; } + // noinspection JSUnusedGlobalSymbols + /** + * @overrides tpvm.TP_Mappable + * @returns {boolean} + */ get highlightable() { return this.post_id !== 0; } @@ -120,6 +120,7 @@ static initMap(mapDivId) { let mapOptions = { mapTypeId: google.maps.MapTypeId.HYBRID, + mapId: 'f0fb8ca5f6beff5237d51d79', linksControl: false, maxZoom: 10, minZoom: 2, @@ -129,26 +130,6 @@ mapTypeControl: false, zoomControl: false, gestureHandling: 'greedy', - styles: [ - { - featureType: "poi", //points of interest - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "road", - stylers: [ - {visibility: 'off'} - ] - }, - { - featureType: "transit", - stylers: [ - {visibility: 'off'} - ] - } - ], zoom: 6, center: {lat: 0, lng: 0}, // gets overwritten by bounds later. streetViewControl: false, diff --git a/assets/template/partials-template-style.css b/assets/template/partials-template-style.css index b24cd20d..cdd0732d 100644 --- a/assets/template/partials-template-style.css +++ b/assets/template/partials-template-style.css @@ -181,6 +181,14 @@ h1.archive-title.page-title { text-align: inherit; } +@keyframes bouncing-marker { + from { transform: translate3d(0, 0, 0);} + to { transform: translate3d(0, -15px, 0);} +} +.tp-map-marker.highlighted-marker { + animation: bouncing-marker .5s ease-in-out infinite alternate; +} + article .section-inner.TouchPointWP-detail { display: flex; } diff --git a/src/TouchPoint-WP/TouchPointWP.php b/src/TouchPoint-WP/TouchPointWP.php index 81931f9f..f8614a83 100644 --- a/src/TouchPoint-WP/TouchPointWP.php +++ b/src/TouchPoint-WP/TouchPointWP.php @@ -974,7 +974,7 @@ public function registerScriptsAndStyles(): void wp_register_script( TouchPointWP::SHORTCODE_PREFIX . "googleMaps", sprintf( - "https://maps.googleapis.com/maps/api/js?key=%s&v=3&loading=async&libraries=geometry&language=$lang", + "https://maps.googleapis.com/maps/api/js?key=%s&v=3&loading=async&libraries=geometry,marker&language=$lang", TouchPointWP::instance()->settings->google_maps_api_key ), [TouchPointWP::SHORTCODE_PREFIX . "base-defer"], From a40525f428c92e2067bdeab087a9e0279ff68c80 Mon Sep 17 00:00:00 2001 From: "James K." Date: Thu, 5 Jun 2025 18:26:24 -0400 Subject: [PATCH 11/83] Changing people lists to not have hover behavior if there are no actions. Closes #229 --- .idea/libraries/Generated_files.xml | 6 ++++++ assets/template/partials-template-style.css | 4 ++-- src/TouchPoint-WP/Person.php | 4 ++-- src/templates/parts/person-list-item.php | 12 ++++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.idea/libraries/Generated_files.xml b/.idea/libraries/Generated_files.xml index 2250062c..11b1f82e 100644 --- a/.idea/libraries/Generated_files.xml +++ b/.idea/libraries/Generated_files.xml @@ -5,12 +5,18 @@ + + + + + + diff --git a/assets/template/partials-template-style.css b/assets/template/partials-template-style.css index cdd0732d..429b627e 100644 --- a/assets/template/partials-template-style.css +++ b/assets/template/partials-template-style.css @@ -74,7 +74,7 @@ article.inv-list-item h2 a { opacity: 0; } -.person-list-item:hover .person-actions { +.person-list-item.has-actions:hover .person-actions { opacity: 1; } @@ -136,7 +136,7 @@ div.inv-list article.inv-list-item h2.entry-title { text-shadow: 0 0 3px #000, 0 0 5px #000; } -.person-list-item:hover header.entry-header > * { +.person-list-item.has-actions:hover header.entry-header > * { opacity: 0; } diff --git a/src/TouchPoint-WP/Person.php b/src/TouchPoint-WP/Person.php index a980e0bb..5a520cf1 100644 --- a/src/TouchPoint-WP/Person.php +++ b/src/TouchPoint-WP/Person.php @@ -1170,9 +1170,9 @@ protected function resCode(): ?WP_Term * elements. * @param bool $withTouchPointLink * - * @return string + * @return StringableArray */ - public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true): string + public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true): StringableArray { TouchPointWP::requireScript('swal2-defer'); TouchPointWP::requireScript('base-defer'); diff --git a/src/templates/parts/person-list-item.php b/src/templates/parts/person-list-item.php index e15eb78a..f8277b8d 100644 --- a/src/templates/parts/person-list-item.php +++ b/src/templates/parts/person-list-item.php @@ -12,9 +12,17 @@ $image = " style=\"background-image: url('$image');\""; } +$actions = $person->getActionButtons("person-list", $btnClass); +$classList = "person-list-item"; +if ($actions->count() === 0) { + $classList .= " no-actions"; +} else { + $classList .= " has-actions"; +} + ?> -
> +
>
description, 20, "..."); ?>
- getActionButtons("person-list", $btnClass); ?> +
\ No newline at end of file From 03adec811b273fec65792addc4ce729cfd16827c Mon Sep 17 00:00:00 2001 From: "James K." Date: Thu, 5 Jun 2025 18:26:38 -0400 Subject: [PATCH 12/83] Missed a scoping issue. --- src/js-partials/involvement-nearby-inline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js-partials/involvement-nearby-inline.js b/src/js-partials/involvement-nearby-inline.js index 7e0cc30d..c4b5614c 100644 --- a/src/js-partials/involvement-nearby-inline.js +++ b/src/js-partials/involvement-nearby-inline.js @@ -1,3 +1,3 @@ tpvm.addEventListener('load', function() { - TP_Involvement.initNearby('{$nearbyListId}', '{$type}','{$count}'); + tpvm.TP_Involvement.initNearby('{$nearbyListId}', '{$type}','{$count}'); }); \ No newline at end of file From 835c7f02b39a3f93168ab83f953b137f5dce3c44 Mon Sep 17 00:00:00 2001 From: "James K." Date: Fri, 6 Jun 2025 00:10:16 -0400 Subject: [PATCH 13/83] Making tp_person_actions filter more flexible. --- src/TouchPoint-WP/Person.php | 11 ++++++++++- src/TouchPoint-WP/TouchPointWP.php | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/TouchPoint-WP/Person.php b/src/TouchPoint-WP/Person.php index 5a520cf1..cdd2c859 100644 --- a/src/TouchPoint-WP/Person.php +++ b/src/TouchPoint-WP/Person.php @@ -1202,6 +1202,7 @@ public function getActionButtons(?string $context = null, string $btnClass = "", * on the Person to allow the user to interact with them. * * @since 0.0.90 Added + * @since 0.0.96 Adjusted parameters and return value for StringableArray rather than string. * * @see Person::getActionButtons() * @@ -1211,7 +1212,15 @@ public function getActionButtons(?string $context = null, string $btnClass = "", * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be 'a' or 'button' * elements. */ - return apply_filters("tp_person_actions", $ret, $this, $context, $btnClass); + $ret = apply_filters("tp_person_actions", $ret, $this, $context, $btnClass); + + if ($ret instanceof StringableArray) { + return $ret; + } + + $r = new StringableArray(); + $r->append($ret); + return $r; } /** diff --git a/src/TouchPoint-WP/TouchPointWP.php b/src/TouchPoint-WP/TouchPointWP.php index f8614a83..6fbe7eec 100644 --- a/src/TouchPoint-WP/TouchPointWP.php +++ b/src/TouchPoint-WP/TouchPointWP.php @@ -69,7 +69,7 @@ class TouchPointWP */ public const HOOK_PREFIX = "tp_"; - public const INIT_ACTION_HOOK = "tp_init"; // Note that this is also hard-coded where the action is declared. + public const INIT_ACTION_HOOK = "tp_init"; // Note that this is also hard-coded where the action is called. /** * Prefix to use for all settings. @@ -901,7 +901,7 @@ public static function init(): void /** * Fires after the plugin has been initialized. */ - do_action(self::INIT_ACTION_HOOK); + do_action("tp_init"); // needs to be hard-coded for documenter } /** From fe3e10c681561325f4c1a3dfb275b49d24514428 Mon Sep 17 00:00:00 2001 From: "James K." Date: Fri, 6 Jun 2025 22:07:23 -0400 Subject: [PATCH 14/83] Adding actionButtons.php interface and making getActionButtons implementations more consistent. --- .../Interfaces/actionButtons.php | 24 +++++++++++ src/TouchPoint-WP/Involvement.php | 2 +- src/TouchPoint-WP/Meeting.php | 2 +- src/TouchPoint-WP/Partner.php | 4 +- src/TouchPoint-WP/Person.php | 41 +++++++++++++------ src/TouchPoint-WP/PostTypeCapable.php | 15 ++----- 6 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 src/TouchPoint-WP/Interfaces/actionButtons.php diff --git a/src/TouchPoint-WP/Interfaces/actionButtons.php b/src/TouchPoint-WP/Interfaces/actionButtons.php new file mode 100644 index 00000000..40fb2c85 --- /dev/null +++ b/src/TouchPoint-WP/Interfaces/actionButtons.php @@ -0,0 +1,24 @@ +host(); - // Translators: %s is the system name. "TouchPoint" by default. + // Translators: %s is the system name, "TouchPoint" by default. $title = wp_sprintf(__("Involvement in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name); $logo = TouchPointWP::TouchPointIcon(); $ret['inv_tp'] = "invId\" title=\"$title\" class=\"tp-TouchPoint-logo $classesOnly\">$logo"; diff --git a/src/TouchPoint-WP/Meeting.php b/src/TouchPoint-WP/Meeting.php index 48cb21b2..569210bb 100644 --- a/src/TouchPoint-WP/Meeting.php +++ b/src/TouchPoint-WP/Meeting.php @@ -523,7 +523,7 @@ public function getActionButtons(?string $context = null, string $btnClass = "", if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin() && !$this->isMeetingGroup()) { $tpHost = TouchPointWP::instance()->host(); - // Translators: %s is the system name. "TouchPoint" by default. + // Translators: %s is the system name, "TouchPoint" by default. $title = wp_sprintf(__("Meeting in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name); $logo = TouchPointWP::TouchPointIcon(); $ret['mtg_tp'] = "mtgId\" title=\"$title\" class=\"tp-TouchPoint-logo $btnClass\">$logo"; diff --git a/src/TouchPoint-WP/Partner.php b/src/TouchPoint-WP/Partner.php index cd4e350b..3b57fd6d 100644 --- a/src/TouchPoint-WP/Partner.php +++ b/src/TouchPoint-WP/Partner.php @@ -222,7 +222,7 @@ public static function init(): void 'hierarchical' => false, 'show_ui' => false, 'show_in_nav_menus' => true, - 'show_in_rest' => false, // For the benefit of secure partners + 'show_in_rest' => true, // For the benefit of secure partners 'supports' => [ 'title', 'custom-fields', @@ -1326,7 +1326,7 @@ protected function enqueueForJsInstantiation(): bool * * @param string|null $context A string that gives filters some context for where the request is coming from * @param string $btnClass HTML class names to put into the buttons/links - * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint. + * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint. (not used) * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails. * * @return StringableArray diff --git a/src/TouchPoint-WP/Person.php b/src/TouchPoint-WP/Person.php index cdd2c859..d9d5d61d 100644 --- a/src/TouchPoint-WP/Person.php +++ b/src/TouchPoint-WP/Person.php @@ -13,6 +13,7 @@ require_once "Interfaces/api.php"; require_once "extraValues.php"; require_once "jsInstantiation.php"; + require_once "Interfaces/actionButtons.php"; require_once "Interfaces/updatesViaCron.php"; require_once "InvolvementMembership.php"; require_once "Utilities.php"; @@ -22,6 +23,7 @@ use Exception; use JsonSerializable; use stdClass; +use tp\TouchPointWP\Interfaces\actionButtons; use tp\TouchPointWP\Interfaces\api; use tp\TouchPointWP\Interfaces\module; use tp\TouchPointWP\Interfaces\updatesViaCron; @@ -43,7 +45,7 @@ * @property-read ?WP_Term resCode The ResCode taxonomy, if present * @property ?int rescode_term_id The ResCode term ID */ -class Person extends WP_User implements api, JsonSerializable, module, updatesViaCron +class Person extends WP_User implements api, JsonSerializable, module, updatesViaCron, actionButtons { use jsInstantiation; use extraValues; @@ -1163,38 +1165,50 @@ protected function resCode(): ?WP_Term /** * Returns the html with buttons for actions the user can perform. This must be called *within* an element with - * the `data-tp-person` attribute with the invId as the value. + * the `data-tp-person` attribute with the peopleId as the value. * * @param ?string $context A reference to where the action buttons are meant to be used. * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be a or button * elements. * @param bool $withTouchPointLink + * @param bool $absoluteLinks * * @return StringableArray */ - public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true): StringableArray + public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray { - TouchPointWP::requireScript('swal2-defer'); - TouchPointWP::requireScript('base-defer'); - $this->enqueueForJsInstantiation(); + if (!$absoluteLinks) { + TouchPointWP::requireScript('swal2-defer'); + TouchPointWP::requireScript('base-defer'); + $this->enqueueForJsInstantiation(); + Person::enqueueUsersForJsInstantiation(); + } + $classesOnly = $btnClass; if ($btnClass !== "") { $btnClass = " class=\"$btnClass\""; } + global $wp; + $baseLink = add_query_arg($wp->query_vars, home_url($wp->request)); $ret = new StringableArray(); if (self::allowContact()) { $text = __("Contact", "TouchPoint-WP"); - TouchPointWP::enqueueActionsStyle('person-contact'); - self::enqueueUsersForJsInstantiation(); - $ret[] = " "; + $pid = $this->peopleId; + if (!$absoluteLinks) { + $ret['contact'] = " "; + TouchPointWP::enqueueActionsStyle('person-contact'); + self::enqueueUsersForJsInstantiation(); + } else { + $ret['contact'] = "$text "; + } } if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin()) { - // Translators: %s is the system name. "TouchPoint" by default. - $title = sprintf(__("Person in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name); + // Translators: %s is the system name, "TouchPoint" by default. + $title = wp_sprintf(__("Person in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name); $logo = TouchPointWP::TouchPointIcon(); - $ret[] = "getProfileUrl()}\" title=\"$title\" class=\"tp-TouchPoint-logo $btnClass\">$logo"; + $ret['inv_tp'] = "getProfileUrl()}\" title=\"$title\" class=\"tp-TouchPoint-logo $classesOnly\">$logo"; } /** @@ -1202,7 +1216,8 @@ public function getActionButtons(?string $context = null, string $btnClass = "", * on the Person to allow the user to interact with them. * * @since 0.0.90 Added - * @since 0.0.96 Adjusted parameters and return value for StringableArray rather than string. + * @since 0.0.96 Adjusted parameters and return value to have type StringableArray rather than string. If the + * return value is not a StringableArray, it will be forced into one. * * @see Person::getActionButtons() * diff --git a/src/TouchPoint-WP/PostTypeCapable.php b/src/TouchPoint-WP/PostTypeCapable.php index 448723f0..45bc3613 100644 --- a/src/TouchPoint-WP/PostTypeCapable.php +++ b/src/TouchPoint-WP/PostTypeCapable.php @@ -6,18 +6,19 @@ namespace tp\TouchPointWP; +use tp\TouchPointWP\Interfaces\actionButtons; use tp\TouchPointWP\Interfaces\module; use tp\TouchPointWP\Interfaces\storedAsPost; -use tp\TouchPointWP\Utilities\StringableArray; use WP_Post; +require_once 'Interfaces/actionButtons.php'; require_once 'Interfaces/module.php'; require_once 'Interfaces/storedAsPost.php'; /** * This is a base class for those objects that can be derived from a Post. */ -abstract class PostTypeCapable implements module, storedAsPost +abstract class PostTypeCapable implements module, storedAsPost, actionButtons { protected int $post_id; @@ -104,16 +105,6 @@ protected function processAttributeExclusions(array $subject, array $exclude): a return $subject; } - /** - * @param string|null $context A string that gives filters some context for where the request is coming from - * @param string $btnClass HTML class names to put into the buttons/links - * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint. - * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails. - * - * @return StringableArray - */ - public abstract function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray; - /** * Indicates if the given post can be instantiated as the given post type. * From cdd8fb2227d5b553ba654ac3db29bada8f3233fb Mon Sep 17 00:00:00 2001 From: "James K." Date: Sat, 7 Jun 2025 13:39:52 -0400 Subject: [PATCH 15/83] Rework NotableAttributes calls to return a StringableArray-based class. Closes #232 --- src/TouchPoint-WP/Involvement.php | 25 +++++++--- src/TouchPoint-WP/Meeting.php | 36 +++++++++----- src/TouchPoint-WP/Partner.php | 36 ++++++++------ src/TouchPoint-WP/PostTypeCapable.php | 21 ++++---- .../Utilities/NotableAttributes.php | 22 +++++++++ .../Utilities/StringableArray.php | 49 ++++++++++++++++--- src/templates/involvement-single.php | 8 +-- src/templates/partner-single.php | 9 +--- src/templates/parts/involvement-list-item.php | 20 +------- src/templates/parts/meeting-list-item.php | 31 ++---------- src/templates/parts/partner-list-item.php | 11 +---- touchpoint-wp.php | 1 + 12 files changed, 152 insertions(+), 117 deletions(-) create mode 100644 src/TouchPoint-WP/Utilities/NotableAttributes.php diff --git a/src/TouchPoint-WP/Involvement.php b/src/TouchPoint-WP/Involvement.php index b1665876..84805cf4 100644 --- a/src/TouchPoint-WP/Involvement.php +++ b/src/TouchPoint-WP/Involvement.php @@ -35,6 +35,7 @@ use tp\TouchPointWP\Utilities\DateFormats; use tp\TouchPointWP\Utilities\DateTimeExtended; use tp\TouchPointWP\Utilities\Http; +use tp\TouchPointWP\Utilities\NotableAttributes; use tp\TouchPointWP\Utilities\PersonArray; use tp\TouchPointWP\Utilities\PersonQuery; use tp\TouchPointWP\Utilities\StringableArray; @@ -3710,13 +3711,20 @@ public function toJsonLD(): ?array /** * Get notable attributes, such as gender restrictions, as strings. * - * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent inv, not needed - * here.) + * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent inv, + * not needed here.) * - * @return string[] + * @return NotableAttributes + * + * @since 0.0.11 + * @since 0.0.96 Changed to use NotableAttributes class, which is a StringableArray. */ - public function notableAttributes(array $exclude = []): array + public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes { + if (!is_array($exclude)) { + $exclude = $exclude->getArrayCopy(); + } + $asMeeting = $this->AsAMeeting(); if ($asMeeting !== null) { $attrs = $asMeeting->notableAttributes(['involvement']); @@ -3724,6 +3732,7 @@ public function notableAttributes(array $exclude = []): array $attrs = self::scheduleStrings($this->invId, $this); unset($attrs['combined']); $attrs = array_filter($attrs); + $attrs = new NotableAttributes($attrs); } unset($schStr); @@ -3779,6 +3788,8 @@ public function notableAttributes(array $exclude = []): array $attrs = $this->processAttributeExclusions($attrs, $exclude); + $inv = $this; + /** * Allows for manipulation of the notable attributes strings for an Involvement. An array of strings. * Typically, these are the standardized strings that appear on the Involvement to give information about it, @@ -3789,10 +3800,10 @@ public function notableAttributes(array $exclude = []): array * * @since 0.0.11 Added * - * @param string[] $attrs The list of notable attributes. - * @param Involvement $this The Involvement object. + * @param NotableAttributes $attrs The list of notable attributes. + * @param Involvement $inv The Involvement object. */ - return apply_filters("tp_involvement_attributes", $attrs, $this); + return apply_filters("tp_involvement_attributes", $attrs, $inv); } /** diff --git a/src/TouchPoint-WP/Meeting.php b/src/TouchPoint-WP/Meeting.php index 569210bb..1d20b2e4 100644 --- a/src/TouchPoint-WP/Meeting.php +++ b/src/TouchPoint-WP/Meeting.php @@ -25,6 +25,7 @@ use tp\TouchPointWP\Interfaces\scheduled; use tp\TouchPointWP\Utilities\DateFormats; use tp\TouchPointWP\Utilities\Http; +use tp\TouchPointWP\Utilities\NotableAttributes; use tp\TouchPointWP\Utilities\StringableArray; use WP_Post; use WP_Query; @@ -425,25 +426,37 @@ public function scheduleStringArray(): StringableArray /** * Get notable attributes, such as gender restrictions, as strings. * - * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not + * needed here.) * - * @return string[] + * @return NotableAttributes */ - public function notableAttributes(array $exclude = []): array + public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes { + if (!is_array($exclude)) { + $exclude = $exclude->getArrayCopy(); + } + if (in_array('involvement', $exclude)) { - $attrs = []; + $attrs = null; } else { try { $attrs = $this->involvement()->notableAttributes(['date', 'datetime', 'time', 'firstLast']); } catch (TouchPointWP_Exception) { - $attrs = []; + $attrs = null; } } $d = $this->scheduleStringArray(); - - $attrs = [...$d, ...$attrs]; + if ($attrs !== null) { + foreach ($attrs as $k => $v) { + if (is_string($v) && $v !== "") { + $d[$k] = $v; + } + } + } + $attrs = $d; + unset($d); $status = $this->status_i18n(true); if ($status) { @@ -460,8 +473,8 @@ public function notableAttributes(array $exclude = []): array $attrs['location'] = $loc; } - $attrs = $this->processAttributeExclusions($attrs, $exclude); + $mtg = $this; /** * Allows for manipulation of the notable attributes strings for a Meeting. An array of strings. @@ -472,11 +485,12 @@ public function notableAttributes(array $exclude = []): array * @see PostTypeCapable::notableAttributes() * * @since 0.0.90 Added + * @since 0.0.96 Changed to use NotableAttributes instead of array. * - * @param string[] $attrs The list of notable attributes. - * @param Meeting $this The Meeting object. + * @param NotableAttributes $attrs The list of notable attributes. + * @param Meeting $mtg The Meeting object. */ - return apply_filters("tp_meeting_attributes", $attrs, $this); + return apply_filters("tp_meeting_attributes", $attrs, $mtg); } /** diff --git a/src/TouchPoint-WP/Partner.php b/src/TouchPoint-WP/Partner.php index 3b57fd6d..18278e8d 100644 --- a/src/TouchPoint-WP/Partner.php +++ b/src/TouchPoint-WP/Partner.php @@ -23,6 +23,7 @@ use tp\TouchPointWP\Interfaces\hasGeo; use tp\TouchPointWP\Interfaces\module; use tp\TouchPointWP\Interfaces\updatesViaCron; +use tp\TouchPointWP\Utilities\NotableAttributes; use tp\TouchPointWP\Utilities\StringableArray; use WP_Error; use WP_Post; @@ -1198,8 +1199,7 @@ public static function filterPublishDate($theDate, $format, $post = null): strin if ($format == '') { try { $gp = self::fromPost($post); - $theDate = $gp->notableAttributes(); - $theDate = implode(TouchPointWP::$joiner, $theDate); + $theDate = $gp->notableAttributes()->join(); } catch (TouchPointWP_Exception $e) { } } else { @@ -1261,33 +1261,40 @@ public static function getFamEvAsContent(string $ev, object $famObj, ?string $de /** * Get notable attributes as strings. * - * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * + * @return NotableAttributes + * @since 0.0.6 Added + * @since 0.0.96 Changed to use NotableAttributes instead of array. * - * @return string[] */ - public function notableAttributes(array $exclude = []): array + public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes { - $r = []; + $attrs = new NotableAttributes(); + if (!is_array($exclude)) { + $exclude = $exclude->getArrayCopy(); + } $l = $this->locationName(); if ($this->decoupleLocation) { - $r['secure'] = $l; + $attrs['secure'] = $l; } elseif ($l) { - $r['location'] = $l; + $attrs['location'] = $l; } unset($l); foreach ($this->category as $c) { - $r['category'] = $c->name; + $attrs['category'] = $c->name; } // Not shown on map (only if there is a map, and the partner isn't on it because they lack geo.) if (self::$_hasArchiveMap && $this->geo === null && ! $this->decoupleLocation) { - $r['hidden'] = __("Not Shown on Map", "TouchPoint-WP"); + $attrs['hidden'] = __("Not Shown on Map", "TouchPoint-WP"); TouchPointWP::requireScript("fontAwesome"); // For map icons } - $r = $this->processAttributeExclusions($r, $exclude); + $attrs = $this->processAttributeExclusions($attrs, $exclude); + $partner = $this; /** * Allows for manipulation of the notable attributes strings for an Partner. An array of strings. @@ -1298,11 +1305,12 @@ public function notableAttributes(array $exclude = []): array * @see PostTypeCapable::notableAttributes() * * @since 0.0.6 Added + * @since 0.0.96 Changed to use NotableAttributes instead of array. * - * @param string[] $attrs The list of notable attributes. - * @param Partner $this The Partner object. + * @param NotableAttributes $attrs The list of notable attributes. + * @param Partner $partner The Partner object. */ - return apply_filters("tp_partner_attributes", $r, $this); + return apply_filters("tp_partner_attributes", $attrs, $partner); } /** diff --git a/src/TouchPoint-WP/PostTypeCapable.php b/src/TouchPoint-WP/PostTypeCapable.php index 45bc3613..b66296c9 100644 --- a/src/TouchPoint-WP/PostTypeCapable.php +++ b/src/TouchPoint-WP/PostTypeCapable.php @@ -6,9 +6,12 @@ namespace tp\TouchPointWP; +use ArrayAccess; use tp\TouchPointWP\Interfaces\actionButtons; use tp\TouchPointWP\Interfaces\module; use tp\TouchPointWP\Interfaces\storedAsPost; +use tp\TouchPointWP\Utilities\NotableAttributes; +use tp\TouchPointWP\Utilities\StringableArray; use WP_Post; require_once 'Interfaces/actionButtons.php'; @@ -78,31 +81,31 @@ public function permalink(): string /** * Get notable attributes. * - * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) * - * @return string[] + * @return NotableAttributes */ - public abstract function notableAttributes(array $exclude = []): array; + public abstract function notableAttributes(array|StringableArray $exclude = []): NotableAttributes; /** * Handle exclusions for the notableAttributes $exclusion variable. * * Removes all array items that have a value or key contained in the $exclude array's values. * - * @param array $subject - * @param array $exclude + * @param StringableArray $subject + * @param array $exclude * - * @return array + * @return NotableAttributes */ - protected function processAttributeExclusions(array $subject, array $exclude): array + protected function processAttributeExclusions(StringableArray $subject, array $exclude): NotableAttributes { - $subject = array_diff($subject, $exclude); + $subject = array_diff($subject->getArrayCopy(), $exclude); foreach ($exclude as $e) { if (isset($subject[$e])) { unset($subject[$e]); } } - return $subject; + return new NotableAttributes($subject); } /** diff --git a/src/TouchPoint-WP/Utilities/NotableAttributes.php b/src/TouchPoint-WP/Utilities/NotableAttributes.php new file mode 100644 index 00000000..235dc45f --- /dev/null +++ b/src/TouchPoint-WP/Utilities/NotableAttributes.php @@ -0,0 +1,22 @@ +separator = TouchPointWP::$joiner; + $this->itemPrefix = ''; + $this->itemSuffix = ""; + } +} \ No newline at end of file diff --git a/src/TouchPoint-WP/Utilities/StringableArray.php b/src/TouchPoint-WP/Utilities/StringableArray.php index e2f920f1..0561efe8 100644 --- a/src/TouchPoint-WP/Utilities/StringableArray.php +++ b/src/TouchPoint-WP/Utilities/StringableArray.php @@ -6,6 +6,7 @@ namespace tp\TouchPointWP\Utilities; use ArrayObject; +use tp\TouchPointWP\TouchPointWP; use tp\TouchPointWP\Utilities; /** @@ -13,19 +14,22 @@ */ class StringableArray extends ArrayObject { - protected string $separator; + protected string $separator = "\n"; + protected string $itemPrefix = ""; + protected string $itemSuffix = ""; /** * StringableArray constructor. * - * @param string $separator * @param object|array $array * @param int $flags * @param string $iteratorClass + * + * @since 0.0.90 Added + * @since 0.0.96 Changed signature to reflect actual usage. */ - public function __construct(string $separator = "\n ", object|array $array = [], int $flags = 0, string $iteratorClass = "ArrayIterator") + public function __construct(object|array $array = [], int $flags = 0, string $iteratorClass = "ArrayIterator") { - $this->separator = $separator; parent::__construct($array, $flags, $iteratorClass); } @@ -56,6 +60,16 @@ public function prepend(mixed $value, $key = null): void $this->exchangeArray($array); } + /** + * Get the stringable array, as an array. + * + * @return array + */ + public function toArray(): array + { + return $this->getArrayCopy(); + } + /** * Determine if the stringable array (haystack) contains the given needle * @@ -68,6 +82,16 @@ public function contains($needle): bool return in_array($needle, $this->getArrayCopy()); } + /** + * Get the keys of the array. + * + * @return array + */ + public function keys(): array + { + return array_keys($this->getArrayCopy()); + } + /** * Standard method to stringify. * @@ -82,17 +106,30 @@ public function __toString() * Link the items together with a given separator, which may be different from the separator used in the constructor. * * @param string|null $separator + * @param string|null $prefix + * @param string|null $postfix * * @return string * * @since 0.0.90 Added + * @since 0.0.96 Added $prefix and $postfix parameters to allow for more flexible joining (and particularly, HTML). */ - public function join(?string $separator = null): string + public function join(?string $separator = null, ?string $prefix = null, ?string $postfix = null): string { + if ($this->count() === 0) { + return ""; + } if (is_null($separator)) { $separator = $this->separator; } - return implode($separator, $this->getArrayCopy()); + if (is_null($prefix)) { + $prefix = $this->itemPrefix; + } + if (is_null($postfix)) { + $postfix = $this->itemSuffix; + } + $joiner = $postfix . $separator . $prefix; + return $prefix . implode($joiner, $this->getArrayCopy()) . $postfix; } /** diff --git a/src/templates/involvement-single.php b/src/templates/involvement-single.php index 3de1705f..af49238b 100644 --- a/src/templates/involvement-single.php +++ b/src/templates/involvement-single.php @@ -70,12 +70,8 @@
notableAttributes() as $a) - { - $metaStrings[] = sprintf( '%s', $a); - } - echo implode("
", $metaStrings); + $notableAttributes = $obj->notableAttributes(); + echo $notableAttributes->join("
"); ?>
diff --git a/src/templates/partner-single.php b/src/templates/partner-single.php index 9f9c0ef6..17a68477 100644 --- a/src/templates/partner-single.php +++ b/src/templates/partner-single.php @@ -46,14 +46,7 @@
- notableAttributes() as $a) - { - $metaStrings[] = sprintf( '%s', $a); - } - echo implode("
", $metaStrings); - ?> + notableAttributes()->join("
"); ?>
getActionButtons('single-template', "btn button") ?> diff --git a/src/templates/parts/involvement-list-item.php b/src/templates/parts/involvement-list-item.php index 80b94498..c6391651 100644 --- a/src/templates/parts/involvement-list-item.php +++ b/src/templates/parts/involvement-list-item.php @@ -33,15 +33,8 @@ @@ -69,16 +62,7 @@ echo "

$child->post_title

"; $childInv = PostTypeCapable::fromPost($child); - - $metaStrings = []; - foreach ($childInv->notableAttributes($notableAttributes) as $a) - { - $metaStrings[] = sprintf( '%s', $a); - } - $m = implode(tp\TouchPointWP\TouchPointWP::$joiner, $metaStrings); - if ($m !== "") { - echo "$m"; - } + echo $childInv->notableAttributes($notableAttributes); echo "
"; } diff --git a/src/templates/parts/meeting-list-item.php b/src/templates/parts/meeting-list-item.php index f3e45507..3faadf1c 100644 --- a/src/templates/parts/meeting-list-item.php +++ b/src/templates/parts/meeting-list-item.php @@ -29,15 +29,8 @@ @@ -79,16 +72,7 @@ echo "

$child->post_title

"; $childInv = Involvement::fromPost($child); - - $metaStrings = []; - foreach ($childInv->notableAttributes($notableAttributes) as $a) - { - $metaStrings[] = sprintf( '%s', $a); - } - $m = implode(tp\TouchPointWP\TouchPointWP::$joiner, $metaStrings); - if ($m !== "") { - echo "$m"; - } + echo $childInv->notableAttributes($notableAttributes); echo "
"; } @@ -128,16 +112,7 @@ echo "

$child->post_title

"; $childInv = Meeting::fromPost($child); - - $metaStrings = []; - foreach ($childInv->notableAttributes($notableAttributes) as $a) - { - $metaStrings[] = sprintf( '%s', $a); - } - $m = implode(tp\TouchPointWP\TouchPointWP::$joiner, $metaStrings); - if ($m !== "") { - echo "$m"; - } + echo $childInv->notableAttributes($notableAttributes); echo "
"; } diff --git a/src/templates/parts/partner-list-item.php b/src/templates/parts/partner-list-item.php index b8c318af..7e3f4dbe 100644 --- a/src/templates/parts/partner-list-item.php +++ b/src/templates/parts/partner-list-item.php @@ -31,16 +31,7 @@
diff --git a/touchpoint-wp.php b/touchpoint-wp.php index 2941fb51..8a93d1f8 100644 --- a/touchpoint-wp.php +++ b/touchpoint-wp.php @@ -57,6 +57,7 @@ require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Translation.php"; require_once __DIR__ . "/src/TouchPoint-WP/Utilities/PersonArray.php"; require_once __DIR__ . "/src/TouchPoint-WP/Utilities/StringableArray.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/NotableAttributes.php"; require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Session.php"; require_once __DIR__ . "/src/TouchPoint-WP/Utilities/DateFormats.php"; require_once __DIR__ . "/src/TouchPoint-WP/Utilities/DateTimeExtended.php"; From bb0939940d87ce408392ea178feee38c466ee095 Mon Sep 17 00:00:00 2001 From: "James K." Date: Mon, 9 Jun 2025 09:09:08 -0400 Subject: [PATCH 16/83] Initial TP-Inv-List Block --- .idea/libraries/Generated_files.xml | 4 + blocks/inv-list/block.json | 17 + blocks/inv-list/index.js | 122 ++++++ build.sh | 13 +- buildPipeline/versionUpdate.js | 23 + i18n/TouchPoint-WP-es_ES.po | 390 ++++++++++------- i18n/TouchPoint-WP.pot | 392 +++++++++++------- package.json | 8 +- src/TouchPoint-WP/Blocks/BlocksController.php | 118 ++++++ src/TouchPoint-WP/Involvement.php | 37 +- src/TouchPoint-WP/TouchPointWP.php | 4 + src/TouchPoint-WP/TouchPointWP_AdminAPI.php | 6 + touchpoint-wp.php | 2 + webpack.config.js | 28 ++ wpml-config.xml | 2 + 15 files changed, 836 insertions(+), 330 deletions(-) create mode 100644 blocks/inv-list/block.json create mode 100644 blocks/inv-list/index.js create mode 100644 buildPipeline/versionUpdate.js create mode 100644 src/TouchPoint-WP/Blocks/BlocksController.php create mode 100644 webpack.config.js diff --git a/.idea/libraries/Generated_files.xml b/.idea/libraries/Generated_files.xml index 11b1f82e..71c16549 100644 --- a/.idea/libraries/Generated_files.xml +++ b/.idea/libraries/Generated_files.xml @@ -5,6 +5,8 @@ + + @@ -14,6 +16,8 @@ + + diff --git a/blocks/inv-list/block.json b/blocks/inv-list/block.json new file mode 100644 index 00000000..89062395 --- /dev/null +++ b/blocks/inv-list/block.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "touchpoint-wp/inv-list", + "version": "0.0.96", + "title": "Involvement List", + "category": "widgets", + "icon": "grid-view", + "description": "A list of Involvements from TouchPoint", + "supports": { + "html": false, + "ariaLabel": true, + "reusable": false + }, + "textdomain": "TouchPoint-WP", + "editorScript": "file:./index.min.js" +} \ No newline at end of file diff --git a/blocks/inv-list/index.js b/blocks/inv-list/index.js new file mode 100644 index 00000000..e6861277 --- /dev/null +++ b/blocks/inv-list/index.js @@ -0,0 +1,122 @@ +/** + * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. + * All files containing `style` keyword are bundled together. The code used + * gets applied both to the front of your site and to the editor. + * + * @see https://www.npmjs.com/package/@wordpress/scripts#using-css + */ +// import './style.css'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import {__} from "@wordpress/i18n"; + +/** + * Every block starts by registering a new block type definition. + * + * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ + */ +wp.blocks.registerBlockType( metadata.name, { + title: __("Involvement List", "TouchPoint-WP"), + category: metadata.category, + description: __("A list of Involvements from TouchPoint", "TouchPoint-WP"), + icon: metadata.icon, + apiVersion: metadata.apiVersion, + attributes: { + postType: { + type: 'string', + default: '', + }, + division: { + type: 'integer', + default: 0, + }, + }, + edit: function(props) { + const { attributes, setAttributes } = props; + const { postType, division } = attributes; + const blockProps = wp.blockEditor.useBlockProps(); + const additionalClasses = blockProps.className || ''; + + const [postTypeOptions, setPostTypeOptions] = wp.element.useState([]); + const [divisionChildren, setDivisionChildren] = wp.element.useState(null); + + wp.element.useEffect(() => { + (async () => { + try { + const response = await fetch('/touchpoint-api/inv/posttypes'); + const data = await response.json(); + const formattedOptions = data.map((item) => ({ + label: item.namePlural, + value: item.postType, + })); + setPostTypeOptions(formattedOptions);setPostTypeOptions(formattedOptions); + if (postType === '' && formattedOptions.length > 0) { + setAttributes({ postType: formattedOptions[0].value }); + } + } catch (error) { + console.error('Error fetching post types:', error); + } + })(); + }, []); + + wp.element.useEffect(() => { + (async () => { + try { + const response = await fetch('/touchpoint-api/admin/divisions'); + const data = await response.json(); + const groupedOptions = []; + const groupedData = data.reduce((acc, item) => { + const group = acc[item.pName] || []; + group.push(); + acc[item.pName] = group; + return acc; + }, {}); + Object.entries(groupedData).forEach(([groupLabel, options]) => { + groupedOptions.push({options}); + }); + setDivisionChildren(groupedOptions); + } catch (error) { + console.error('Error fetching divisions:', error); + } + })(); + }, []); + + return ( +
+ + + setAttributes({ postType: value })} + /> + setAttributes({ division: Number(value) })} + __next40pxDefaultSize={true} + __nextHasNoMarginBottom={true} + /> + + +

{`[TP-Inv-List class="${additionalClasses}" type="${postType}"${division !== 0 ? ` div="${division}"` : ""}]`}

+
+ ); + }, + save: function(props) { + const { attributes } = props; + const { postType, division } = attributes; + const blockProps = wp.blockEditor.useBlockProps.save(); + const additionalClasses = blockProps.className || ''; + + return ( + `[TP-Inv-List class="${additionalClasses}" type="${postType}" div="${division}"]` + ); + }, + +} ); diff --git a/build.sh b/build.sh index 7a31b13a..f02a61c6 100644 --- a/build.sh +++ b/build.sh @@ -9,6 +9,10 @@ export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion nvm install node +# update version in various json files +node ./buildPipeline/versionUpdate.js + +# update NPM packages npm update rm -r build @@ -26,11 +30,10 @@ cp -r assets build cd ./build || exit cd .. - -# compile translations -if [ ! -f wp-cli.phar ]; then - wget -O wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -fi +# build blocks +npm install -g @wordpress/scripts +wp-scripts build --webpack-src-dir=blocks --output-path=build/blocks +wp-scripts build-blocks-manifest --input=blocks --output=build/blocks/blocks-manifest.php cp -r ./i18n ./build/i18n diff --git a/buildPipeline/versionUpdate.js b/buildPipeline/versionUpdate.js new file mode 100644 index 00000000..da0c8ac4 --- /dev/null +++ b/buildPipeline/versionUpdate.js @@ -0,0 +1,23 @@ + + +const fs = require('fs'); + +// get the version in composer.json +let version = JSON.parse(fs.readFileSync('./composer.json', 'utf8')).version; + +// update the version in package.json +let packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); +packageJson.version = version; +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2)); + +// update the version in all blocks/*/block.json +const blockFiles = fs.readdirSync('./blocks'); + +blockFiles.forEach((blockFile) => { + const blockPath = `./blocks/${blockFile}/block.json`; + if (fs.existsSync(blockPath)) { + let blockJson = JSON.parse(fs.readFileSync(blockPath, 'utf8')); + blockJson.version = version; + fs.writeFileSync(blockPath, JSON.stringify(blockJson, null, 2)); + } +}); \ No newline at end of file diff --git a/i18n/TouchPoint-WP-es_ES.po b/i18n/TouchPoint-WP-es_ES.po index 7a3d69d6..4d90565e 100644 --- a/i18n/TouchPoint-WP-es_ES.po +++ b/i18n/TouchPoint-WP-es_ES.po @@ -88,10 +88,10 @@ msgstr "Tipos de miembros de líder" #: src/templates/admin/invKoForm.php:178 #: src/templates/admin/invKoForm.php:332 #: src/templates/parts/involvement-nearby-list.php:2 -#: src/TouchPoint-WP/Meeting.php:789 +#: src/TouchPoint-WP/Meeting.php:803 #: src/TouchPoint-WP/Rsvp.php:77 -#: assets/js/base-defer.js:192 -#: assets/js/base-defer.js:1133 +#: assets/js/base-defer.js:195 +#: assets/js/base-defer.js:1113 msgid "Loading..." msgstr "Cargando..." @@ -124,13 +124,13 @@ msgid "Gender" msgstr "Género" #: src/templates/admin/invKoForm.php:250 -#: src/TouchPoint-WP/Involvement.php:1872 +#: src/TouchPoint-WP/Involvement.php:1896 #: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekday" msgstr "Día laborable" #: src/templates/admin/invKoForm.php:254 -#: src/TouchPoint-WP/Involvement.php:1898 +#: src/TouchPoint-WP/Involvement.php:1922 #: src/TouchPoint-WP/Taxonomies.php:808 msgid "Time of Day" msgstr "Hora del día" @@ -174,12 +174,14 @@ msgstr "Seleccione..." #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:16 -#: src/TouchPoint-WP/Involvement.php:2142 +#: src/TouchPoint-WP/Involvement.php:2172 +#, php-format msgid "No %s Found." msgstr "No se encontraron %s" #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:20 +#, php-format msgid "%s will be imported overnight for the first time." msgstr "%s se importarán durante la noche por primera vez." @@ -197,129 +199,131 @@ msgstr "Periódico" msgid "Multi-Day" msgstr "varios días" -#: src/TouchPoint-WP/Involvement.php:514 +#: src/TouchPoint-WP/Involvement.php:515 msgid "Currently Full" msgstr "Actualmente lleno" -#: src/TouchPoint-WP/Involvement.php:519 +#: src/TouchPoint-WP/Involvement.php:520 msgid "Currently Closed" msgstr "Actualmente cerrado" -#: src/TouchPoint-WP/Involvement.php:526 +#: src/TouchPoint-WP/Involvement.php:527 msgid "Registration Not Open Yet" msgstr "Registro aún no abierto" -#: src/TouchPoint-WP/Involvement.php:532 +#: src/TouchPoint-WP/Involvement.php:533 msgid "Registration Closed" msgstr "Registro cerrado" -#: src/TouchPoint-WP/Involvement.php:1747 -#: src/TouchPoint-WP/Partner.php:841 +#: src/TouchPoint-WP/Involvement.php:1771 +#: src/TouchPoint-WP/Partner.php:842 msgid "Any" msgstr "Cualquier" #. translators: %s is for the user-provided term for the items on the map (e.g. Small Group or Partner) -#: src/TouchPoint-WP/Involvement.php:1955 -#: src/TouchPoint-WP/Partner.php:865 +#: src/TouchPoint-WP/Involvement.php:1979 +#: src/TouchPoint-WP/Partner.php:866 +#, php-format msgid "The %s listed are only those shown on the map." msgstr "Los %s enumerados son solo los que se muestran en el mapa." -#: src/TouchPoint-WP/Involvement.php:3749 +#: src/TouchPoint-WP/Involvement.php:3787 msgid "Men Only" msgstr "Solo hombres" -#: src/TouchPoint-WP/Involvement.php:3752 +#: src/TouchPoint-WP/Involvement.php:3790 msgid "Women Only" msgstr "Solo mujeres" -#: src/TouchPoint-WP/Involvement.php:3829 +#: src/TouchPoint-WP/Involvement.php:3869 msgid "Contact Leaders" msgstr "Contacta con las líderes" -#: src/TouchPoint-WP/Involvement.php:3899 -#: src/TouchPoint-WP/Involvement.php:3958 +#: src/TouchPoint-WP/Involvement.php:3939 +#: src/TouchPoint-WP/Involvement.php:3998 msgid "Register" msgstr "Regístrate ahora" -#: src/TouchPoint-WP/Involvement.php:3905 +#: src/TouchPoint-WP/Involvement.php:3945 msgid "Create Account" msgstr "Crear cuenta" -#: src/TouchPoint-WP/Involvement.php:3909 +#: src/TouchPoint-WP/Involvement.php:3949 msgid "Schedule" msgstr "Programe" -#: src/TouchPoint-WP/Involvement.php:3914 +#: src/TouchPoint-WP/Involvement.php:3954 msgid "Give" msgstr "Dar" -#: src/TouchPoint-WP/Involvement.php:3917 +#: src/TouchPoint-WP/Involvement.php:3957 msgid "Manage Subscriptions" msgstr "Administrar suscripciones" -#: src/TouchPoint-WP/Involvement.php:3920 +#: src/TouchPoint-WP/Involvement.php:3960 msgid "Record Attendance" msgstr "Registre su asistencia" -#: src/TouchPoint-WP/Involvement.php:3923 +#: src/TouchPoint-WP/Involvement.php:3963 msgid "Get Tickets" msgstr "Obtener boletos" -#: src/TouchPoint-WP/Involvement.php:3949 -#: assets/js/base-defer.js:1001 +#: src/TouchPoint-WP/Involvement.php:3989 +#: assets/js/base-defer.js:1004 msgid "Join" msgstr "Únete" -#: src/TouchPoint-WP/Involvement.php:3846 -#: src/TouchPoint-WP/Partner.php:1345 +#: src/TouchPoint-WP/Involvement.php:3886 +#: src/TouchPoint-WP/Partner.php:1353 msgid "Show on Map" msgstr "Muestra en el mapa" #. translators: %s is for the user-provided "Global Partner" and "Secure Partner" terms. -#: src/TouchPoint-WP/Partner.php:872 +#: src/TouchPoint-WP/Partner.php:873 +#, php-format msgid "The %1$s listed are only those shown on the map, as well as all %2$s." msgstr "Los %1$s enumerados son solo los que se muestran en el mapa, así como todos los %2$s." -#: src/TouchPoint-WP/Partner.php:1286 +#: src/TouchPoint-WP/Partner.php:1292 msgid "Not Shown on Map" msgstr "No se muestra en el mapa" -#: src/TouchPoint-WP/Person.php:146 +#: src/TouchPoint-WP/Person.php:148 msgid "No WordPress User ID provided for initializing a person object." msgstr "No se proporcionó una identificación de usuario de WordPress para inicializar un objeto de persona." -#: src/TouchPoint-WP/Person.php:638 +#: src/TouchPoint-WP/Person.php:640 msgid "TouchPoint People ID" msgstr "ID de Personas de TouchPoint" -#: src/TouchPoint-WP/Person.php:1187 +#: src/TouchPoint-WP/Person.php:1196 msgid "Contact" msgstr "Contacta" -#: src/TouchPoint-WP/Meeting.php:788 -#: src/TouchPoint-WP/Meeting.php:809 +#: src/TouchPoint-WP/Meeting.php:802 +#: src/TouchPoint-WP/Meeting.php:823 #: src/TouchPoint-WP/Rsvp.php:82 msgid "RSVP" msgstr "RSVP" -#: src/TouchPoint-WP/TouchPointWP.php:2120 +#: src/TouchPoint-WP/TouchPointWP.php:2125 msgid "Unknown Type" msgstr "Tipo desconocido" -#: src/TouchPoint-WP/TouchPointWP.php:2177 +#: src/TouchPoint-WP/TouchPointWP.php:2182 msgid "Your Searches" msgstr "Tus búsquedas" -#: src/TouchPoint-WP/TouchPointWP.php:2180 +#: src/TouchPoint-WP/TouchPointWP.php:2185 msgid "Public Searches" msgstr "Búsquedas públicas" -#: src/TouchPoint-WP/TouchPointWP.php:2183 +#: src/TouchPoint-WP/TouchPointWP.php:2188 msgid "Status Flags" msgstr "Indicadores de Estado" -#: src/TouchPoint-WP/TouchPointWP.php:2188 -#: src/TouchPoint-WP/TouchPointWP.php:2189 +#: src/TouchPoint-WP/TouchPointWP.php:2193 +#: src/TouchPoint-WP/TouchPointWP.php:2194 msgid "Current Value" msgstr "Valor actual" @@ -335,7 +339,7 @@ msgstr "Configuración de API no válida o incompleta." msgid "Host appears to be missing from TouchPoint-WP configuration." msgstr "Parece que falta el host en la configuración de TouchPoint-WP." -#: src/TouchPoint-WP/TouchPointWP.php:2403 +#: src/TouchPoint-WP/TouchPointWP.php:2408 msgid "People Query Failed" msgstr "Consulta de registros de personas fallida" @@ -779,6 +783,7 @@ msgid "TouchPoint-WP" msgstr "TouchPoint-WP" #: src/TouchPoint-WP/Settings.php:1451 +#: blocks/inv-list/index.js:90 msgid "Settings" msgstr "Ajustes" @@ -794,127 +799,127 @@ msgstr "Configuración de TouchPoint-WP" msgid "Save Settings" msgstr "Guardar ajustes" -#: src/TouchPoint-WP/Person.php:1466 +#: src/TouchPoint-WP/Person.php:1490 #: src/TouchPoint-WP/Utilities.php:299 -#: assets/js/base-defer.js:18 +#: assets/js/base-defer.js:20 msgid "and" msgstr "y" -#: assets/js/base-defer.js:212 -#: assets/js/base-defer.js:1168 +#: assets/js/base-defer.js:215 +#: assets/js/base-defer.js:1148 msgid "Your Location" msgstr "Tu ubicación" -#: assets/js/base-defer.js:233 +#: assets/js/base-defer.js:236 msgid "User denied the request for Geolocation." msgstr "El usuario denegó la solicitud de geolocalización." -#: assets/js/base-defer.js:237 +#: assets/js/base-defer.js:240 msgid "Location information is unavailable." msgstr "La información de ubicación no está disponible." -#: assets/js/base-defer.js:241 +#: assets/js/base-defer.js:244 msgid "The request to get user location timed out." msgstr "Se agotó el tiempo de espera de la solicitud para obtener la ubicación del usuario." -#: assets/js/base-defer.js:245 +#: assets/js/base-defer.js:248 msgid "An unknown error occurred." msgstr "Un error desconocido ocurrió." -#: assets/js/base-defer.js:281 -#: assets/js/base-defer.js:291 +#: assets/js/base-defer.js:284 +#: assets/js/base-defer.js:294 msgid "No geolocation option available." msgstr "No hay opción de geolocalización disponible." -#: assets/js/base-defer.js:931 -#: assets/js/base-defer.js:968 -#: assets/js/base-defer.js:1425 +#: assets/js/base-defer.js:934 +#: assets/js/base-defer.js:971 +#: assets/js/base-defer.js:1406 #: assets/js/meeting-defer.js:211 msgid "Something strange happened." msgstr "Algo extraño sucedió." -#: assets/js/base-defer.js:957 -#: assets/js/base-defer.js:1414 +#: assets/js/base-defer.js:960 +#: assets/js/base-defer.js:1395 msgid "Your message has been sent." msgstr "Tu mensaje ha sido enviado." -#: assets/js/base-defer.js:997 +#: assets/js/base-defer.js:1000 msgid "Who is joining the group?" msgstr "¿Quién se une al grupo?" -#: assets/js/base-defer.js:1002 -#: assets/js/base-defer.js:1060 -#: assets/js/base-defer.js:1376 -#: assets/js/base-defer.js:1469 +#: assets/js/base-defer.js:1005 +#: assets/js/base-defer.js:1059 +#: assets/js/base-defer.js:1357 +#: assets/js/base-defer.js:1450 #: assets/js/meeting-defer.js:253 msgid "Cancel" msgstr "Cancelar" -#: assets/js/base-defer.js:1015 +#: assets/js/base-defer.js:1018 msgid "Select who should be added to the group." msgstr "Seleccione quién debe agregarse al grupo." -#: assets/js/base-defer.js:1053 -#: assets/js/base-defer.js:1369 +#: assets/js/base-defer.js:1052 +#: assets/js/base-defer.js:1350 msgid "From" msgstr "De" -#: assets/js/base-defer.js:1054 -#: assets/js/base-defer.js:1370 +#: assets/js/base-defer.js:1053 +#: assets/js/base-defer.js:1351 msgid "Message" msgstr "Mensaje" -#: assets/js/base-defer.js:1069 -#: assets/js/base-defer.js:1385 +#: assets/js/base-defer.js:1068 +#: assets/js/base-defer.js:1366 msgid "Please provide a message." msgstr "Proporcione un mensaje." -#: assets/js/base-defer.js:1154 -#: assets/js/base-defer.js:1156 +#: assets/js/base-defer.js:1134 +#: assets/js/base-defer.js:1136 msgid "We don't know where you are." msgstr "No sabemos dónde estás." -#: assets/js/base-defer.js:1154 -#: assets/js/base-defer.js:1164 +#: assets/js/base-defer.js:1134 +#: assets/js/base-defer.js:1144 msgid "Click here to use your actual location." msgstr "Haga clic aquí para usar su ubicación real." -#: assets/js/base-defer.js:1315 -#: assets/js/base-defer.js:1332 +#: assets/js/base-defer.js:1296 +#: assets/js/base-defer.js:1313 msgid "clear" msgstr "borrar" -#: assets/js/base-defer.js:1321 +#: assets/js/base-defer.js:1302 msgid "Other Relatives..." msgstr "Otros familiares..." -#: assets/js/base-defer.js:1459 +#: assets/js/base-defer.js:1440 msgid "Tell us about yourself." msgstr "Dinos sobre ti." -#: assets/js/base-defer.js:1461 -#: assets/js/base-defer.js:1516 +#: assets/js/base-defer.js:1442 +#: assets/js/base-defer.js:1497 msgid "Email Address" msgstr "Correo electrónico" -#: assets/js/base-defer.js:1462 -#: assets/js/base-defer.js:1517 +#: assets/js/base-defer.js:1443 +#: assets/js/base-defer.js:1498 msgid "Zip Code" msgstr "Condigo postal" -#: assets/js/base-defer.js:1514 +#: assets/js/base-defer.js:1495 msgid "Our system doesn't recognize you,
so we need a little more info." msgstr "Nuestro sistema no te reconoce.
Necesitamos un poco más de información." -#: assets/js/base-defer.js:1518 +#: assets/js/base-defer.js:1499 msgid "First Name" msgstr "Primer nombre" -#: assets/js/base-defer.js:1519 +#: assets/js/base-defer.js:1500 msgid "Last Name" msgstr "Apellido" -#: assets/js/base-defer.js:1521 +#: assets/js/base-defer.js:1502 msgid "Phone" msgstr "Teléfono" @@ -964,6 +969,7 @@ msgstr "Los scripts en TouchPoint que interactúan con este complemento están d #. translators: "RSVP for {Event Name}" This is the heading on the RSVP modal. The event name isn't translated because it comes from TouchPoint. #: assets/js/meeting-defer.js:233 +#, js-format msgid "RSVP for %s" msgstr "RSVP para %s" @@ -974,55 +980,59 @@ msgstr[0] "Respuesta registrada" msgstr[1] "Respuestas registrada" #. translators: %s is the name of an involvement, like a particular small group -#: assets/js/base-defer.js:920 +#: assets/js/base-defer.js:923 +#, js-format msgid "Added to %s" msgstr "Añadido a %s" #. translators: %s is the name of an Involvement -#: assets/js/base-defer.js:981 +#: assets/js/base-defer.js:984 +#, js-format msgid "Join %s" msgstr "Únete %s" #. translators: %s is a person's name. This is a heading for a contact modal. -#: assets/js/base-defer.js:1352 +#: assets/js/base-defer.js:1333 +#, js-format msgid "Contact %s" msgstr "Contactar a %s" #. translators: %s is the name of an involvement. This is a heading for a modal. -#: assets/js/base-defer.js:1036 +#: assets/js/base-defer.js:1035 +#, js-format msgid "Contact the Leaders of %s" msgstr "Contacta a los líderes de %s" -#: assets/js/base-defer.js:1059 -#: assets/js/base-defer.js:1375 +#: assets/js/base-defer.js:1058 +#: assets/js/base-defer.js:1356 msgid "Send" msgstr "Envía" -#: assets/js/base-defer.js:923 -#: assets/js/base-defer.js:934 -#: assets/js/base-defer.js:960 -#: assets/js/base-defer.js:971 -#: assets/js/base-defer.js:1417 -#: assets/js/base-defer.js:1428 +#: assets/js/base-defer.js:926 +#: assets/js/base-defer.js:937 +#: assets/js/base-defer.js:963 +#: assets/js/base-defer.js:974 +#: assets/js/base-defer.js:1398 +#: assets/js/base-defer.js:1409 #: assets/js/meeting-defer.js:203 #: assets/js/meeting-defer.js:214 msgid "OK" msgstr "OK" -#: assets/js/base-defer.js:1468 +#: assets/js/base-defer.js:1449 msgid "Next" msgstr "Siguiente" -#: src/TouchPoint-WP/Involvement.php:1920 +#: src/TouchPoint-WP/Involvement.php:1944 #: src/TouchPoint-WP/Taxonomies.php:869 msgid "Marital Status" msgstr "Estado civil" -#: src/TouchPoint-WP/Involvement.php:1933 +#: src/TouchPoint-WP/Involvement.php:1957 msgid "Age" msgstr "Años" -#: src/TouchPoint-WP/Involvement.php:1804 +#: src/TouchPoint-WP/Involvement.php:1828 msgid "Genders" msgstr "Géneros" @@ -1184,72 +1194,81 @@ msgctxt "Time of Day" msgid "Night" msgstr "Noche" -#: src/TouchPoint-WP/Involvement.php:1921 +#: src/TouchPoint-WP/Involvement.php:1945 msgctxt "Marital status for a group of people" msgid "Mostly Single" msgstr "Mayoría solteras" -#: src/TouchPoint-WP/Involvement.php:1922 +#: src/TouchPoint-WP/Involvement.php:1946 msgctxt "Marital status for a group of people" msgid "Mostly Married" msgstr "Mayoría casadas" #. translators: %s is the link to "reset the map" -#: src/TouchPoint-WP/Involvement.php:1963 -#: src/TouchPoint-WP/Partner.php:881 +#: src/TouchPoint-WP/Involvement.php:1987 +#: src/TouchPoint-WP/Partner.php:882 +#, php-format msgid "Zoom out or %s to see more." msgstr "Alejar o %s para ver más." -#: src/TouchPoint-WP/Involvement.php:1966 -#: src/TouchPoint-WP/Partner.php:884 +#: src/TouchPoint-WP/Involvement.php:1990 +#: src/TouchPoint-WP/Partner.php:885 msgctxt "Zoom out or reset the map to see more." msgid "reset the map" msgstr "restablecer el mapa" #. translators: %1$s is the date(s), %2$s is the time(s). -#: src/TouchPoint-WP/Involvement.php:1014 -#: src/TouchPoint-WP/Involvement.php:1046 -#: src/TouchPoint-WP/Involvement.php:1139 -#: src/TouchPoint-WP/Involvement.php:1163 +#: src/TouchPoint-WP/Involvement.php:1015 +#: src/TouchPoint-WP/Involvement.php:1047 +#: src/TouchPoint-WP/Involvement.php:1140 +#: src/TouchPoint-WP/Involvement.php:1164 #: src/TouchPoint-WP/StatusWidget.php:73 #: src/TouchPoint-WP/Utilities/DateFormats.php:288 #: src/TouchPoint-WP/Utilities/DateFormats.php:352 +#, php-format msgid "%1$s at %2$s" msgstr "%1$s a las %2$s" #. translators: {start date} through {end date} e.g. February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:1055 +#: src/TouchPoint-WP/Involvement.php:1056 +#, php-format msgid "%1$s through %2$s" msgstr "%1$s al %2$s" #. translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:1064 +#: src/TouchPoint-WP/Involvement.php:1065 +#, php-format msgid "%1$s, %2$s through %3$s" msgstr "%1$s, %2$s al %3$s" #. translators: Starts {start date} e.g. Starts September 15 -#: src/TouchPoint-WP/Involvement.php:1073 +#: src/TouchPoint-WP/Involvement.php:1074 +#, php-format msgid "Starts %1$s" msgstr "Comienza el %1$s" #. translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14 -#: src/TouchPoint-WP/Involvement.php:1081 +#: src/TouchPoint-WP/Involvement.php:1082 +#, php-format msgid "%1$s, starting %2$s" msgstr "%1$s, comienza el %2$s" #. translators: Through {end date} e.g. Through September 15 -#: src/TouchPoint-WP/Involvement.php:1089 +#: src/TouchPoint-WP/Involvement.php:1090 +#, php-format msgid "Through %1$s" msgstr "Hasta el %1$s" #. translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14 -#: src/TouchPoint-WP/Involvement.php:1097 +#: src/TouchPoint-WP/Involvement.php:1098 +#, php-format msgid "%1$s, through %2$s" msgstr "%1$s, hasta el %2$s" #. translators: number of miles #: src/templates/parts/involvement-nearby-list.php:10 -#: src/TouchPoint-WP/Involvement.php:3770 +#: src/TouchPoint-WP/Involvement.php:3808 +#, php-format msgctxt "miles. Unit is appended to a number. %2.1f is the number, so %2.1fmi looks like '12.3mi'" msgid "%2.1fmi" msgstr "%2.1fmi" @@ -1266,35 +1285,35 @@ msgstr "La participación tiene un tipo de registro de \"No Online Registration\ msgid "Involvement registration has ended (end date is past)" msgstr "El registro de participación ha finalizado (la fecha de finalización ya pasó)" -#: src/TouchPoint-WP/Involvement.php:2083 +#: src/TouchPoint-WP/Involvement.php:2113 msgid "This involvement type doesn't exist." msgstr "Este tipo de participación no existe." -#: src/TouchPoint-WP/Involvement.php:2093 +#: src/TouchPoint-WP/Involvement.php:2123 msgid "This involvement type doesn't have geographic locations enabled." msgstr "Este tipo de participación no tiene habilitadas las ubicaciones geográficas." -#: src/TouchPoint-WP/Involvement.php:2112 +#: src/TouchPoint-WP/Involvement.php:2142 msgid "Could not locate." msgstr "No se pudo localizar." -#: src/TouchPoint-WP/Meeting.php:715 -#: src/TouchPoint-WP/TouchPointWP.php:1110 +#: src/TouchPoint-WP/Meeting.php:729 +#: src/TouchPoint-WP/TouchPointWP.php:1115 msgid "Only GET requests are allowed." msgstr "Solo se permiten solicitudes GET." -#: src/TouchPoint-WP/Meeting.php:743 -#: src/TouchPoint-WP/TouchPointWP.php:370 +#: src/TouchPoint-WP/Meeting.php:757 +#: src/TouchPoint-WP/TouchPointWP.php:374 msgid "Only POST requests are allowed." msgstr "Solo se permiten solicitudes POST." -#: src/TouchPoint-WP/Meeting.php:753 -#: src/TouchPoint-WP/TouchPointWP.php:379 +#: src/TouchPoint-WP/Meeting.php:767 +#: src/TouchPoint-WP/TouchPointWP.php:383 msgid "Invalid data provided." msgstr "Datos proporcionados no válidos." -#: src/TouchPoint-WP/Involvement.php:4066 -#: src/TouchPoint-WP/Involvement.php:4171 +#: src/TouchPoint-WP/Involvement.php:4106 +#: src/TouchPoint-WP/Involvement.php:4211 msgid "Invalid Post Type." msgstr "Tipo de publicación no válida." @@ -1312,6 +1331,7 @@ msgstr "Clasifique las publicaciones por el campus." #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:718 +#, php-format msgid "Classify things by %s." msgstr "Clasifica las cosas por %s." @@ -1369,31 +1389,37 @@ msgstr "Importar campus como taxonomía. (Probablemente quieras hacer esto si ti #. translators: %s: taxonomy name, plural #: src/TouchPoint-WP/Taxonomies.php:51 +#, php-format msgid "Search %s" msgstr "Buscar %s" #. translators: %s: taxonomy name, plural #: src/TouchPoint-WP/Taxonomies.php:53 +#, php-format msgid "All %s" msgstr "Todos los %s" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:55 +#, php-format msgid "Edit %s" msgstr "Editar %s" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:57 +#, php-format msgid "Update %s" msgstr "Actualizar %s" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:59 +#, php-format msgid "Add New %s" msgstr "Agregar Nuevo %s" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:61 +#, php-format msgid "New %s" msgstr "Nuevo %s" @@ -1407,14 +1433,15 @@ msgstr "Informe de TouchPoint" #. translators: Last updated date/time for a report. %1$s is the date. %2$s is the time. #: src/TouchPoint-WP/Report.php:449 +#, php-format msgid "Updated on %1$s at %2$s" msgstr "Actualizada %1$s %2$s" -#: src/TouchPoint-WP/TouchPointWP.php:274 +#: src/TouchPoint-WP/TouchPointWP.php:278 msgid "Every 15 minutes" msgstr "Cada 15 minutos" -#: src/TouchPoint-WP/Involvement.php:1847 +#: src/TouchPoint-WP/Involvement.php:1871 msgid "Language" msgstr "Idioma" @@ -1426,7 +1453,7 @@ msgstr "Importar imágenes desde TouchPoint" msgid "Importing images sometimes conflicts with other plugins. Disabling image imports can help." msgstr "La importación de imágenes a veces entra en conflicto con otros complementos. Deshabilitar las importaciones de imágenes puede ayudar." -#: src/TouchPoint-WP/Person.php:1473 +#: src/TouchPoint-WP/Person.php:1497 msgctxt "list of people, and *others*" msgid "others" msgstr "otros" @@ -1451,8 +1478,8 @@ msgstr "Slug de reuniones" msgid "The root path for Meetings" msgstr "La ruta raíz para las reuniones" -#: src/TouchPoint-WP/Involvement.php:4158 -#: src/TouchPoint-WP/Person.php:1855 +#: src/TouchPoint-WP/Involvement.php:4198 +#: src/TouchPoint-WP/Person.php:1879 msgid "Contact Prohibited." msgstr "Contacto prohibido." @@ -1468,30 +1495,30 @@ msgstr "El dominio de los enlaces profundos de su aplicación móvil, sin https msgid "Once your settings on this page are set and saved, use this tool to generate the scripts needed for TouchPoint in a convenient installation package." msgstr "Una vez que haya configurado y guardado la configuración en esta página, utilice esta herramienta para generar los scripts necesarios para TouchPoint en un paquete de instalación conveniente." -#: assets/js/base-defer.js:1502 +#: assets/js/base-defer.js:1483 msgid "Something went wrong." msgstr "Algo salió mal." -#: src/TouchPoint-WP/Person.php:1710 +#: src/TouchPoint-WP/Person.php:1734 msgid "You may need to sign in." msgstr "Es posible que tengas que iniciar sesión." -#: src/TouchPoint-WP/Involvement.php:4148 -#: src/TouchPoint-WP/Person.php:1872 +#: src/TouchPoint-WP/Involvement.php:4188 +#: src/TouchPoint-WP/Person.php:1896 msgid "Contact Blocked for Spam." msgstr "Contacto bloqueado por spam." -#: src/TouchPoint-WP/Person.php:1612 +#: src/TouchPoint-WP/Person.php:1636 msgid "Registration Blocked for Spam." msgstr "Registro bloqueado por spam." #: src/templates/meeting-archive.php:27 -#: src/TouchPoint-WP/Meeting.php:291 +#: src/TouchPoint-WP/Meeting.php:292 msgctxt "What Meetings should be called, plural." msgid "Events" msgstr "Eventos" -#: src/TouchPoint-WP/Meeting.php:292 +#: src/TouchPoint-WP/Meeting.php:293 msgctxt "What Meetings should be called, singular." msgid "Event" msgstr "Evento" @@ -1556,59 +1583,67 @@ msgstr "(persona nombrada)" msgid "Expand" msgstr "Ampliar" -#: src/TouchPoint-WP/Meeting.php:592 +#: src/TouchPoint-WP/Meeting.php:606 msgid "Cancelled" msgstr "Cancelado" -#: src/TouchPoint-WP/Meeting.php:593 +#: src/TouchPoint-WP/Meeting.php:607 msgid "Scheduled" msgstr "Programado" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:147 +#, php-format msgctxt "Date format string" msgid "Last %1$s, %2$s" msgstr "el pasado %1$s %2$s" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:153 +#, php-format msgctxt "Date format string" msgid "This %1$s, %2$s" msgstr "este %1$s %2$s" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:159 +#, php-format msgctxt "Date format string" msgid "Next %1$s, %2$s" msgstr "el proximo %1$s %2$s" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:164 +#, php-format msgctxt "Date format string" msgid "%1$s, %2$s" msgstr "%1$s %2$s" #. Translators: %s is the singular name of the of a Meeting, such as "Event". #: src/TouchPoint-WP/CalendarGrid.php:199 +#, php-format msgid "%s is cancelled." msgstr "%s esta cancelado." -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Involvement.php:3858 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Involvement.php:3898 +#, php-format msgid "Involvement in %s" msgstr "Participaciones en %s" -#: src/TouchPoint-WP/Meeting.php:454 +#: src/TouchPoint-WP/Meeting.php:467 msgid "In the Past" msgstr "en el pasado" -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Meeting.php:527 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Meeting.php:541 +#, php-format msgid "Meeting in %s" msgstr "Reunión en %s" -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Person.php:1195 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Person.php:1209 +#, php-format msgid "Person in %s" msgstr "Persona en %s" @@ -1668,28 +1703,30 @@ msgctxt "Date string when the year is not current." msgid "F j, Y" msgstr "j F Y" -#: src/TouchPoint-WP/Meeting.php:594 +#: src/TouchPoint-WP/Meeting.php:608 msgctxt "Event Status is not a recognized value." msgid "Unknown" msgstr "desconocido" -#: src/TouchPoint-WP/Involvement.php:142 +#: src/TouchPoint-WP/Involvement.php:143 msgid "Creating an Involvement object from an object without a post_id is not yet supported." msgstr "Aún no se admite la creación de un objeto de participación a partir de un objeto sin post_id." #. translators: "Mon All Day" or "Sundays All Day" -#: src/TouchPoint-WP/Involvement.php:1017 -#: src/TouchPoint-WP/Involvement.php:1040 +#: src/TouchPoint-WP/Involvement.php:1018 +#: src/TouchPoint-WP/Involvement.php:1041 +#, php-format msgid "%1$s All Day" msgstr "todo el dia los %1$s" -#: src/TouchPoint-WP/Meeting.php:106 +#: src/TouchPoint-WP/Meeting.php:107 msgid "Creating a Meeting object from an object without a post_id is not yet supported." msgstr "Aún no se admite la creación de un objeto de reunión a partir de un objeto sin post_id." #. translators: %1$s is the start date/time, %2$s is the end date/time. #: src/TouchPoint-WP/Utilities/DateFormats.php:89 #: src/TouchPoint-WP/Utilities/DateFormats.php:331 +#, php-format msgid "%1$s – %2$s" msgstr "%1$s – %2$s" @@ -1715,35 +1752,41 @@ msgstr "j M Y" #. translators: %1$s is the start date, %2$s start time, %3$s is the end date, and %4$s end time. #: src/TouchPoint-WP/Utilities/DateFormats.php:364 +#, php-format msgid "%1$s at %2$s – %3$s at %4$s" msgstr "%1$s a %2$s – %3$s a %4$s" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:221 +#, php-format msgctxt "Short date format string" msgid "Last %1$s, %2$s" msgstr "el pasado %1$s %2$s" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:227 +#, php-format msgctxt "Short date format string" msgid "This %1$s, %2$s" msgstr "este %1$s %2$s" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:233 +#, php-format msgctxt "Short date format string" msgid "Next %1$s, %2$s" msgstr "proximo %1$s %2$s" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:238 +#, php-format msgctxt "Short date format string" msgid "%1$s, %2$s" msgstr "%1$s %2$s" #. Translators: %s is the plural name of the of the Meetings, such as "Events". #: src/TouchPoint-WP/CalendarGrid.php:279 +#, php-format msgid "There are no %s published for this month." msgstr "No hay %s publicados para este mes." @@ -1781,10 +1824,12 @@ msgstr "Integre la versión 2.0 de la aplicación móvil personalizada con el ca #. Translators: %s is the singular name of the of a Meeting, such as "Event". #: src/templates/involvement-single.php:49 +#, php-format msgid "This %s has been Cancelled." msgstr "Este %s ha sido Cancelado." #: src/templates/admin/invKoForm.php:210 +#: blocks/inv-list/index.js:98 msgid "Division" msgstr "Division" @@ -1796,7 +1841,7 @@ msgstr "Código de Residente" msgid "Campus" msgstr "Campus" -#: src/TouchPoint-WP/Involvement.php:1041 +#: src/TouchPoint-WP/Involvement.php:1042 msgid "All Day" msgstr "todo el dia" @@ -1856,6 +1901,7 @@ msgstr "Permita que los desarrolladores de TouchPoint-WP incluyan públicamente #. translators: %s is "what you call TouchPoint at your church", which is a setting #: src/TouchPoint-WP/Auth.php:135 +#, php-format msgid "Sign in with %s" msgstr "Iniciar sesión con %s" @@ -1871,7 +1917,7 @@ msgstr "Las reuniones se mantendrán en el calendario hasta que el evento tenga msgid "Change Profile Links" msgstr "Cambiar enlaces de perfil" -#: src/TouchPoint-WP/TouchPointWP.php:2379 +#: src/TouchPoint-WP/TouchPointWP.php:2384 msgid "People Count Failed" msgstr "El recuento de personas falló" @@ -1888,7 +1934,7 @@ msgstr "sin recoger" #: src/templates/admin/invKoForm.php:114 #: src/TouchPoint-WP/Settings.php:984 msgid "Collect Meetings only from Involvements without Schedules" -msgstr "" +msgstr "Recopilar reuniones solo de participaciones sin horarios" #: src/templates/admin/invKoForm.php:115 #: src/TouchPoint-WP/Settings.php:988 @@ -1899,3 +1945,29 @@ msgstr "Recopilar reuniones solo de participaciones sin horarios" #: src/TouchPoint-WP/Settings.php:980 msgid "Allows multiple meetings that are part of one larger event to be grouped together, such as sessions within a conference. For meetings to be collected, they must be in the same involvement and must not have gaps between them larger than 23 hours." msgstr "Permite agrupar varias reuniones que forman parte de un evento mayor, como las sesiones de una conferencia. Para que se recopilen, las reuniones deben estar relacionadas con la misma actividad y no deben tener intervalos superiores a 23 horas entre ellas." + +#: blocks/inv-list/index.js:22 +msgid "Involvement List" +msgstr "Lista de Participaciones" + +#: blocks/inv-list/index.js:24 +msgid "A list of Involvements from TouchPoint" +msgstr "Una lista de participaciones de TouchPoint" + +#: blocks/inv-list/index.js:70 +msgid "All" +msgstr "Todos" + +#: blocks/inv-list/index.js:92 +msgid "Post Type" +msgstr "tipo de publicación" + +#: blocks/inv-list/block.json +msgctxt "block title" +msgid "Involvement List" +msgstr "Lista de Participaciones" + +#: blocks/inv-list/block.json +msgctxt "block description" +msgid "A list of Involvements from TouchPoint" +msgstr "Una lista de participaciones de TouchPoint" diff --git a/i18n/TouchPoint-WP.pot b/i18n/TouchPoint-WP.pot index ec00ec5f..9308e33f 100644 --- a/i18n/TouchPoint-WP.pot +++ b/i18n/TouchPoint-WP.pot @@ -9,9 +9,9 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2025-05-01T19:21:04+00:00\n" +"POT-Creation-Date: 2025-06-09T12:46:24+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"X-Generator: WP-CLI 2.11.0\n" +"X-Generator: WP-CLI 2.12.0\n" "X-Domain: TouchPoint-WP\n" #. Plugin Name of the plugin @@ -71,10 +71,10 @@ msgstr "" #: src/templates/admin/invKoForm.php:178 #: src/templates/admin/invKoForm.php:332 #: src/templates/parts/involvement-nearby-list.php:2 -#: src/TouchPoint-WP/Meeting.php:789 +#: src/TouchPoint-WP/Meeting.php:803 #: src/TouchPoint-WP/Rsvp.php:77 -#: assets/js/base-defer.js:192 -#: assets/js/base-defer.js:1133 +#: assets/js/base-defer.js:195 +#: assets/js/base-defer.js:1113 msgid "Loading..." msgstr "" @@ -192,6 +192,7 @@ msgid "Default Filters" msgstr "" #: src/templates/admin/invKoForm.php:210 +#: blocks/inv-list/index.js:98 msgid "Division" msgstr "" @@ -208,13 +209,13 @@ msgid "Gender" msgstr "" #: src/templates/admin/invKoForm.php:250 -#: src/TouchPoint-WP/Involvement.php:1872 +#: src/TouchPoint-WP/Involvement.php:1896 #: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekday" msgstr "" #: src/templates/admin/invKoForm.php:254 -#: src/TouchPoint-WP/Involvement.php:1898 +#: src/TouchPoint-WP/Involvement.php:1922 #: src/TouchPoint-WP/Taxonomies.php:808 msgid "Time of Day" msgstr "" @@ -298,29 +299,33 @@ msgstr "" #. Translators: %s is the singular name of the of a Meeting, such as "Event". #: src/templates/involvement-single.php:49 +#, php-format msgid "This %s has been Cancelled." msgstr "" #: src/templates/meeting-archive.php:27 -#: src/TouchPoint-WP/Meeting.php:291 +#: src/TouchPoint-WP/Meeting.php:292 msgctxt "What Meetings should be called, plural." msgid "Events" msgstr "" #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:16 -#: src/TouchPoint-WP/Involvement.php:2142 +#: src/TouchPoint-WP/Involvement.php:2172 +#, php-format msgid "No %s Found." msgstr "" #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:20 +#, php-format msgid "%s will be imported overnight for the first time." msgstr "" #. translators: number of miles #: src/templates/parts/involvement-nearby-list.php:10 -#: src/TouchPoint-WP/Involvement.php:3770 +#: src/TouchPoint-WP/Involvement.php:3808 +#, php-format msgctxt "miles. Unit is appended to a number. %2.1f is the number, so %2.1fmi looks like '12.3mi'" msgid "%2.1fmi" msgstr "" @@ -343,6 +348,7 @@ msgstr "" #. translators: %s is "what you call TouchPoint at your church", which is a setting #: src/TouchPoint-WP/Auth.php:135 +#, php-format msgid "Sign in with %s" msgstr "" @@ -357,11 +363,13 @@ msgstr "" #. Translators: %s is the singular name of the of a Meeting, such as "Event". #: src/TouchPoint-WP/CalendarGrid.php:199 +#, php-format msgid "%s is cancelled." msgstr "" #. Translators: %s is the plural name of the of the Meetings, such as "Events". #: src/TouchPoint-WP/CalendarGrid.php:279 +#, php-format msgid "There are no %s published for this month." msgstr "" @@ -374,304 +382,318 @@ msgstr "" msgid "Multi-Day" msgstr "" -#: src/TouchPoint-WP/Involvement.php:142 +#: src/TouchPoint-WP/Involvement.php:143 msgid "Creating an Involvement object from an object without a post_id is not yet supported." msgstr "" -#: src/TouchPoint-WP/Involvement.php:514 +#: src/TouchPoint-WP/Involvement.php:515 msgid "Currently Full" msgstr "" -#: src/TouchPoint-WP/Involvement.php:519 +#: src/TouchPoint-WP/Involvement.php:520 msgid "Currently Closed" msgstr "" -#: src/TouchPoint-WP/Involvement.php:526 +#: src/TouchPoint-WP/Involvement.php:527 msgid "Registration Not Open Yet" msgstr "" -#: src/TouchPoint-WP/Involvement.php:532 +#: src/TouchPoint-WP/Involvement.php:533 msgid "Registration Closed" msgstr "" #. translators: %1$s is the date(s), %2$s is the time(s). -#: src/TouchPoint-WP/Involvement.php:1014 -#: src/TouchPoint-WP/Involvement.php:1046 -#: src/TouchPoint-WP/Involvement.php:1139 -#: src/TouchPoint-WP/Involvement.php:1163 +#: src/TouchPoint-WP/Involvement.php:1015 +#: src/TouchPoint-WP/Involvement.php:1047 +#: src/TouchPoint-WP/Involvement.php:1140 +#: src/TouchPoint-WP/Involvement.php:1164 #: src/TouchPoint-WP/StatusWidget.php:73 #: src/TouchPoint-WP/Utilities/DateFormats.php:288 #: src/TouchPoint-WP/Utilities/DateFormats.php:352 +#, php-format msgid "%1$s at %2$s" msgstr "" #. translators: "Mon All Day" or "Sundays All Day" -#: src/TouchPoint-WP/Involvement.php:1017 -#: src/TouchPoint-WP/Involvement.php:1040 +#: src/TouchPoint-WP/Involvement.php:1018 +#: src/TouchPoint-WP/Involvement.php:1041 +#, php-format msgid "%1$s All Day" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1041 +#: src/TouchPoint-WP/Involvement.php:1042 msgid "All Day" msgstr "" #. translators: {start date} through {end date} e.g. February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:1055 +#: src/TouchPoint-WP/Involvement.php:1056 +#, php-format msgid "%1$s through %2$s" msgstr "" #. translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:1064 +#: src/TouchPoint-WP/Involvement.php:1065 +#, php-format msgid "%1$s, %2$s through %3$s" msgstr "" #. translators: Starts {start date} e.g. Starts September 15 -#: src/TouchPoint-WP/Involvement.php:1073 +#: src/TouchPoint-WP/Involvement.php:1074 +#, php-format msgid "Starts %1$s" msgstr "" #. translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14 -#: src/TouchPoint-WP/Involvement.php:1081 +#: src/TouchPoint-WP/Involvement.php:1082 +#, php-format msgid "%1$s, starting %2$s" msgstr "" #. translators: Through {end date} e.g. Through September 15 -#: src/TouchPoint-WP/Involvement.php:1089 +#: src/TouchPoint-WP/Involvement.php:1090 +#, php-format msgid "Through %1$s" msgstr "" #. translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14 -#: src/TouchPoint-WP/Involvement.php:1097 +#: src/TouchPoint-WP/Involvement.php:1098 +#, php-format msgid "%1$s, through %2$s" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1747 -#: src/TouchPoint-WP/Partner.php:841 +#: src/TouchPoint-WP/Involvement.php:1771 +#: src/TouchPoint-WP/Partner.php:842 msgid "Any" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1804 +#: src/TouchPoint-WP/Involvement.php:1828 msgid "Genders" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1847 +#: src/TouchPoint-WP/Involvement.php:1871 msgid "Language" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1920 +#: src/TouchPoint-WP/Involvement.php:1944 #: src/TouchPoint-WP/Taxonomies.php:869 msgid "Marital Status" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1921 +#: src/TouchPoint-WP/Involvement.php:1945 msgctxt "Marital status for a group of people" msgid "Mostly Single" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1922 +#: src/TouchPoint-WP/Involvement.php:1946 msgctxt "Marital status for a group of people" msgid "Mostly Married" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1933 +#: src/TouchPoint-WP/Involvement.php:1957 msgid "Age" msgstr "" #. translators: %s is for the user-provided term for the items on the map (e.g. Small Group or Partner) -#: src/TouchPoint-WP/Involvement.php:1955 -#: src/TouchPoint-WP/Partner.php:865 +#: src/TouchPoint-WP/Involvement.php:1979 +#: src/TouchPoint-WP/Partner.php:866 +#, php-format msgid "The %s listed are only those shown on the map." msgstr "" #. translators: %s is the link to "reset the map" -#: src/TouchPoint-WP/Involvement.php:1963 -#: src/TouchPoint-WP/Partner.php:881 +#: src/TouchPoint-WP/Involvement.php:1987 +#: src/TouchPoint-WP/Partner.php:882 +#, php-format msgid "Zoom out or %s to see more." msgstr "" -#: src/TouchPoint-WP/Involvement.php:1966 -#: src/TouchPoint-WP/Partner.php:884 +#: src/TouchPoint-WP/Involvement.php:1990 +#: src/TouchPoint-WP/Partner.php:885 msgctxt "Zoom out or reset the map to see more." msgid "reset the map" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2083 +#: src/TouchPoint-WP/Involvement.php:2113 msgid "This involvement type doesn't exist." msgstr "" -#: src/TouchPoint-WP/Involvement.php:2093 +#: src/TouchPoint-WP/Involvement.php:2123 msgid "This involvement type doesn't have geographic locations enabled." msgstr "" -#: src/TouchPoint-WP/Involvement.php:2112 +#: src/TouchPoint-WP/Involvement.php:2142 msgid "Could not locate." msgstr "" -#: src/TouchPoint-WP/Involvement.php:3749 +#: src/TouchPoint-WP/Involvement.php:3787 msgid "Men Only" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3752 +#: src/TouchPoint-WP/Involvement.php:3790 msgid "Women Only" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3829 +#: src/TouchPoint-WP/Involvement.php:3869 msgid "Contact Leaders" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3846 -#: src/TouchPoint-WP/Partner.php:1345 +#: src/TouchPoint-WP/Involvement.php:3886 +#: src/TouchPoint-WP/Partner.php:1353 msgid "Show on Map" msgstr "" -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Involvement.php:3858 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Involvement.php:3898 +#, php-format msgid "Involvement in %s" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3899 -#: src/TouchPoint-WP/Involvement.php:3958 +#: src/TouchPoint-WP/Involvement.php:3939 +#: src/TouchPoint-WP/Involvement.php:3998 msgid "Register" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3905 +#: src/TouchPoint-WP/Involvement.php:3945 msgid "Create Account" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3909 +#: src/TouchPoint-WP/Involvement.php:3949 msgid "Schedule" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3914 +#: src/TouchPoint-WP/Involvement.php:3954 msgid "Give" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3917 +#: src/TouchPoint-WP/Involvement.php:3957 msgid "Manage Subscriptions" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3920 +#: src/TouchPoint-WP/Involvement.php:3960 msgid "Record Attendance" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3923 +#: src/TouchPoint-WP/Involvement.php:3963 msgid "Get Tickets" msgstr "" -#: src/TouchPoint-WP/Involvement.php:3949 -#: assets/js/base-defer.js:1001 +#: src/TouchPoint-WP/Involvement.php:3989 +#: assets/js/base-defer.js:1004 msgid "Join" msgstr "" -#: src/TouchPoint-WP/Involvement.php:4066 -#: src/TouchPoint-WP/Involvement.php:4171 +#: src/TouchPoint-WP/Involvement.php:4106 +#: src/TouchPoint-WP/Involvement.php:4211 msgid "Invalid Post Type." msgstr "" -#: src/TouchPoint-WP/Involvement.php:4148 -#: src/TouchPoint-WP/Person.php:1872 +#: src/TouchPoint-WP/Involvement.php:4188 +#: src/TouchPoint-WP/Person.php:1896 msgid "Contact Blocked for Spam." msgstr "" -#: src/TouchPoint-WP/Involvement.php:4158 -#: src/TouchPoint-WP/Person.php:1855 +#: src/TouchPoint-WP/Involvement.php:4198 +#: src/TouchPoint-WP/Person.php:1879 msgid "Contact Prohibited." msgstr "" -#: src/TouchPoint-WP/Meeting.php:106 +#: src/TouchPoint-WP/Meeting.php:107 msgid "Creating a Meeting object from an object without a post_id is not yet supported." msgstr "" -#: src/TouchPoint-WP/Meeting.php:292 +#: src/TouchPoint-WP/Meeting.php:293 msgctxt "What Meetings should be called, singular." msgid "Event" msgstr "" -#: src/TouchPoint-WP/Meeting.php:454 +#: src/TouchPoint-WP/Meeting.php:467 msgid "In the Past" msgstr "" -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Meeting.php:527 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Meeting.php:541 +#, php-format msgid "Meeting in %s" msgstr "" -#: src/TouchPoint-WP/Meeting.php:592 +#: src/TouchPoint-WP/Meeting.php:606 msgid "Cancelled" msgstr "" -#: src/TouchPoint-WP/Meeting.php:593 +#: src/TouchPoint-WP/Meeting.php:607 msgid "Scheduled" msgstr "" -#: src/TouchPoint-WP/Meeting.php:594 +#: src/TouchPoint-WP/Meeting.php:608 msgctxt "Event Status is not a recognized value." msgid "Unknown" msgstr "" -#: src/TouchPoint-WP/Meeting.php:715 -#: src/TouchPoint-WP/TouchPointWP.php:1110 +#: src/TouchPoint-WP/Meeting.php:729 +#: src/TouchPoint-WP/TouchPointWP.php:1115 msgid "Only GET requests are allowed." msgstr "" -#: src/TouchPoint-WP/Meeting.php:743 -#: src/TouchPoint-WP/TouchPointWP.php:370 +#: src/TouchPoint-WP/Meeting.php:757 +#: src/TouchPoint-WP/TouchPointWP.php:374 msgid "Only POST requests are allowed." msgstr "" -#: src/TouchPoint-WP/Meeting.php:753 -#: src/TouchPoint-WP/TouchPointWP.php:379 +#: src/TouchPoint-WP/Meeting.php:767 +#: src/TouchPoint-WP/TouchPointWP.php:383 msgid "Invalid data provided." msgstr "" -#: src/TouchPoint-WP/Meeting.php:788 -#: src/TouchPoint-WP/Meeting.php:809 +#: src/TouchPoint-WP/Meeting.php:802 +#: src/TouchPoint-WP/Meeting.php:823 #: src/TouchPoint-WP/Rsvp.php:82 msgid "RSVP" msgstr "" #. translators: %s is for the user-provided "Global Partner" and "Secure Partner" terms. -#: src/TouchPoint-WP/Partner.php:872 +#: src/TouchPoint-WP/Partner.php:873 +#, php-format msgid "The %1$s listed are only those shown on the map, as well as all %2$s." msgstr "" -#: src/TouchPoint-WP/Partner.php:1286 +#: src/TouchPoint-WP/Partner.php:1292 msgid "Not Shown on Map" msgstr "" -#: src/TouchPoint-WP/Person.php:146 +#: src/TouchPoint-WP/Person.php:148 msgid "No WordPress User ID provided for initializing a person object." msgstr "" -#: src/TouchPoint-WP/Person.php:638 +#: src/TouchPoint-WP/Person.php:640 msgid "TouchPoint People ID" msgstr "" -#: src/TouchPoint-WP/Person.php:1187 +#: src/TouchPoint-WP/Person.php:1196 msgid "Contact" msgstr "" -#. Translators: %s is the system name. "TouchPoint" by default. -#: src/TouchPoint-WP/Person.php:1195 +#. Translators: %s is the system name, "TouchPoint" by default. +#: src/TouchPoint-WP/Person.php:1209 +#, php-format msgid "Person in %s" msgstr "" -#: src/TouchPoint-WP/Person.php:1466 +#: src/TouchPoint-WP/Person.php:1490 #: src/TouchPoint-WP/Utilities.php:299 -#: assets/js/base-defer.js:18 +#: assets/js/base-defer.js:20 msgid "and" msgstr "" -#: src/TouchPoint-WP/Person.php:1473 +#: src/TouchPoint-WP/Person.php:1497 msgctxt "list of people, and *others*" msgid "others" msgstr "" -#: src/TouchPoint-WP/Person.php:1612 +#: src/TouchPoint-WP/Person.php:1636 msgid "Registration Blocked for Spam." msgstr "" -#: src/TouchPoint-WP/Person.php:1710 +#: src/TouchPoint-WP/Person.php:1734 msgid "You may need to sign in." msgstr "" @@ -685,6 +707,7 @@ msgstr "" #. translators: Last updated date/time for a report. %1$s is the date. %2$s is the time. #: src/TouchPoint-WP/Report.php:449 +#, php-format msgid "Updated on %1$s at %2$s" msgstr "" @@ -1306,6 +1329,7 @@ msgid "TouchPoint-WP" msgstr "" #: src/TouchPoint-WP/Settings.php:1451 +#: blocks/inv-list/index.js:90 msgid "Settings" msgstr "" @@ -1335,31 +1359,37 @@ msgstr "" #. translators: %s: taxonomy name, plural #: src/TouchPoint-WP/Taxonomies.php:51 +#, php-format msgid "Search %s" msgstr "" #. translators: %s: taxonomy name, plural #: src/TouchPoint-WP/Taxonomies.php:53 +#, php-format msgid "All %s" msgstr "" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:55 +#, php-format msgid "Edit %s" msgstr "" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:57 +#, php-format msgid "Update %s" msgstr "" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:59 +#, php-format msgid "Add New %s" msgstr "" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:61 +#, php-format msgid "New %s" msgstr "" @@ -1373,6 +1403,7 @@ msgstr "" #. translators: %s: taxonomy name, singular #: src/TouchPoint-WP/Taxonomies.php:718 +#, php-format msgid "Classify things by %s." msgstr "" @@ -1424,36 +1455,36 @@ msgstr "" msgid "Classify Partners by category chosen in settings." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:274 +#: src/TouchPoint-WP/TouchPointWP.php:278 msgid "Every 15 minutes" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2120 +#: src/TouchPoint-WP/TouchPointWP.php:2125 msgid "Unknown Type" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2177 +#: src/TouchPoint-WP/TouchPointWP.php:2182 msgid "Your Searches" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2180 +#: src/TouchPoint-WP/TouchPointWP.php:2185 msgid "Public Searches" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2183 +#: src/TouchPoint-WP/TouchPointWP.php:2188 msgid "Status Flags" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2188 -#: src/TouchPoint-WP/TouchPointWP.php:2189 +#: src/TouchPoint-WP/TouchPointWP.php:2193 +#: src/TouchPoint-WP/TouchPointWP.php:2194 msgid "Current Value" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2379 +#: src/TouchPoint-WP/TouchPointWP.php:2384 msgid "People Count Failed" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2403 +#: src/TouchPoint-WP/TouchPointWP.php:2408 msgid "People Query Failed" msgstr "" @@ -1574,6 +1605,7 @@ msgstr "" #. translators: %1$s is the start date/time, %2$s is the end date/time. #: src/TouchPoint-WP/Utilities/DateFormats.php:89 #: src/TouchPoint-WP/Utilities/DateFormats.php:331 +#, php-format msgid "%1$s – %2$s" msgstr "" @@ -1619,24 +1651,28 @@ msgstr "" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:147 +#, php-format msgctxt "Date format string" msgid "Last %1$s, %2$s" msgstr "" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:153 +#, php-format msgctxt "Date format string" msgid "This %1$s, %2$s" msgstr "" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:159 +#, php-format msgctxt "Date format string" msgid "Next %1$s, %2$s" msgstr "" #. translators: %1$s is "Monday". %2$s is "January 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:164 +#, php-format msgctxt "Date format string" msgid "%1$s, %2$s" msgstr "" @@ -1663,192 +1699,201 @@ msgstr "" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:221 +#, php-format msgctxt "Short date format string" msgid "Last %1$s, %2$s" msgstr "" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:227 +#, php-format msgctxt "Short date format string" msgid "This %1$s, %2$s" msgstr "" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:233 +#, php-format msgctxt "Short date format string" msgid "Next %1$s, %2$s" msgstr "" #. translators: %1$s is "Mon". %2$s is "Jan 1". #: src/TouchPoint-WP/Utilities/DateFormats.php:238 +#, php-format msgctxt "Short date format string" msgid "%1$s, %2$s" msgstr "" #. translators: %1$s is the start date, %2$s start time, %3$s is the end date, and %4$s end time. #: src/TouchPoint-WP/Utilities/DateFormats.php:364 +#, php-format msgid "%1$s at %2$s – %3$s at %4$s" msgstr "" -#: assets/js/base-defer.js:212 -#: assets/js/base-defer.js:1168 +#: assets/js/base-defer.js:215 +#: assets/js/base-defer.js:1148 msgid "Your Location" msgstr "" -#: assets/js/base-defer.js:233 +#: assets/js/base-defer.js:236 msgid "User denied the request for Geolocation." msgstr "" -#: assets/js/base-defer.js:237 +#: assets/js/base-defer.js:240 msgid "Location information is unavailable." msgstr "" -#: assets/js/base-defer.js:241 +#: assets/js/base-defer.js:244 msgid "The request to get user location timed out." msgstr "" -#: assets/js/base-defer.js:245 +#: assets/js/base-defer.js:248 msgid "An unknown error occurred." msgstr "" -#: assets/js/base-defer.js:281 -#: assets/js/base-defer.js:291 +#: assets/js/base-defer.js:284 +#: assets/js/base-defer.js:294 msgid "No geolocation option available." msgstr "" #. translators: %s is the name of an involvement, like a particular small group -#: assets/js/base-defer.js:920 +#: assets/js/base-defer.js:923 +#, js-format msgid "Added to %s" msgstr "" -#: assets/js/base-defer.js:923 -#: assets/js/base-defer.js:934 -#: assets/js/base-defer.js:960 -#: assets/js/base-defer.js:971 -#: assets/js/base-defer.js:1417 -#: assets/js/base-defer.js:1428 +#: assets/js/base-defer.js:926 +#: assets/js/base-defer.js:937 +#: assets/js/base-defer.js:963 +#: assets/js/base-defer.js:974 +#: assets/js/base-defer.js:1398 +#: assets/js/base-defer.js:1409 #: assets/js/meeting-defer.js:203 #: assets/js/meeting-defer.js:214 msgid "OK" msgstr "" -#: assets/js/base-defer.js:931 -#: assets/js/base-defer.js:968 -#: assets/js/base-defer.js:1425 +#: assets/js/base-defer.js:934 +#: assets/js/base-defer.js:971 +#: assets/js/base-defer.js:1406 #: assets/js/meeting-defer.js:211 msgid "Something strange happened." msgstr "" -#: assets/js/base-defer.js:957 -#: assets/js/base-defer.js:1414 +#: assets/js/base-defer.js:960 +#: assets/js/base-defer.js:1395 msgid "Your message has been sent." msgstr "" #. translators: %s is the name of an Involvement -#: assets/js/base-defer.js:981 +#: assets/js/base-defer.js:984 +#, js-format msgid "Join %s" msgstr "" -#: assets/js/base-defer.js:997 +#: assets/js/base-defer.js:1000 msgid "Who is joining the group?" msgstr "" -#: assets/js/base-defer.js:1002 -#: assets/js/base-defer.js:1060 -#: assets/js/base-defer.js:1376 -#: assets/js/base-defer.js:1469 +#: assets/js/base-defer.js:1005 +#: assets/js/base-defer.js:1059 +#: assets/js/base-defer.js:1357 +#: assets/js/base-defer.js:1450 #: assets/js/meeting-defer.js:253 msgid "Cancel" msgstr "" -#: assets/js/base-defer.js:1015 +#: assets/js/base-defer.js:1018 msgid "Select who should be added to the group." msgstr "" #. translators: %s is the name of an involvement. This is a heading for a modal. -#: assets/js/base-defer.js:1036 +#: assets/js/base-defer.js:1035 +#, js-format msgid "Contact the Leaders of %s" msgstr "" -#: assets/js/base-defer.js:1053 -#: assets/js/base-defer.js:1369 +#: assets/js/base-defer.js:1052 +#: assets/js/base-defer.js:1350 msgid "From" msgstr "" -#: assets/js/base-defer.js:1054 -#: assets/js/base-defer.js:1370 +#: assets/js/base-defer.js:1053 +#: assets/js/base-defer.js:1351 msgid "Message" msgstr "" -#: assets/js/base-defer.js:1059 -#: assets/js/base-defer.js:1375 +#: assets/js/base-defer.js:1058 +#: assets/js/base-defer.js:1356 msgid "Send" msgstr "" -#: assets/js/base-defer.js:1069 -#: assets/js/base-defer.js:1385 +#: assets/js/base-defer.js:1068 +#: assets/js/base-defer.js:1366 msgid "Please provide a message." msgstr "" -#: assets/js/base-defer.js:1154 -#: assets/js/base-defer.js:1156 +#: assets/js/base-defer.js:1134 +#: assets/js/base-defer.js:1136 msgid "We don't know where you are." msgstr "" -#: assets/js/base-defer.js:1154 -#: assets/js/base-defer.js:1164 +#: assets/js/base-defer.js:1134 +#: assets/js/base-defer.js:1144 msgid "Click here to use your actual location." msgstr "" -#: assets/js/base-defer.js:1315 -#: assets/js/base-defer.js:1332 +#: assets/js/base-defer.js:1296 +#: assets/js/base-defer.js:1313 msgid "clear" msgstr "" -#: assets/js/base-defer.js:1321 +#: assets/js/base-defer.js:1302 msgid "Other Relatives..." msgstr "" #. translators: %s is a person's name. This is a heading for a contact modal. -#: assets/js/base-defer.js:1352 +#: assets/js/base-defer.js:1333 +#, js-format msgid "Contact %s" msgstr "" -#: assets/js/base-defer.js:1459 +#: assets/js/base-defer.js:1440 msgid "Tell us about yourself." msgstr "" -#: assets/js/base-defer.js:1461 -#: assets/js/base-defer.js:1516 +#: assets/js/base-defer.js:1442 +#: assets/js/base-defer.js:1497 msgid "Email Address" msgstr "" -#: assets/js/base-defer.js:1462 -#: assets/js/base-defer.js:1517 +#: assets/js/base-defer.js:1443 +#: assets/js/base-defer.js:1498 msgid "Zip Code" msgstr "" -#: assets/js/base-defer.js:1468 +#: assets/js/base-defer.js:1449 msgid "Next" msgstr "" -#: assets/js/base-defer.js:1502 +#: assets/js/base-defer.js:1483 msgid "Something went wrong." msgstr "" -#: assets/js/base-defer.js:1514 +#: assets/js/base-defer.js:1495 msgid "Our system doesn't recognize you,
so we need a little more info." msgstr "" -#: assets/js/base-defer.js:1518 +#: assets/js/base-defer.js:1499 msgid "First Name" msgstr "" -#: assets/js/base-defer.js:1519 +#: assets/js/base-defer.js:1500 msgid "Last Name" msgstr "" -#: assets/js/base-defer.js:1521 +#: assets/js/base-defer.js:1502 msgid "Phone" msgstr "" @@ -1864,6 +1909,7 @@ msgstr[1] "" #. translators: "RSVP for {Event Name}" This is the heading on the RSVP modal. The event name isn't translated because it comes from TouchPoint. #: assets/js/meeting-defer.js:233 +#, js-format msgid "RSVP for %s" msgstr "" @@ -1902,3 +1948,29 @@ msgstr "" #: assets/js/meeting-defer.js:271 msgid "Nothing to submit." msgstr "" + +#: blocks/inv-list/index.js:22 +msgid "Involvement List" +msgstr "" + +#: blocks/inv-list/index.js:24 +msgid "A list of Involvements from TouchPoint" +msgstr "" + +#: blocks/inv-list/index.js:70 +msgid "All" +msgstr "" + +#: blocks/inv-list/index.js:92 +msgid "Post Type" +msgstr "" + +#: blocks/inv-list/block.json +msgctxt "block title" +msgid "Involvement List" +msgstr "" + +#: blocks/inv-list/block.json +msgctxt "block description" +msgid "A list of Involvements from TouchPoint" +msgstr "" diff --git a/package.json b/package.json index ddbe5cce..f6ff979a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,12 @@ }, "homepage": "https://github.com/TenthPres/TouchPoint-WP#readme", "devDependencies": { + "@wordpress/block-editor": "^14.17.0", + "@wordpress/blocks": "^14.11.0", + "@wordpress/env": "^10.22.0", + "@wordpress/i18n": "^5.22.0", + "@wordpress/scripts": "^30.15.0", + "style-loader": "^4.0.0", "uglify-js": "^3.17.4" } -} +} \ No newline at end of file diff --git a/src/TouchPoint-WP/Blocks/BlocksController.php b/src/TouchPoint-WP/Blocks/BlocksController.php new file mode 100644 index 00000000..c167c16d --- /dev/null +++ b/src/TouchPoint-WP/Blocks/BlocksController.php @@ -0,0 +1,118 @@ + $block_metadata) { + $block_dir = $blocksRoot . $block_name; + + if (isset($block_metadata['editorScript'])) { + $fileName = substr($block_metadata['editorScript'], 7); + wp_enqueue_script( + "{$block_name}-editor-script", + plugins_url("$block_name/$fileName", $block_dir), + ['wp-blocks', 'wp-element', 'wp-editor'], + TouchPointWP::VERSION + ); + } + + if (isset($block_metadata['editorStyle'])) { + $fileName = substr($block_metadata['editorStyle'], 7); + wp_enqueue_style( + "{$block_name}-editor-style", + plugins_url("$block_name/$fileName", $block_dir), + [], + TouchPointWP::VERSION + ); + } + + if (isset($block_metadata['style'])) { + $fileName = substr($block_metadata['style'], 7); + wp_enqueue_style( + "{$block_name}-style", + plugins_url("$block_name/$fileName", $block_dir), + [], + TouchPointWP::VERSION + ); + } + + if (isset($block_metadata['viewScript'])) { + $fileName = substr($block_metadata['viewScript'], 7); + wp_enqueue_script( + "{$block_name}-view-script", + plugins_url("$block_name/$fileName", $block_dir), + [], + TouchPointWP::VERSION + ); + } + } + } +} + diff --git a/src/TouchPoint-WP/Involvement.php b/src/TouchPoint-WP/Involvement.php index 84805cf4..f056e0e4 100644 --- a/src/TouchPoint-WP/Involvement.php +++ b/src/TouchPoint-WP/Involvement.php @@ -69,7 +69,7 @@ class Involvement extends PostTypeCapable implements api, updatesViaCron, hasGeo protected const MEETING_STRATEGY_MULTIPLE = 2; public const CRON_HOOK = TouchPointWP::HOOK_PREFIX . "inv_cron_hook"; - public const CRON_OFFSET = 86400 + 3600; + public const CRON_OFFSET = 86400 - 3600; protected static bool $_hasUsedMap = false; protected static bool $_hasArchiveMap = false; @@ -374,7 +374,7 @@ public final static function updateFromTouchPoint(bool $verbose = false): int $startTime = microtime(true); // Prevent other threads from attempting for an hour. - TouchPointWP::instance()->settings->set('inv_cron_last_run', time() - self::CRON_OFFSET + 3600); + TouchPointWP::instance()->settings->set('inv_cron_last_run', time() + 3600); $verbose &= TouchPointWP::currentUserIsAdmin(); @@ -1267,6 +1267,26 @@ public static function getPostTypes(): array return $r; } + + /** + * Get an array of Involvement Post Types, with some basic info + * + * @return array[] + */ + public static function getPostTypesSummary(): array + { + $r = []; + foreach (self::allTypeSettings() as $pt) { + $r[] = [ + /** @var Involvement_PostTypeSettings $pt */ + 'postType' => $pt->postTypeWithoutPrefix(), + 'nameSingular' => $pt->nameSingular, + 'namePlural' => $pt->namePlural + ]; + } + return $r; + } + /** * Display action buttons for an involvement. Takes an id parameter for the Involvement ID. If not provided, * the current post will be used. @@ -2052,6 +2072,13 @@ public static function api(array $uri): bool self::ajaxNearby(); exit; + /** @noinspection SpellCheckingInspection */ + case "posttypes": + // Return the post types that are available for involvements. + header('Content-Type: application/json'); + echo json_encode(Involvement::getPostTypesSummary()); + exit; + case "force-sync": TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE); echo self::updateFromTouchPoint(true); @@ -2294,9 +2321,6 @@ public static function load(): bool /// Syncing /// /////////////// - // Do an update if needed. - add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'checkUpdates']); - // Setup cron for updating Involvements daily. add_action(self::CRON_HOOK, [self::class, 'updateCron']); if ( ! wp_next_scheduled(self::CRON_HOOK)) { @@ -2308,6 +2332,9 @@ public static function load(): bool ); } + // Do an update if needed. + add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'checkUpdates']); + return true; } diff --git a/src/TouchPoint-WP/TouchPointWP.php b/src/TouchPoint-WP/TouchPointWP.php index 6fbe7eec..c2402273 100644 --- a/src/TouchPoint-WP/TouchPointWP.php +++ b/src/TouchPoint-WP/TouchPointWP.php @@ -7,6 +7,7 @@ use JsonException; use stdClass; +use tp\TouchPointWP\Blocks\BlocksController; use tp\TouchPointWP\Utilities\Cleanup; use tp\TouchPointWP\Utilities\Http; use tp\TouchPointWP\Utilities\Session; @@ -233,6 +234,9 @@ protected function __construct(string $file = '') // Register frontend JS & CSS. add_action('init', [$this, 'registerScriptsAndStyles'], 0); + // Register blocks + add_action('init', [BlocksController::class, 'init']); + add_action('wp_print_footer_scripts', [$this, 'printDynamicFooterScripts'], 1000); add_action('admin_print_footer_scripts', [$this, 'printDynamicFooterScripts'], 1000); diff --git a/src/TouchPoint-WP/TouchPointWP_AdminAPI.php b/src/TouchPoint-WP/TouchPointWP_AdminAPI.php index 0888657a..92b9a334 100644 --- a/src/TouchPoint-WP/TouchPointWP_AdminAPI.php +++ b/src/TouchPoint-WP/TouchPointWP_AdminAPI.php @@ -50,6 +50,12 @@ public static function api(array $uri): bool echo json_encode($mt); exit; + case "divisions": + header('Content-Type: application/json'); + $divs = TouchPointWP::instance()->getDivisions(); + echo json_encode($divs); + exit; + case self::API_ENDPOINT_SCRIPTZIP: if ( ! TouchPointWP::currentUserIsAdmin()) { return false; diff --git a/touchpoint-wp.php b/touchpoint-wp.php index 8a93d1f8..da743135 100644 --- a/touchpoint-wp.php +++ b/touchpoint-wp.php @@ -65,6 +65,8 @@ require_once __DIR__ . "/src/TouchPoint-WP/Taxonomies.php"; require_once __DIR__ . "/src/TouchPoint-WP/Interfaces/hasGeo.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Blocks/BlocksController.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Person.php"; require_once __DIR__ . "/src/TouchPoint-WP/Meeting.php"; require_once __DIR__ . "/src/TouchPoint-WP/CalendarGrid.php"; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..f556a0dc --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,28 @@ +const path = require('path'); + +module.exports = { + entry: './blocks/inv-list/index.js', + output: { + path: path.resolve(__dirname, 'build/blocks/inv-list'), + filename: 'inv-list/index.min.js', + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-env', '@babel/preset-react'], + }, + }, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + mode: 'production', +}; diff --git a/wpml-config.xml b/wpml-config.xml index 622386c7..57fe652f 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -2,6 +2,7 @@ tp_partner tp_report + tp_meeting tp_rescode @@ -47,4 +48,5 @@ + TP-Inv-Nearby,TP-Inv-List,TP-People,TP-RSVP,TP-Report \ No newline at end of file From 756efb50a5fe31c3a03c1a6f06b58b59c192f6bc Mon Sep 17 00:00:00 2001 From: "James K." Date: Mon, 9 Jun 2025 16:31:05 -0400 Subject: [PATCH 17/83] Editor UX for inv list block --- .idea/watcherTasks.xml | 40 +++++++++ assets/template/block-preview-style.css | 21 +++++ blocks/inv-list/index.js | 93 +++++++++++++++----- package.json | 2 +- src/TouchPoint-WP/Involvement.php | 94 +++++++++++++++++---- src/TouchPoint-WP/Taxonomies.php | 9 +- src/TouchPoint-WP/TouchPointWP.php | 21 +++++ src/TouchPoint-WP/TouchPointWP_AdminAPI.php | 2 +- 8 files changed, 233 insertions(+), 49 deletions(-) create mode 100644 assets/template/block-preview-style.css diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml index 49ba7008..8129e6ac 100644 --- a/.idea/watcherTasks.xml +++ b/.idea/watcherTasks.xml @@ -121,5 +121,45 @@