Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b22e82bdb1 | |||
| 40b15942bc | |||
| bee75c1bd2 | |||
| 5b9a1708f7 | |||
| ef1aec3782 | |||
| cb81b7f58c | |||
| cd862bc64b | |||
| 55b8a474e0 | |||
| 34294f98a2 | |||
| 4b116f5eee | |||
| bd5ac4b236 | |||
| c7182f6cbb | |||
| 84d42f70a1 | |||
| 9ed2fb1963 | |||
| 9b09d61473 | |||
| 42501f0597 | |||
| 7f4451628d | |||
| c8f7422996 | |||
| 8dfc1227cb | |||
| e8d67215b1 | |||
| f7bbddd98d | |||
| 1ece8a006f | |||
| 5ea2fd2b98 | |||
| b6900aec6e | |||
| ecfb7c426d | |||
| 03c9ca53a6 | |||
| 8c2bf7b02c | |||
| 056b339dee | |||
| 58f3ac96d9 | |||
| 086c50e150 | |||
| 3487072b8a | |||
| edc6bbf62c | |||
| 8b42c016a8 | |||
| 41875e7878 | |||
| 797101474a | |||
| dccdb88617 | |||
| 2897a1ceba | |||
| 926b4c7576 | |||
| 80cefe1624 | |||
| 32b541597a | |||
| a11ec9b73a | |||
| 2fe10deedf | |||
| cb37757087 | |||
| 99f738b39c | |||
| d6bde53f96 | |||
| ad459ba54b | |||
| 6f5f5913e9 | |||
| 8a231b00af | |||
| 57e106fff7 |
@@ -7,7 +7,7 @@
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +=======================================================================+
|
||||
@@ -75,6 +75,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
@@ -173,6 +174,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.20
|
||||
# VERSION: 01.02.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -20,7 +20,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
@@ -55,14 +55,14 @@ jobs:
|
||||
|
||||
- name: Validate metadata against Joomla manifest
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||
--path . \
|
||||
--token "${GITEA_TOKEN}" \
|
||||
--token "${MOKOGITEA_TOKEN}" \
|
||||
--org "${GITEA_ORG}" \
|
||||
--repo "${GITEA_REPO}" \
|
||||
--api-base "${GITEA_URL}/api/v1" \
|
||||
--api-base "${MOKOGITEA_URL}/api/v1" \
|
||||
--ci
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
|
||||
@@ -93,8 +93,20 @@ jobs:
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Check platform eligibility (Joomla only)
|
||||
id: eligibility
|
||||
run: |
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
||||
fi
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
@@ -171,6 +183,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
@@ -181,6 +194,7 @@ jobs:
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
@@ -217,6 +231,7 @@ jobs:
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
@@ -230,6 +245,7 @@ jobs:
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
@@ -47,13 +47,6 @@ jobs:
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Platform: ${PLATFORM:-all}"
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Clone mokocli
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
@@ -22,16 +22,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `LocationBridgeHelper` — static helper for cross-extension integration (#48)
|
||||
- `LocationSavedEvent` — fires `onStoreLocatorLocationSaved` for cache invalidation
|
||||
- Plugin added to package manifest
|
||||
- Leaflet map on location detail page with marker and popup (#57)
|
||||
- Leaflet.markercluster for automatic marker grouping at low zoom levels (#61)
|
||||
- Clustering toggle parameter in map module settings (enabled by default)
|
||||
- Junction table orphan cleanup on location/category delete (#60)
|
||||
- License key warning on install/update when no download key is configured
|
||||
- Download key (dlid) preserved across package upgrades
|
||||
- Pretty display names for all extensions in Joomla admin (e.g. "MokoSuite Store Locator" instead of raw element names)
|
||||
- Plugin `.sys.ini` language file for system-level name translation
|
||||
|
||||
### Changed
|
||||
- Map module dispatcher uses aliased table queries with category JOIN
|
||||
- ORDER BY clauses in admin and site models now validated against filter_fields allowlist
|
||||
- Category filter uses EXISTS subquery instead of JOIN to avoid ONLY_FULL_GROUP_BY errors (#59)
|
||||
- Inline `<script>` blocks replaced with `$wa->addInlineScript()` for CSP nonce support (#34)
|
||||
|
||||
### Removed
|
||||
- Dead `catid` column from locations table — junction table is the source of truth (#58)
|
||||
- `idx_catid` index dropped from locations table
|
||||
|
||||
### Security
|
||||
- CSV import: MIME type validation, 2 MB file size limit, delimiter allowlist (#34)
|
||||
- CSV import: formula injection prevention (strips leading `=+\-@\t\r` characters)
|
||||
- ORDER BY injection prevention — replaced `$db->escape()` with allowlist validation
|
||||
- Map module: `$mapHeight` CSS value validated with regex pattern
|
||||
- CSP compatibility: all inline scripts use WebAssetManager for automatic nonce injection (#34)
|
||||
- XSS fix: detail map popup uses DOM textContent instead of raw string in bindPopup()
|
||||
|
||||
### Fixed
|
||||
- SQL migration compatibility: removed `DROP COLUMN IF EXISTS` (MySQL 8.0.13+ only) in favor of plain `DROP COLUMN`
|
||||
|
||||
## [1.1.0] - 2026-06-23
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP:
|
||||
INGROUP: Project.Documentation
|
||||
REPO:
|
||||
VERSION: 01.00.20
|
||||
VERSION: 01.02.00
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
|
||||
+119
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
# MokoSuiteStoreLocator
|
||||
|
||||
A Joomla 4/5 package providing a store locator listing component with coordinating map and search modules.
|
||||
A Joomla 5/6 package providing a store locator listing component with coordinating map and search modules.
|
||||
|
||||
## Package Contents
|
||||
|
||||
@@ -34,7 +34,7 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
||||
- **Schema.org** — LocalBusiness structured data markup on all frontend templates
|
||||
- **SEF URLs** — router with menu, standard, and nomenu rules
|
||||
- **Menu items** — "All Locations" list and single "Location Detail" picker
|
||||
- **Interactive map** — Leaflet.js with OpenStreetMap tiles, markers with popups, auto-fit bounds
|
||||
- **Interactive map** — Leaflet.js with OpenStreetMap tiles, markers with popups, auto-fit bounds, marker clustering
|
||||
- **Location search** — city dropdown, radius filter, and browser geolocation ("Use My Location")
|
||||
- **Proximity search** — Haversine distance filtering with distance-sorted results
|
||||
- **Get Directions** — Google Maps directions link on detail page and map popups
|
||||
@@ -45,13 +45,16 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
||||
- **ACL permissions** — `access.xml` with standard Joomla permission actions
|
||||
- **SQL update schema** — versioned migration files for safe upgrades
|
||||
- **Shop integration** — `LocationBridgeHelper` for cross-extension data access, `LocationSavedEvent` for cache invalidation
|
||||
- **Security hardening** — CSV injection prevention, MIME validation, ORDER BY allowlists, input sanitization
|
||||
- **Detail page map** — Leaflet map on single location view with marker and popup
|
||||
- **Marker clustering** — Leaflet.markercluster groups nearby markers at low zoom levels (toggleable)
|
||||
- **Security hardening** — CSV injection prevention, MIME validation, ORDER BY allowlists, CSP-compatible inline scripts
|
||||
- **Junction cleanup** — orphan rows automatically removed when locations or categories are deleted
|
||||
|
||||
### Planned
|
||||
- Marker clustering for dense location areas (Leaflet.markercluster)
|
||||
- Google Maps provider as alternative to Leaflet
|
||||
- CSV export
|
||||
- Photo gallery per location
|
||||
- Address autocomplete on admin edit form
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 01.00.20
|
||||
VERSION: 01.02.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
+25
-7
@@ -1,9 +1,27 @@
|
||||
{
|
||||
"name": "mokoconsulting/mokojoomstorelocator",
|
||||
"description": "Joomla store locator listing package with component and modules",
|
||||
"type": "joomla-package",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
}
|
||||
"name": "mokoconsulting/mokojoomgallery",
|
||||
"description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display",
|
||||
"type": "joomla-package",
|
||||
"version": "01.00.00",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Moko Consulting",
|
||||
"email": "hello@mokoconsulting.tech",
|
||||
"homepage": "https://mokoconsulting.tech"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"joomla/coding-standards": "3.0.x-dev"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR="Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings."
|
||||
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR="Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR_DESC="A store locator component for managing and displaying location listings."
|
||||
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
|
||||
|
||||
@@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` (
|
||||
`image` varchar(255) NOT NULL DEFAULT '',
|
||||
`published` tinyint(4) NOT NULL DEFAULT 0,
|
||||
`ordering` int(11) NOT NULL DEFAULT 0,
|
||||
`catid` int(11) NOT NULL DEFAULT 0,
|
||||
`params` text NOT NULL,
|
||||
`created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`created_by` int(10) unsigned NOT NULL DEFAULT 0,
|
||||
@@ -34,7 +33,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` (
|
||||
`checked_out_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_published` (`published`),
|
||||
KEY `idx_catid` (`catid`),
|
||||
KEY `idx_alias` (`alias`(191)),
|
||||
KEY `idx_coordinates` (`latitude`, `longitude`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- MokoSuiteStoreLocator 01.00.02
|
||||
-- Legacy catid column removed from install.mysql.sql.
|
||||
-- No runtime migration needed — Joomla aborts on DROP errors
|
||||
-- and fresh installs never had the column.
|
||||
SELECT 1;
|
||||
@@ -15,6 +15,7 @@ use Joomla\CMS\Filter\OutputFilter;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
/**
|
||||
* Category table class.
|
||||
@@ -78,4 +79,34 @@ class CategoryTable extends Table
|
||||
|
||||
return parent::check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override delete to clean up junction table rows.
|
||||
*
|
||||
* @param mixed $pk Primary key value to delete.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public function delete($pk = null): bool
|
||||
{
|
||||
$pk = $pk ?: $this->id;
|
||||
|
||||
if (!parent::delete($pk))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitestorelocator_location_categories'))
|
||||
->where($db->quoteName('category_id') . ' = :pk')
|
||||
->bind(':pk', $pk, ParameterType::INTEGER);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Joomla\CMS\Filter\OutputFilter;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\Database\DatabaseDriver;
|
||||
use Joomla\Database\ParameterType;
|
||||
|
||||
/**
|
||||
* Location table class.
|
||||
@@ -95,4 +96,34 @@ class LocationTable extends Table
|
||||
|
||||
return parent::check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override delete to clean up junction table rows.
|
||||
*
|
||||
* @param mixed $pk Primary key value to delete.
|
||||
*
|
||||
* @return boolean True on success.
|
||||
*
|
||||
* @since 1.2.0
|
||||
*/
|
||||
public function delete($pk = null): bool
|
||||
{
|
||||
$pk = $pk ?: $this->id;
|
||||
|
||||
if (!parent::delete($pk))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = $this->getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuitestorelocator_location_categories'))
|
||||
->where($db->quoteName('location_id') . ' = :pk')
|
||||
->bind(':pk', $pk, ParameterType::INTEGER);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokosuitestorelocator</name>
|
||||
<version>01.00.20</version>
|
||||
<version>01.02.00</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
COM_MOKOSUITESTORELOCATOR="MokoSuite Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR="Store Locator"
|
||||
COM_MOKOJOOMSTORELOCATOR_LOCATIONS="Locations"
|
||||
COM_MOKOJOOMSTORELOCATOR_NO_LOCATIONS="No locations found."
|
||||
|
||||
@@ -157,14 +157,18 @@ class LocationsModel extends ListModel
|
||||
->bind(':state', $state);
|
||||
}
|
||||
|
||||
// Category filter
|
||||
// Category filter — use EXISTS to avoid GROUP BY / ONLY_FULL_GROUP_BY issues
|
||||
$catid = (int) $this->getState('filter.catid');
|
||||
|
||||
if ($catid > 0)
|
||||
{
|
||||
$query->join('INNER', $db->quoteName('#__mokosuitestorelocator_location_categories', 'lc')
|
||||
. ' ON ' . $db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
|
||||
->where($db->quoteName('lc.category_id') . ' = :catid')
|
||||
$subQuery = $db->getQuery(true)
|
||||
->select('1')
|
||||
->from($db->quoteName('#__mokosuitestorelocator_location_categories', 'lc'))
|
||||
->where($db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
|
||||
->where($db->quoteName('lc.category_id') . ' = :catid');
|
||||
|
||||
$query->where('EXISTS (' . $subQuery . ')')
|
||||
->bind(':catid', $catid, ParameterType::INTEGER);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@ use Joomla\CMS\Language\Text;
|
||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Site\View\Location\HtmlView $this */
|
||||
|
||||
$item = $this->item;
|
||||
|
||||
if ($item->latitude && $item->longitude)
|
||||
{
|
||||
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
|
||||
$wa = $this->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], ['integrity' => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=', 'crossorigin' => '']);
|
||||
$wa->registerAndUseScript('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], ['integrity' => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=', 'crossorigin' => '', 'defer' => true]);
|
||||
}
|
||||
?>
|
||||
<div class="com-mokosuitestorelocator-location" itemscope itemtype="https://schema.org/LocalBusiness">
|
||||
<h2 itemprop="name"><?php echo $this->escape($item->title); ?></h2>
|
||||
@@ -111,10 +119,29 @@ $item = $this->item;
|
||||
<meta itemprop="latitude" content="<?php echo $this->escape($item->latitude); ?>">
|
||||
<meta itemprop="longitude" content="<?php echo $this->escape($item->longitude); ?>">
|
||||
<div class="com-mokosuitestorelocator-location__map"
|
||||
data-lat="<?php echo $this->escape($item->latitude); ?>"
|
||||
data-lng="<?php echo $this->escape($item->longitude); ?>"
|
||||
id="mokosuitestorelocator-detail-map"
|
||||
data-lat="<?php echo (float) $item->latitude; ?>"
|
||||
data-lng="<?php echo (float) $item->longitude; ?>"
|
||||
data-title="<?php echo $this->escape($item->title); ?>"
|
||||
style="height: 300px;">
|
||||
</div>
|
||||
<?php
|
||||
$wa->addInlineScript(<<<JS
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var el = document.getElementById('mokosuitestorelocator-detail-map');
|
||||
if (!el || typeof L === 'undefined') return;
|
||||
var lat = parseFloat(el.getAttribute('data-lat'));
|
||||
var lng = parseFloat(el.getAttribute('data-lng'));
|
||||
var map = L.map(el.id).setView([lat, lng], 15);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
var span = document.createElement('span');
|
||||
span.textContent = el.getAttribute('data-title') || '';
|
||||
L.marker([lat, lng]).addTo(map).bindPopup(span).openPopup();
|
||||
});
|
||||
JS, [], ['position' => 'after'], ['leaflet']);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
+2
@@ -2,6 +2,7 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
MOD_MOKOSUITESTORELOCATOR_MAP="MokoSuite Store Locator - Map"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers."
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_HEIGHT="Map Height"
|
||||
@@ -9,4 +10,5 @@ MOD_MOKOJOOMSTORELOCATOR_MAP_ZOOM="Default Zoom Level"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_PROVIDER="Map Provider"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY="API Key"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC="Required for Google Maps. Not needed for OpenStreetMap."
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_CLUSTERING="Enable Marker Clustering"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_NOSCRIPT="JavaScript is required to display the map."
|
||||
|
||||
+1
@@ -2,5 +2,6 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
MOD_MOKOSUITESTORELOCATOR_MAP="MokoSuite Store Locator - Map"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP="Store Locator Map"
|
||||
MOD_MOKOJOOMSTORELOCATOR_MAP_DESC="Displays an interactive map with store location markers."
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<extension type="module" client="site" method="upgrade">
|
||||
<name>mod_mokosuitestorelocator_map</name>
|
||||
<version>01.00.20</version>
|
||||
<version>01.02.00</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -67,6 +67,17 @@
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY"
|
||||
description="MOD_MOKOJOOMSTORELOCATOR_MAP_API_KEY_DESC"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="enable_clustering"
|
||||
type="radio"
|
||||
label="MOD_MOKOJOOMSTORELOCATOR_MAP_CLUSTERING"
|
||||
default="1"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
|
||||
@@ -27,10 +27,19 @@ $apiKey = $params->get('api_key', '');
|
||||
/** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */
|
||||
$wa = $displayData['app']->getDocument()->getWebAssetManager();
|
||||
|
||||
$enableClustering = (bool) $params->get('enable_clustering', 1);
|
||||
|
||||
if ($provider === 'leaflet')
|
||||
{
|
||||
$wa->registerAndUseStyle('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css', [], ['integrity' => 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=', 'crossorigin' => '']);
|
||||
$wa->registerAndUseScript('leaflet', 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js', [], ['integrity' => 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=', 'crossorigin' => '', 'defer' => true]);
|
||||
|
||||
if ($enableClustering)
|
||||
{
|
||||
$wa->registerAndUseStyle('leaflet.markercluster', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css', [], ['crossorigin' => '']);
|
||||
$wa->registerAndUseStyle('leaflet.markercluster.default', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css', [], ['crossorigin' => '']);
|
||||
$wa->registerAndUseScript('leaflet.markercluster', 'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js', [], ['crossorigin' => '', 'defer' => true], ['leaflet']);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="mod-mokosuitestorelocator-map"
|
||||
@@ -45,9 +54,13 @@ if ($provider === 'leaflet')
|
||||
</noscript>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<?php
|
||||
$directionsText = Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true);
|
||||
$clusteringJs = $enableClustering ? 'true' : 'false';
|
||||
|
||||
$wa->addInlineScript(<<<JS
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var el = document.getElementById('mokosuitestorelocator-map-<?php echo (int) $moduleId; ?>');
|
||||
var el = document.getElementById('mokosuitestorelocator-map-{$moduleId}');
|
||||
if (!el || typeof L === 'undefined') return;
|
||||
|
||||
var locations = JSON.parse(el.getAttribute('data-locations') || '[]');
|
||||
@@ -66,6 +79,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
var bounds = L.latLngBounds();
|
||||
var useClustering = {$clusteringJs} && typeof L.markerClusterGroup === 'function';
|
||||
var markerLayer = useClustering ? L.markerClusterGroup() : L.layerGroup();
|
||||
|
||||
function esc(str) {
|
||||
var d = document.createElement('div');
|
||||
@@ -83,15 +98,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
popupAnchor: [0, -32]
|
||||
});
|
||||
}
|
||||
var marker = L.marker([loc.lat, loc.lng], markerOptions).addTo(map);
|
||||
var marker = L.marker([loc.lat, loc.lng], markerOptions);
|
||||
var popup = '<strong>' + esc(loc.title) + '</strong>';
|
||||
if (loc.address) popup += '<br>' + esc(loc.address);
|
||||
if (loc.phone) popup += '<br><a href="tel:' + esc(loc.phone) + '">' + esc(loc.phone) + '</a>';
|
||||
popup += '<br><a href="https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng + '" target="_blank" rel="noopener"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_GET_DIRECTIONS', true); ?></a>';
|
||||
popup += '<br><a href="https://www.google.com/maps/dir/?api=1&destination=' + loc.lat + ',' + loc.lng + '" target="_blank" rel="noopener">{$directionsText}</a>';
|
||||
marker.bindPopup(popup);
|
||||
markerLayer.addLayer(marker);
|
||||
bounds.extend([loc.lat, loc.lng]);
|
||||
});
|
||||
|
||||
map.addLayer(markerLayer);
|
||||
map.fitBounds(bounds, { padding: [30, 30], maxZoom: zoom });
|
||||
});
|
||||
</script>
|
||||
JS, [], ['position' => 'after'], ['leaflet']);
|
||||
?>
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search"
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search"
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations."
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_LABEL="Find a Store"
|
||||
|
||||
+1
@@ -2,5 +2,6 @@
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
MOD_MOKOSUITESTORELOCATOR_SEARCH="MokoSuite Store Locator - Search"
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH="Store Locator Search"
|
||||
MOD_MOKOJOOMSTORELOCATOR_SEARCH_DESC="Provides a search/filter form for finding store locations."
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<extension type="module" client="site" method="upgrade">
|
||||
<name>mod_mokosuitestorelocator_search</name>
|
||||
<version>01.00.20</version>
|
||||
<version>01.02.00</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuiteStoreLocator - Web Services"
|
||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services"
|
||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component."
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
; MokoSuiteStoreLocator Web Services Plugin - System language strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GNU General Public License version 3 or later; see LICENSE
|
||||
|
||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuite Store Locator - Web Services"
|
||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component."
|
||||
@@ -5,7 +5,7 @@
|
||||
========================================================================= -->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>plg_webservices_mokosuitestorelocator</name>
|
||||
<version>01.00.20</version>
|
||||
<version>01.02.00</version>
|
||||
<creationDate>2026-06-24</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
=========================================================================
|
||||
-->
|
||||
<extension type="package" method="upgrade">
|
||||
<name>pkg_mokosuitestorelocator</name>
|
||||
<name>MokoSuite Store Locator</name>
|
||||
<packagename>mokosuitestorelocator</packagename>
|
||||
<version>01.00.20</version>
|
||||
<version>01.02.00</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -69,6 +69,8 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->saveDownloadKey();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,6 +128,117 @@ class Pkg_MokosuitestorelocatorInstallerScript implements InstallerScriptInterfa
|
||||
*/
|
||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||
{
|
||||
$this->restoreDownloadKey();
|
||||
$this->warnMissingLicenseKey();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ?string $savedDownloadKey = null;
|
||||
|
||||
private function saveDownloadKey(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
||||
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator'))
|
||||
->setLimit(1)
|
||||
);
|
||||
$key = $db->loadResult();
|
||||
|
||||
if (!empty($key))
|
||||
{
|
||||
$this->savedDownloadKey = $key;
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
}
|
||||
|
||||
private function restoreDownloadKey(): void
|
||||
{
|
||||
if ($this->savedDownloadKey === null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('us.update_site_id'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
||||
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokosuitestorelocator'))
|
||||
->setLimit(1)
|
||||
);
|
||||
$siteId = (int) $db->loadResult();
|
||||
|
||||
if ($siteId > 0)
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . $siteId)
|
||||
)->execute();
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
}
|
||||
|
||||
private function warnMissingLicenseKey(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
||||
->from($db->quoteName('#__update_sites'))
|
||||
->where(
|
||||
'(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%')
|
||||
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteStoreLocator%') . ')'
|
||||
)
|
||||
->setLimit(1)
|
||||
);
|
||||
$site = $db->loadObject();
|
||||
|
||||
if ($site)
|
||||
{
|
||||
$eq = (string) ($site->extra_query ?? '');
|
||||
|
||||
if (!empty($eq) && strpos($eq, 'dlid=') !== false)
|
||||
{
|
||||
parse_str($eq, $p);
|
||||
|
||||
if (!empty($p['dlid']))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$editUrl = 'index.php?option=com_installer&view=updatesites';
|
||||
}
|
||||
|
||||
\Joomla\CMS\Factory::getApplication()->enqueueMessage(
|
||||
'<strong>Moko Consulting License Key Required</strong> — '
|
||||
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
||||
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user