diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..499c7fe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,131 @@
+name: ๐ Bug Report
+description: Report a bug or unexpected behavior
+title: "[Bug]: "
+labels: ["bug", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to report a bug! Please fill out this form to help us understand and fix the issue.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: A clear and concise description of what the bug is.
+ placeholder: The context menu doesn't appear when...
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps
+ attributes:
+ label: Steps to Reproduce
+ description: Steps to reproduce the behavior
+ placeholder: |
+ 1. Create a map with '...'
+ 2. Right-click on '...'
+ 3. See error
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ description: What you expected to happen
+ placeholder: The context menu should appear...
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual
+ attributes:
+ label: Actual Behavior
+ description: What actually happened
+ placeholder: Instead, nothing happens...
+ validations:
+ required: true
+
+ - type: input
+ id: version-contextmenu
+ attributes:
+ label: ol-contextmenu Version
+ description: Which version of ol-contextmenu are you using?
+ placeholder: "5.5.0"
+ validations:
+ required: true
+
+ - type: input
+ id: version-openlayers
+ attributes:
+ label: OpenLayers Version
+ description: Which version of OpenLayers are you using?
+ placeholder: "10.2.1"
+ validations:
+ required: true
+
+ - type: dropdown
+ id: browser
+ attributes:
+ label: Browser
+ description: Which browser are you using?
+ options:
+ - Chrome
+ - Firefox
+ - Safari
+ - Edge
+ - Other
+ validations:
+ required: true
+
+ - type: input
+ id: browser-version
+ attributes:
+ label: Browser Version
+ placeholder: "120.0"
+
+ - type: dropdown
+ id: os
+ attributes:
+ label: Operating System
+ options:
+ - Windows
+ - macOS
+ - Linux
+ - iOS
+ - Android
+ - Other
+
+ - type: textarea
+ id: reproduction
+ attributes:
+ label: Minimal Reproduction
+ description: |
+ Please provide a link to a minimal reproduction (CodeSandbox, JSFiddle, or GitHub repository).
+ This helps us identify and fix the issue faster.
+ placeholder: https://codesandbox.io/s/...
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: Console Errors
+ description: Any errors from the browser console
+ render: shell
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Any other context, screenshots, or information about the problem
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues to avoid duplicates
+ required: true
+ - label: I have provided a minimal reproduction (if applicable)
+ - label: I am using a supported version of OpenLayers (7.x - 10.x)
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..d1d5cf1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: ๐ Documentation
+ url: https://github.com/jonataswalker/ol-contextmenu#readme
+ about: Read the documentation and guides
+ - name: ๐ฌ Discussions
+ url: https://github.com/jonataswalker/ol-contextmenu/discussions
+ about: Ask questions and discuss ideas with the community
+ - name: ๐ฎ Examples
+ url: https://github.com/jonataswalker/ol-contextmenu/tree/master/examples
+ about: Check out working examples
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..f197c08
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,74 @@
+name: ๐ก Feature Request
+description: Suggest a new feature or enhancement
+title: "[Feature]: "
+labels: ["enhancement", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for suggesting a new feature! Please provide as much detail as possible.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem Statement
+ description: What problem does this feature solve?
+ placeholder: I'm always frustrated when...
+ validations:
+ required: true
+
+ - type: textarea
+ id: solution
+ attributes:
+ label: Proposed Solution
+ description: How would you like this feature to work?
+ placeholder: The context menu should...
+ validations:
+ required: true
+
+ - type: textarea
+ id: example
+ attributes:
+ label: Usage Example
+ description: Show how you would use this feature
+ placeholder: |
+ ```javascript
+ const contextmenu = new ContextMenu({
+ newFeature: true,
+ // ...
+ });
+ ```
+ render: javascript
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered
+ description: What other solutions or workarounds have you considered?
+
+ - type: dropdown
+ id: breaking
+ attributes:
+ label: Breaking Change?
+ description: Would this be a breaking change?
+ options:
+ - "No"
+ - "Yes"
+ - "Not sure"
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Additional Context
+ description: Any mockups, screenshots, or references that would help
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues/PRs for similar features
+ required: true
+ - label: I am willing to help implement this feature
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 0000000..8483a45
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,53 @@
+name: โ Question
+description: Ask a question about using ol-contextmenu
+title: "[Question]: "
+labels: ["question"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Have a question? We're here to help!
+
+ **Before asking:**
+ - Check the [documentation](https://github.com/jonataswalker/ol-contextmenu#readme)
+ - Search [existing issues](https://github.com/jonataswalker/ol-contextmenu/issues)
+ - Review the [examples](https://github.com/jonataswalker/ol-contextmenu/tree/master/examples)
+
+ - type: textarea
+ id: question
+ attributes:
+ label: Your Question
+ description: What would you like to know?
+ placeholder: How do I...?
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: What You've Tried
+ description: What have you already attempted?
+ placeholder: |
+ I tried...
+ I looked at...
+
+ - type: textarea
+ id: code
+ attributes:
+ label: Code Example
+ description: Share relevant code (if applicable)
+ render: javascript
+
+ - type: input
+ id: version
+ attributes:
+ label: ol-contextmenu Version
+ placeholder: "5.5.0"
+
+ - type: checkboxes
+ id: checklist
+ attributes:
+ label: Checklist
+ options:
+ - label: I have read the documentation
+ - label: I have searched existing issues
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..25ccb2b
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,85 @@
+# Description
+
+
+
+## Type of Change
+
+
+
+- [ ] ๐ Bug fix (non-breaking change that fixes an issue)
+- [ ] โจ New feature (non-breaking change that adds functionality)
+- [ ] ๐ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] ๐ Documentation update
+- [ ] ๐จ Code style update (formatting, renaming)
+- [ ] โป๏ธ Code refactoring (no functional changes)
+- [ ] โก Performance improvement
+- [ ] โ Test update
+- [ ] ๐ง Build configuration change
+- [ ] ๐จ Other (please describe)
+
+## Related Issues
+
+
+
+Fixes #
+Closes #
+Related to #
+
+## Changes Made
+
+
+
+-
+-
+-
+
+## Testing
+
+
+
+- [ ] Tested locally
+- [ ] Added/updated unit tests
+- [ ] Added/updated browser tests
+- [ ] All tests pass (`npm test`)
+- [ ] Linter passes (`npm run lint`)
+
+### Test Environment
+
+- **ol-contextmenu version:**
+- **OpenLayers version:**
+- **Node version:**
+- **Browser(s):**
+
+## Screenshots
+
+
+
+## Breaking Changes
+
+
+
+- [ ] No breaking changes
+- [ ] Breaking changes (described below)
+
+
+
+## Checklist
+
+
+
+- [ ] My code follows the project's code style
+- [ ] I have performed a self-review of my code
+- [ ] I have commented my code, particularly in hard-to-understand areas
+- [ ] I have made corresponding changes to the documentation
+- [ ] My changes generate no new warnings
+- [ ] I have added tests that prove my fix is effective or that my feature works
+- [ ] New and existing unit tests pass locally with my changes
+- [ ] Any dependent changes have been merged and published
+
+## Additional Notes
+
+
diff --git a/.gitignore b/.gitignore
index c1c01b1..4c09ced 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ dist
.eslintcache
coverage
.idea
+.claude
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..62d7d1b
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Project maintainers are responsible for clarifying and enforcing our standards
+of acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the project maintainers responsible for enforcement at
+[jonataswalker@gmail.com](mailto:jonataswalker@gmail.com).
+
+All complaints will be reviewed and investigated promptly and fairly.
+
+All project maintainers are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Project maintainers will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from project maintainers, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ce85764
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,328 @@
+# Contributing to ol-contextmenu
+
+First off, thank you for considering contributing to ol-contextmenu! ๐
+
+We love contributions from the community. Whether it's a bug report, feature request, documentation improvement, or code contribution, every contribution is valuable.
+
+## Table of Contents
+
+- [Code of Conduct](#code-of-conduct)
+- [How Can I Contribute?](#how-can-i-contribute)
+- [Development Setup](#development-setup)
+- [Pull Request Process](#pull-request-process)
+- [Coding Guidelines](#coding-guidelines)
+- [Testing](#testing)
+- [Documentation](#documentation)
+
+## Code of Conduct
+
+This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers.
+
+## How Can I Contribute?
+
+### Reporting Bugs
+
+Before creating bug reports, please check existing issues to avoid duplicates.
+
+**When submitting a bug report, include:**
+
+- **Clear title and description**
+- **Steps to reproduce** the issue
+- **Expected behavior** vs **actual behavior**
+- **Screenshots** if applicable
+- **Environment details:**
+ - ol-contextmenu version
+ - OpenLayers version
+ - Browser and version
+ - Operating system
+- **Minimal reproduction example** (CodeSandbox, JSFiddle, or repository)
+
+### Suggesting Enhancements
+
+Enhancement suggestions are welcome! Please provide:
+
+- **Clear use case** - Why is this needed?
+- **Detailed description** - What should it do?
+- **Examples** - How would it be used?
+- **Alternatives considered** - What other solutions did you consider?
+
+### Pull Requests
+
+We actively welcome pull requests!
+
+## Development Setup
+
+### Prerequisites
+
+- **Node.js**: 16.x or higher
+- **npm**: 8.x or higher
+- **Git**
+
+### Fork and Clone
+
+1. Fork the repository on GitHub
+2. Clone your fork locally:
+
+```bash
+git clone https://github.com/YOUR_USERNAME/ol-contextmenu.git
+cd ol-contextmenu
+```
+
+3. Add upstream remote:
+
+```bash
+git remote add upstream https://github.com/jonataswalker/ol-contextmenu.git
+```
+
+### Install Dependencies
+
+```bash
+npm install
+```
+
+### Development Commands
+
+```bash
+# Start development server
+npm run dev
+
+# Run tests
+npm test
+
+# Run tests in watch mode
+npm run test:ui
+
+# Run linter
+npm run lint
+
+# Fix linting issues
+npm run lint:fix
+
+# Build for production
+npm run build
+```
+
+### Project Structure
+
+```
+ol-contextmenu/
+โโโ src/
+โ โโโ main.ts # Main entry point
+โ โโโ constants.ts # Constants and defaults
+โ โโโ types.ts # TypeScript type definitions
+โ โโโ helpers/ # Helper functions
+โ โโโ sass/ # Styles
+โโโ tests/ # Test files
+โ โโโ *.unit.spec.ts # Unit tests
+โ โโโ *.browser.spec.ts # Browser tests
+โโโ examples/ # Example implementations
+โโโ docs/ # Documentation
+โโโ dist/ # Build output (generated)
+```
+
+## Pull Request Process
+
+### 1. Create a Feature Branch
+
+```bash
+git checkout -b feature/my-new-feature
+# or
+git checkout -b fix/issue-description
+```
+
+### 2. Make Your Changes
+
+- Write clean, readable code
+- Follow the [coding guidelines](#coding-guidelines)
+- Add tests for new functionality
+- Update documentation as needed
+
+### 3. Test Your Changes
+
+```bash
+# Run all tests
+npm test
+
+# Run linter
+npm run lint
+
+# Build to ensure no errors
+npm run build
+```
+
+**All tests must pass before submitting a PR.**
+
+### 4. Commit Your Changes
+
+Write clear, descriptive commit messages:
+
+```bash
+git commit -m "feat: add support for custom menu positioning
+
+- Add new positioning API
+- Update documentation
+- Add tests for new feature
+
+Fixes #123"
+```
+
+**Commit message format:**
+- `feat:` - New feature
+- `fix:` - Bug fix
+- `docs:` - Documentation changes
+- `test:` - Adding or updating tests
+- `refactor:` - Code refactoring
+- `style:` - Code style changes (formatting)
+- `chore:` - Maintenance tasks
+
+### 5. Push to Your Fork
+
+```bash
+git push origin feature/my-new-feature
+```
+
+### 6. Create Pull Request
+
+1. Go to the [original repository](https://github.com/jonataswalker/ol-contextmenu)
+2. Click "New Pull Request"
+3. Select your fork and branch
+4. Fill out the PR template:
+ - Describe your changes
+ - Reference related issues
+ - Add screenshots if applicable
+ - Check all boxes in the PR checklist
+
+### 7. Code Review
+
+- Maintainers will review your PR
+- Address any requested changes
+- Keep the PR up to date with the main branch
+
+### 8. Merge
+
+Once approved, a maintainer will merge your PR. Congratulations! ๐
+
+## Coding Guidelines
+
+### TypeScript
+
+- **Use TypeScript** for all new code
+- **Export types** for public APIs
+- **Avoid `any`** - use proper types
+- **Document complex types** with comments
+
+```typescript
+// โ Good
+interface MenuOptions {
+ /** Width of the menu in pixels */
+ width?: number;
+ /** Default menu items */
+ items?: Item[];
+}
+
+// โ Avoid
+function doSomething(data: any) { ... }
+```
+
+### Code Style
+
+We use ESLint for code style. Run `npm run lint:fix` to auto-fix issues.
+
+**Key points:**
+- **Indentation**: 4 spaces
+- **Semicolons**: Required
+- **Quotes**: Single quotes for strings
+- **Line length**: 100 characters max
+- **Naming**:
+ - `camelCase` for variables and functions
+ - `PascalCase` for classes and types
+ - `UPPER_CASE` for constants
+
+### Best Practices
+
+- **Keep functions small** and focused
+- **Avoid side effects** where possible
+- **Use descriptive names** for variables and functions
+- **Comment complex logic** but avoid obvious comments
+- **Handle errors gracefully**
+- **Validate inputs** for public APIs
+
+## Testing
+
+We use Vitest for testing with both unit and browser tests.
+
+### Writing Tests
+
+1. **Place tests** in the `tests/` directory
+2. **Name test files**: `*.spec.ts` or `*.test.ts`
+3. **Use descriptive test names**
+
+```typescript
+describe('ContextMenu', () => {
+ it('should open menu on right-click', async () => {
+ // Arrange
+ const contextmenu = new ContextMenu();
+ map.addControl(contextmenu);
+
+ // Act
+ dispatchContextMenu(viewport);
+ await new Promise(resolve => setTimeout(resolve, 150));
+
+ // Assert
+ expect(contextmenu.isOpen()).toBe(true);
+ });
+});
+```
+
+### Test Coverage
+
+- **Aim for high coverage** (>90%)
+- **Test edge cases** and error conditions
+- **Test TypeScript types** where applicable
+
+### Running Specific Tests
+
+```bash
+# Run specific file
+npx vitest run tests/instance.unit.spec.ts
+
+# Run tests matching pattern
+npx vitest run --grep "should open menu"
+```
+
+## Documentation
+
+### Updating Docs
+
+When adding features or changing behavior:
+
+1. **Update README.md** if it affects basic usage
+2. **Update API docs** in `docs/api-reference.md`
+3. **Add examples** to `docs/examples.md`
+4. **Update TypeScript docs** if types changed
+5. **Add to CHANGELOG.md**
+
+### Writing Documentation
+
+- **Be clear and concise**
+- **Include code examples**
+- **Explain the "why" not just the "how"**
+- **Use proper markdown formatting**
+- **Keep it up to date**
+
+## Getting Help
+
+Need help with your contribution?
+
+- ๐ฌ **Ask questions** in your PR or issue
+- ๐ **Read the docs** in the `/docs` directory
+- ๐ **Search existing issues** and PRs
+- ๐ฅ **Reach out to maintainers**
+
+## Recognition
+
+Contributors are recognized in:
+- [Contributors Graph](https://github.com/jonataswalker/ol-contextmenu/graphs/contributors)
+- Release notes (for significant contributions)
+
+Thank you for contributing to ol-contextmenu! โค๏ธ
diff --git a/README.md b/README.md
index c0a6d5d..8a164f2 100644
--- a/README.md
+++ b/README.md
@@ -1,251 +1,223 @@
-# OpenLayers Custom Context Menu
+# ๐บ๏ธ OpenLayers Context Menu
+
+> A customizable, feature-rich context menu extension for OpenLayers maps
-A `contextmenu` extension for [OpenLayers](http://openlayers.org/). **Requires** OpenLayers **v7.0.0** or higher.
-
-
+
+
+
-## Demo
+**Quick Links:** [Demo](#-demo) โข [Installation](#-installation) โข [Quick Start](#-quick-start) โข [API](#-api) โข [TypeScript](#-typescript) โข [Examples](#-examples)
-[JSFiddle](https://jsfiddle.net/jonataswalker/ooxs1w5d/)
-[CodeSandbox](https://codesandbox.io/s/openlayers-custom-context-menu-5s99kb?file=/src/index.js)
+---
-## How to use it?
+## โจ Features
-##### NPM
+- ๐ฏ **Easy Integration** - Works seamlessly with OpenLayers 7.x - 10.x
+- ๐จ **Fully Customizable** - Control width, icons, styles, and appearance
+- ๐ **Nested Submenus** - Unlimited nesting levels for complex menu structures
+- ๐ฆ **Zero CSS Dependencies** - Styles are bundled inline (since v6.0)
+- ๐ช **Event-Driven** - React to `beforeopen`, `open`, and `close` events
+- ๐ **Multiple Trigger Types** - Context menu, click, or double-click
+- ๐ฑ **Viewport-Aware** - Automatically repositions to stay visible at screen edges
+- ๐ช **TypeScript Support** - Full type definitions included out of the box
+- โก **Lightweight** - Minimal footprint with only one dependency (`tiny-emitter`)
+- โ **Well Tested** - 191 tests covering all functionality
-`npm install ol-contextmenu`
+## ๐ช Demo
-##### CDN Hosted - [jsDelivr](https://www.jsdelivr.com/package/npm/ol-contextmenu)
+**Try it live:**
+- ๐ฎ [CodeSandbox](https://codesandbox.io/s/openlayers-custom-context-menu-5s99kb?file=/src/index.js) - Interactive demo with full source
+- ๐ [JSFiddle](https://jsfiddle.net/jonataswalker/ooxs1w5d/) - Quick playground
+- ๐ป [Local Examples](./examples) - Webpack, Vite, and CDN examples
-Load Javascript (CSS is bundled):
+## ๐ฆ Installation
-```HTML
-
+**npm (Recommended):**
+```bash
+npm install ol ol-contextmenu
```
-##### CDN Hosted - UNPKG
-
-Load Javascript (CSS is bundled):
-
-```HTML
-
+**CDN:**
+```html
+
```
-##### Self hosted
+**Requires:** OpenLayers 7.0.0 or higher
-Download [latest release](https://github.com/jonataswalker/ol-contextmenu/releases/latest) and load Javascript (CSS is bundled).
+๐ See [Getting Started Guide](docs/getting-started.md) for detailed installation instructions and setup.
-##### Instantiate with some options and add the Control
+## ๐ Quick Start
```javascript
+import ContextMenu from 'ol-contextmenu';
+
const contextmenu = new ContextMenu({
width: 170,
- defaultItems: true, // defaultItems are (for now) Zoom In/Zoom Out
+ defaultItems: true, // Includes Zoom In/Zoom Out
items: [
{
text: 'Center map here',
- classname: 'some-style-class', // add some CSS rules
- callback: center, // `center` is your callback function
+ callback: (obj, map) => {
+ map.getView().setCenter(obj.coordinate);
+ },
},
{
text: 'Add a Marker',
- classname: 'some-style-class', // you can add this icon with a CSS class
- // instead of `icon` property (see next line)
- icon: 'img/marker.png', // this can be relative or absolute
- callback: marker,
+ icon: 'img/marker.png',
+ callback: addMarker,
},
- '-', // this is a separator
+ '-', // Separator
],
});
+
map.addControl(contextmenu);
```
-##### You can add a (nested) submenu like this:
-
-If you provide `items {Array}` a submenu will be created as a child of the current item.
+๐ See [Getting Started Guide](docs/getting-started.md) for complete setup instructions and [Examples](docs/examples.md) for advanced patterns.
-```javascript
-const all_items = [
- {
- text: 'Some Actions',
- items: [
- // <== this is a submenu
- {
- text: 'Action 1',
- callback: action,
- },
- {
- text: 'Other action',
- callback: action2,
- },
- ],
- },
- {
- text: 'Add a Marker',
- icon: 'img/marker.png',
- callback: marker,
- },
- '-', // this is a separator
-];
-```
+## ๐ Documentation
-##### Would you like to propagate custom data to the callback handler?
+- **[Getting Started](docs/getting-started.md)** - Installation and basic setup
+- **[API Reference](docs/api-reference.md)** - Complete API documentation
+- **[TypeScript Guide](docs/typescript.md)** - TypeScript usage and type definitions
+- **[Examples & Recipes](docs/examples.md)** - Common patterns and code examples
+- **[Troubleshooting](docs/troubleshooting.md)** - Common issues and solutions
-```javascript
-const removeMarker = function (obj) {
- vectorLayer.getSource().removeFeature(obj.data.marker);
-};
-const removeMarkerItem = {
- text: 'Remove this Marker',
- icon: 'img/marker.png',
- callback: removeMarker,
-};
-
-let restore = false;
-contextmenu.on('open', function (evt) {
- const feature = map.forEachFeatureAtPixel(evt.pixel, function (ft, l) {
- return ft;
- });
- if (feature) {
- contextmenu.clear();
- removeMarkerItem.data = { marker: feature };
- contextmenu.push(removeMarkerItem);
- restore = true;
- } else if (restore) {
- contextmenu.clear();
- contextmenu.extend(contextmenu_items);
- contextmenu.extend(contextmenu.getDefaultItems());
- restore = false;
- }
-});
-```
+## ๐ TypeScript
-# API
+Full TypeScript support with comprehensive type definitions included:
-## Constructor
+```typescript
+import ContextMenu, { type Item } from 'ol-contextmenu';
-#### `new ContextMenu(options)`
+const items: Item[] = [
+ {
+ text: 'Center map here',
+ callback: (obj, map) => {
+ map.getView().setCenter(obj.coordinate);
+ },
+ },
+];
-###### `options` is an object with the following possible properties:
+const contextmenu = new ContextMenu({ width: 170, items });
+```
-- `eventType`: `contextmenu`; The listening event type (You could use `'click'`, `'dblclick'`)
-- `defaultItems`: `true`; Whether the default items (which are: Zoom In/Out) are enabled
-- `width`: `150`; The menu's width
-- `items`: `[]`; An array of object|string
+๐ See the [TypeScript Guide](docs/typescript.md) for complete type definitions and usage patterns.
-## Methods
+## ๐ฏ API
-#### contextmenu.clear()
+### Constructor
-Remove all elements from the menu.
+```javascript
+new ContextMenu(options)
+```
-#### contextmenu.closeMenu()
+**Options:**
+- `width` (number, default: `150`) - Menu width in pixels
+- `defaultItems` (boolean, default: `true`) - Include default Zoom In/Out items
+- `items` (Item[], default: `[]`) - Array of menu items
+- `eventType` (string, default: `'contextmenu'`) - Event type to trigger menu
-Close the menu programmatically.
+### Methods
-#### contextmenu.extend(arr)
+- `clear()` - Remove all items
+- `close()` - Close menu programmatically
+- `extend(items)` - Add multiple items
+- `push(item)` - Add single item
+- `pop()` / `shift()` - Remove items
+- `getDefaultItems()` - Get default items array
+- `isOpen()` - Check if menu is open
+- `enable()` / `disable()` - Control menu state
-`@param {Array} arr`
+### Events
-Add items to the menu. This pushes each item in the provided array to the end of the menu.
+- `beforeopen` - Fired before menu opens
+- `open` - Fired when menu opens
+- `close` - Fired when menu closes
-Example:
+๐ See [API Reference](docs/api-reference.md) for complete documentation with examples.
-```js
-const contextmenu = new ContextMenu();
-map.addControl(contextmenu);
+## ๐จ Examples
-const add_later = [
- '-', // this is a separator
- {
- text: 'Add a Marker',
- icon: 'img/marker.png',
- callback: marker,
- },
-];
-contextmenu.extend(add_later);
-```
+- **[Examples & Recipes](docs/examples.md)** - Common patterns and code examples
+- **[Local Examples](./examples)** - Complete working projects:
+ - [CDN Example](./examples/contextmenu.html) - No build tools required
+ - [Webpack Example](./examples/my-project-with-webpack) - Integration with Webpack
+ - [Vite Example](./examples/my-project-with-vite) - Modern setup with Vite + TypeScript
-#### contextmenu.push(item)
+## ๐ง Troubleshooting
-`@param {Object|String} item`
+Common issues:
+- **Menu doesn't appear:** Ensure `map.addControl(contextmenu)` is called
+- **Menu cut off:** Update to v5.5.0+ for automatic viewport positioning
+- **TypeScript errors:** Use `import ContextMenu, { type Item } from 'ol-contextmenu'`
-Insert the provided item at the end of the menu.
+๐ See the [Troubleshooting Guide](docs/troubleshooting.md) for detailed solutions and common issues.
-#### contextmenu.shift()
+## ๐ Browser Support
-Remove the first item of the menu.
+Modern browsers supporting ES6+:
+- Chrome/Edge (latest)
+- Firefox (latest)
+- Safari (latest)
-#### contextmenu.pop()
+## ๐ค Contributing
-Remove the last item of the menu.
+Contributions are welcome! We appreciate:
+- ๐ Bug reports
+- ๐ก Feature requests
+- ๐ Documentation improvements
+- โจ Code contributions
-#### contextmenu.getDefaultItems()
+Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a PR.
-Get an array of default items.
+### Development Setup
-#### contextmenu.isOpen()
+```bash
+# Clone the repository
+git clone https://github.com/jonataswalker/ol-contextmenu.git
+cd ol-contextmenu
-Whether the menu is open.
+# Install dependencies
+npm install
-#### contextmenu.updatePosition(pixel)
+# Run tests
+npm test
-`@param {Array} pixel`
+# Start development server
+npm run dev
-Update menu's position.
+# Build for production
+npm run build
+```
-## Events
+## ๐ License
-#### If you want to disable this plugin under certain circumstances, listen to `beforeopen`
+MIT ยฉ [Jonatas Walker](https://github.com/jonataswalker)
-```javascript
-contextmenu.on('beforeopen', function (evt) {
- const feature = map.forEachFeatureAtPixel(evt.pixel, function (ft, l) {
- return ft;
- });
-
- if (feature) {
- // open only on features
- contextmenu.enable();
- } else {
- contextmenu.disable();
- }
-});
-```
+## ๐ Acknowledgments
-#### Listen and make some changes when context menu opens
+Built with โค๏ธ for the [OpenLayers](https://openlayers.org/) community.
-```javascript
-contextmenu.on('open', function (evt) {
- const feature = map.forEachFeatureAtPixel(evt.pixel, function (ft, l) {
- return ft;
- });
-
- if (feature) {
- // add some other items to the menu
- }
-});
-```
+Special thanks to all [contributors](https://github.com/jonataswalker/ol-contextmenu/graphs/contributors) who have helped improve this project.
-#### Any action when context menu gets closed?
+---
-```javascript
-contextmenu.on('close', function (evt) {
- // it's upon you
-});
-```
+
+ Made with โค๏ธ by the community
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..dbf1759
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,96 @@
+# Security Policy
+
+## What to Include in Your Report
+
+Help us understand the issue by including:
+
+1. **Type of vulnerability** (XSS, injection, etc.)
+2. **Affected version(s)**
+3. **Step-by-step instructions** to reproduce
+4. **Proof of concept** (if applicable)
+6. **Suggested fix** (if you have one)
+
+## Security Best Practices
+
+When using ol-contextmenu:
+
+### Input Validation
+
+Always validate and sanitize user input before displaying in menu items:
+
+```javascript
+// โ Unsafe - XSS vulnerability
+{
+ text: userInput, // Could contain malicious HTML
+ callback: fn
+}
+
+// โ Safe - Sanitized
+{
+ text: sanitizeHTML(userInput),
+ callback: fn
+}
+```
+
+### Custom Callbacks
+
+Be cautious with callbacks that execute user-controlled code:
+
+```javascript
+// โ Unsafe - eval is dangerous
+{
+ text: 'Execute',
+ callback: (obj) => {
+ eval(obj.data.code); // Never do this!
+ }
+}
+
+// โ Safe - Validated actions
+{
+ text: 'Execute',
+ callback: (obj) => {
+ if (allowedActions.includes(obj.data.action)) {
+ performAction(obj.data.action);
+ }
+ }
+}
+```
+
+### Icon URLs
+
+Validate icon URLs to prevent XSS:
+
+```javascript
+// โ Unsafe
+{
+ text: 'Item',
+ icon: userProvidedURL // Could be javascript: or data: URL
+}
+
+// โ Safe
+{
+ text: 'Item',
+ icon: isValidImageURL(userProvidedURL) ? userProvidedURL : defaultIcon
+}
+```
+
+### Content Security Policy
+
+Consider using CSP headers to mitigate XSS risks:
+
+```html
+
+```
+
+## Known Security Considerations
+
+### DOM-based XSS
+
+ol-contextmenu renders user-provided text and icons in the DOM. Always sanitize:
+
+- Menu item text
+- Icon URLs
+- Custom CSS classes
+- Data passed to callbacks
+
diff --git a/docs/api-reference.md b/docs/api-reference.md
new file mode 100644
index 0000000..4dbff3d
--- /dev/null
+++ b/docs/api-reference.md
@@ -0,0 +1,479 @@
+# API Reference
+
+Complete API documentation for `ol-contextmenu`.
+
+## Constructor
+
+### `new ContextMenu(options)`
+
+Creates a new context menu instance.
+
+**Parameters:**
+
+| Name | Type | Default | Description |
+|------|------|---------|-------------|
+| `options` | `Partial` | `{}` | Configuration options |
+| `options.width` | `number` | `150` | Width of the menu in pixels |
+| `options.defaultItems` | `boolean` | `true` | Include default zoom items |
+| `options.items` | `Item[]` | `[]` | Array of custom menu items |
+| `options.eventType` | `EventTypes` | `'contextmenu'` | Event that triggers the menu |
+| `options.scrollAt` | `number` | `4` | Number of items before menu becomes scrollable |
+
+**Example:**
+
+```javascript
+const contextmenu = new ContextMenu({
+ width: 200,
+ defaultItems: false,
+ eventType: 'click',
+ items: [
+ { text: 'Item 1', callback: fn1 },
+ { text: 'Item 2', callback: fn2 },
+ ],
+});
+```
+
+## Methods
+
+### `clear()`
+
+Removes all items from the menu.
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.clear();
+```
+
+---
+
+### `closeMenu()`
+
+Closes the context menu programmatically.
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.closeMenu();
+```
+
+---
+
+### `extend(items)`
+
+Adds multiple items to the end of the menu.
+
+**Parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `items` | `Item[]` | Array of items to add |
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.extend([
+ { text: 'New Item 1', callback: fn1 },
+ '-',
+ { text: 'New Item 2', callback: fn2 },
+]);
+```
+
+---
+
+### `push(item)`
+
+Adds a single item to the end of the menu.
+
+**Parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `item` | `Item \| '-'` | Item to add (or separator) |
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.push({ text: 'New Item', callback: fn });
+contextmenu.push('-'); // Add separator
+```
+
+---
+
+### `pop()`
+
+Removes the last item from the menu.
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.pop();
+```
+
+---
+
+### `shift()`
+
+Removes the first item from the menu.
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.shift();
+```
+
+---
+
+### `getDefaultItems()`
+
+Returns the array of default menu items (Zoom In/Zoom Out).
+
+**Returns:** `SingleItem[]`
+
+**Example:**
+
+```javascript
+const defaults = contextmenu.getDefaultItems();
+console.log(defaults);
+// [
+// { text: 'Zoom In', callback: fn, ... },
+// { text: 'Zoom Out', callback: fn, ... }
+// ]
+```
+
+---
+
+### `isOpen()`
+
+Checks whether the menu is currently open.
+
+**Returns:** `boolean`
+
+**Example:**
+
+```javascript
+if (contextmenu.isOpen()) {
+ console.log('Menu is open');
+}
+```
+
+---
+
+### `updatePosition(pixel)`
+
+Updates the position of the menu.
+
+**Parameters:**
+
+| Name | Type | Description |
+|------|------|-------------|
+| `pixel` | `[number, number]` | New pixel coordinates |
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.updatePosition([100, 200]);
+```
+
+---
+
+### `enable()`
+
+Enables the context menu (allows it to open).
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.enable();
+```
+
+---
+
+### `disable()`
+
+Disables the context menu (prevents it from opening).
+
+**Returns:** `void`
+
+**Example:**
+
+```javascript
+contextmenu.disable();
+```
+
+---
+
+### `countItems()`
+
+Returns the number of items in the menu (excluding separators and submenu items).
+
+**Returns:** `number`
+
+**Example:**
+
+```javascript
+const count = contextmenu.countItems();
+console.log(`Menu has ${count} items`);
+```
+
+## Events
+
+The context menu extends OpenLayers' `Control` class and emits custom events.
+
+### `beforeopen`
+
+Fired before the menu opens. Can be used to conditionally prevent opening.
+
+**Event Data:**
+
+```typescript
+{
+ map: Map,
+ pixel: [number, number],
+ coordinate: Coordinate,
+ originalEvent: MouseEvent,
+ type: 'beforeopen'
+}
+```
+
+**Example:**
+
+```javascript
+contextmenu.on('beforeopen', (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ if (!feature) {
+ contextmenu.disable(); // Don't open if no feature
+ } else {
+ contextmenu.enable();
+ }
+});
+```
+
+---
+
+### `open`
+
+Fired when the menu opens.
+
+**Event Data:**
+
+```typescript
+{
+ map: Map,
+ pixel: [number, number],
+ coordinate: Coordinate,
+ originalEvent: MouseEvent,
+ type: 'open'
+}
+```
+
+**Example:**
+
+```javascript
+contextmenu.on('open', (evt) => {
+ console.log('Menu opened at:', evt.coordinate);
+
+ // Modify menu based on context
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+ if (feature) {
+ // Add feature-specific items
+ }
+});
+```
+
+---
+
+### `close`
+
+Fired when the menu closes.
+
+**Event Data:** `BaseEvent`
+
+**Example:**
+
+```javascript
+contextmenu.on('close', () => {
+ console.log('Menu closed');
+});
+```
+
+## Types
+
+### `Item`
+
+Union type for menu items:
+
+```typescript
+type Item = SingleItem | ItemWithNested | ItemSeparator;
+```
+
+### `SingleItem`
+
+A standard menu item with a callback:
+
+```typescript
+interface SingleItem {
+ text: string;
+ callback: (obj: CallbackObject, map: Map) => void;
+ icon?: string;
+ classname?: string;
+ data?: any;
+}
+```
+
+### `ItemWithNested`
+
+A menu item with nested subitems:
+
+```typescript
+interface ItemWithNested {
+ text: string;
+ items: Item[];
+ icon?: string;
+ classname?: string;
+}
+```
+
+### `ItemSeparator`
+
+A menu separator:
+
+```typescript
+type ItemSeparator = '-';
+```
+
+### `CallbackObject`
+
+Data passed to item callbacks:
+
+```typescript
+interface CallbackObject {
+ coordinate: Coordinate;
+ data: unknown;
+}
+```
+
+### `Options`
+
+Configuration options for the constructor:
+
+```typescript
+interface Options {
+ width: number;
+ defaultItems: boolean;
+ items: Item[];
+ eventType: EventTypes;
+ scrollAt: number;
+}
+```
+
+**Note:** The constructor accepts `Partial`, so all fields are optional when creating an instance. The type definition above shows the complete options object with defaults applied.
+
+### `EventTypes`
+
+Possible event types:
+
+```typescript
+enum EventTypes {
+ CONTEXTMENU = 'contextmenu',
+ CLICK = 'click',
+ DBLCLICK = 'dblclick',
+}
+```
+
+## Examples
+
+### Dynamic Menu Based on Feature
+
+```javascript
+let currentFeature = null;
+
+contextmenu.on('beforeopen', (evt) => {
+ currentFeature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ contextmenu.clear();
+
+ if (currentFeature) {
+ contextmenu.extend([
+ {
+ text: 'Delete Feature',
+ callback: () => {
+ vectorSource.removeFeature(currentFeature);
+ },
+ },
+ {
+ text: 'Feature Properties',
+ callback: () => {
+ console.log(currentFeature.getProperties());
+ },
+ },
+ ]);
+ } else {
+ contextmenu.extend([
+ {
+ text: 'Add Marker Here',
+ callback: (obj) => {
+ const marker = new Feature({
+ geometry: new Point(obj.coordinate),
+ });
+ vectorSource.addFeature(marker);
+ },
+ },
+ ]);
+ }
+});
+```
+
+### Using Different Event Types
+
+```javascript
+// Trigger on single click instead of right-click
+const contextmenu = new ContextMenu({
+ eventType: 'click',
+ items: [/* ... */],
+});
+
+// Trigger on double-click
+const contextmenu2 = new ContextMenu({
+ eventType: 'dblclick',
+ items: [/* ... */],
+});
+```
+
+### Custom Data in Callbacks
+
+```javascript
+const items = [
+ {
+ text: 'Process Data',
+ data: { id: 123, type: 'custom' },
+ callback: (obj) => {
+ console.log('Custom data:', obj.data);
+ // Output: { id: 123, type: 'custom' }
+ },
+ },
+];
+```
+
+## See Also
+
+- [Getting Started](./getting-started.md)
+- [TypeScript Guide](./typescript.md)
+- [Examples & Recipes](./examples.md)
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000..39a8204
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1,465 @@
+# Examples & Recipes
+
+Common patterns and code examples for `ol-contextmenu`.
+
+## Table of Contents
+
+- [Basic Examples](#basic-examples)
+- [Dynamic Menus](#dynamic-menus)
+- [Feature Detection](#feature-detection)
+- [Custom Styling](#custom-styling)
+- [Advanced Patterns](#advanced-patterns)
+
+## Basic Examples
+
+### Simple Menu
+
+```javascript
+import ContextMenu from 'ol-contextmenu';
+
+const contextmenu = new ContextMenu({
+ width: 170,
+ items: [
+ {
+ text: 'Center map here',
+ callback: (obj, map) => {
+ map.getView().animate({
+ center: obj.coordinate,
+ duration: 700,
+ });
+ },
+ },
+ ],
+});
+
+map.addControl(contextmenu);
+```
+
+### Menu with Separators
+
+```javascript
+const items = [
+ { text: 'Zoom In', callback: zoomIn },
+ { text: 'Zoom Out', callback: zoomOut },
+ '-', // Separator
+ { text: 'Center Map', callback: center },
+ '-',
+ { text: 'Export View', callback: exportView },
+];
+
+const contextmenu = new ContextMenu({ items });
+```
+
+### Menu with Icons
+
+```javascript
+const items = [
+ {
+ text: 'Add Marker',
+ icon: 'data:image/svg+xml;base64,...', // SVG data URI
+ callback: addMarker,
+ },
+ {
+ text: 'Delete',
+ icon: './icons/trash.png', // Relative path
+ callback: deleteFeature,
+ },
+];
+```
+
+### Multi-Level Submenus
+
+```javascript
+const items = [
+ {
+ text: 'Measure',
+ items: [
+ {
+ text: 'Distance',
+ callback: measureDistance,
+ },
+ {
+ text: 'Area',
+ callback: measureArea,
+ },
+ ],
+ },
+ {
+ text: 'Export',
+ items: [
+ {
+ text: 'PNG',
+ callback: exportPNG,
+ },
+ {
+ text: 'JPEG',
+ callback: exportJPEG,
+ },
+ {
+ text: 'PDF',
+ callback: exportPDF,
+ },
+ ],
+ },
+];
+```
+
+## Dynamic Menus
+
+### Context-Aware Menu
+
+Change menu items based on what's clicked:
+
+```javascript
+let currentFeature = null;
+
+contextmenu.on('beforeopen', (evt) => {
+ currentFeature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ contextmenu.clear();
+
+ if (currentFeature) {
+ // Menu for features
+ contextmenu.extend([
+ {
+ text: 'Delete Feature',
+ callback: () => vectorSource.removeFeature(currentFeature),
+ },
+ {
+ text: 'Edit Properties',
+ callback: () => editProperties(currentFeature),
+ },
+ '-',
+ {
+ text: 'Zoom to Feature',
+ callback: () => {
+ const extent = currentFeature.getGeometry().getExtent();
+ map.getView().fit(extent, { duration: 700 });
+ },
+ },
+ ]);
+ } else {
+ // Menu for empty map
+ contextmenu.extend([
+ {
+ text: 'Add Marker',
+ callback: (obj) => addMarkerAt(obj.coordinate),
+ },
+ {
+ text: 'Center Here',
+ callback: (obj) => {
+ map.getView().animate({
+ center: obj.coordinate,
+ duration: 700,
+ });
+ },
+ },
+ ]);
+ }
+});
+```
+
+### Conditional Menu Items
+
+```javascript
+contextmenu.on('beforeopen', (evt) => {
+ const view = map.getView();
+ const zoom = view.getZoom();
+ const maxZoom = view.getMaxZoom();
+
+ // Prevent menu from opening at max zoom
+ if (zoom >= maxZoom) {
+ contextmenu.disable();
+ } else {
+ contextmenu.enable();
+ }
+});
+
+contextmenu.on('open', (evt) => {
+ const view = map.getView();
+ const zoom = view.getZoom();
+
+ // Add contextual items based on zoom level
+ if (zoom < 10) {
+ contextmenu.push({
+ text: 'Switch to Satellite',
+ callback: () => switchLayer('satellite'),
+ });
+ }
+});
+```
+
+## Feature Detection
+
+### Detect Feature Type
+
+```javascript
+contextmenu.on('beforeopen', (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ if (feature) {
+ const geomType = feature.getGeometry().getType();
+
+ contextmenu.clear();
+
+ switch (geomType) {
+ case 'Point':
+ contextmenu.extend([
+ { text: 'Move Marker', callback: moveMarker },
+ { text: 'Delete Marker', callback: deleteMarker },
+ ]);
+ break;
+
+ case 'LineString':
+ contextmenu.extend([
+ { text: 'Measure Length', callback: measureLength },
+ { text: 'Delete Line', callback: deleteLine },
+ ]);
+ break;
+
+ case 'Polygon':
+ contextmenu.extend([
+ { text: 'Measure Area', callback: measureArea },
+ { text: 'Delete Polygon', callback: deletePolygon },
+ ]);
+ break;
+ }
+ }
+});
+```
+
+### Multiple Feature Layers
+
+```javascript
+contextmenu.on('beforeopen', (evt) => {
+ const features = [];
+
+ map.forEachFeatureAtPixel(evt.pixel, (feature, layer) => {
+ features.push({ feature, layer });
+ });
+
+ contextmenu.clear();
+
+ if (features.length === 0) {
+ contextmenu.extend(defaultItems);
+ } else if (features.length === 1) {
+ contextmenu.extend(getSingleFeatureItems(features[0]));
+ } else {
+ // Multiple features - show selection submenu
+ contextmenu.push({
+ text: `${features.length} features`,
+ items: features.map((f, i) => ({
+ text: `Feature ${i + 1}`,
+ callback: () => selectFeature(f.feature),
+ })),
+ });
+ }
+});
+```
+
+## Custom Styling
+
+### Custom CSS Classes
+
+```javascript
+const items = [
+ {
+ text: 'Danger Action',
+ classname: 'danger-item',
+ callback: dangerAction,
+ },
+ {
+ text: 'Success Action',
+ classname: 'success-item',
+ callback: successAction,
+ },
+];
+```
+
+```css
+.danger-item {
+ color: #dc3545;
+ font-weight: bold;
+}
+
+.success-item {
+ color: #28a745;
+}
+```
+
+### Font Awesome Icons
+
+```javascript
+const items = [
+ {
+ text: 'Delete',
+ classname: 'fa fa-trash',
+ callback: deleteItem,
+ },
+ {
+ text: 'Edit',
+ classname: 'fa fa-edit',
+ callback: editItem,
+ },
+];
+```
+
+## Advanced Patterns
+
+### Restore Previous Menu
+
+```javascript
+// Store menu items separately since there's no getItems() method
+const defaultMenuItems = [
+ { text: 'Add Marker', callback: addMarker },
+ { text: 'Center Here', callback: centerMap },
+];
+
+const featureMenuItems = [
+ { text: 'Delete Feature', callback: deleteFeature },
+ { text: 'Edit Properties', callback: editProperties },
+];
+
+let isFeatureMenu = false;
+
+contextmenu.on('beforeopen', (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ if (feature && !isFeatureMenu) {
+ // Switch to feature menu
+ contextmenu.clear();
+ contextmenu.extend(featureMenuItems);
+ isFeatureMenu = true;
+ } else if (!feature && isFeatureMenu) {
+ // Restore default menu
+ contextmenu.clear();
+ contextmenu.extend(defaultMenuItems);
+ isFeatureMenu = false;
+ }
+});
+```
+
+### Async Data Loading
+
+```javascript
+contextmenu.on('open', async (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ if (feature) {
+ const featureId = feature.getId();
+
+ contextmenu.clear();
+ contextmenu.push({ text: 'Loading...', callback: () => {} });
+
+ try {
+ const data = await fetch(`/api/features/${featureId}`).then(r => r.json());
+
+ contextmenu.clear();
+ contextmenu.extend([
+ {
+ text: `Edit ${data.name}`,
+ callback: () => edit(data),
+ },
+ {
+ text: 'View Details',
+ callback: () => showDetails(data),
+ },
+ ]);
+ } catch (error) {
+ contextmenu.clear();
+ contextmenu.push({
+ text: 'Error loading data',
+ callback: () => {},
+ });
+ }
+ }
+});
+```
+
+### Coordinate Formatting
+
+```javascript
+import { toLonLat } from 'ol/proj';
+import { toStringHDMS } from 'ol/coordinate';
+
+const showCoordinates = (obj) => {
+ const lonLat = toLonLat(obj.coordinate);
+ const hdms = toStringHDMS(lonLat);
+
+ alert(`
+ Decimal: ${lonLat[1].toFixed(5)}, ${lonLat[0].toFixed(5)}
+ DMS: ${hdms}
+ `);
+};
+
+const items = [
+ {
+ text: 'Show Coordinates',
+ callback: showCoordinates,
+ },
+];
+```
+
+### Integration with Drawing
+
+```javascript
+import Draw from 'ol/interaction/Draw';
+
+let draw;
+const source = new VectorSource();
+
+contextmenu.on('open', (evt) => {
+ const items = [
+ {
+ text: 'Start Drawing',
+ items: [
+ {
+ text: 'Point',
+ callback: () => startDrawing('Point'),
+ },
+ {
+ text: 'LineString',
+ callback: () => startDrawing('LineString'),
+ },
+ {
+ text: 'Polygon',
+ callback: () => startDrawing('Polygon'),
+ },
+ ],
+ },
+ ];
+
+ if (draw) {
+ items.push({
+ text: 'Stop Drawing',
+ callback: () => {
+ map.removeInteraction(draw);
+ draw = null;
+ },
+ });
+ }
+
+ contextmenu.clear();
+ contextmenu.extend(items);
+});
+
+function startDrawing(type) {
+ if (draw) {
+ map.removeInteraction(draw);
+ }
+
+ draw = new Draw({
+ source,
+ type,
+ });
+
+ map.addInteraction(draw);
+}
+```
+
+## See Also
+
+- [API Reference](./api-reference.md)
+- [TypeScript Guide](./typescript.md)
+- [Getting Started](./getting-started.md)
+- [Local Examples](../examples)
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..9d51bba
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,233 @@
+# Getting Started with ol-contextmenu
+
+This guide will help you get up and running with `ol-contextmenu` in your OpenLayers project.
+
+## Prerequisites
+
+- **OpenLayers**: Version 7.0.0 or higher
+- **Modern Browser**: Chrome, Firefox, Safari, or Edge with ES6 support
+- **Build Tool** (optional): Webpack, Vite, Rollup, or similar
+
+## Installation
+
+### npm (Recommended)
+
+```bash
+npm install ol-contextmenu
+```
+
+If you don't have OpenLayers installed:
+
+```bash
+npm install ol ol-contextmenu
+```
+
+### CDN
+
+For quick prototyping or simple projects:
+
+#### jsDelivr
+
+```html
+
+
+
+```
+
+#### UNPKG
+
+```html
+
+
+
+```
+
+## Basic Setup
+
+### 1. Import the Library
+
+**ES6 Modules:**
+
+```javascript
+import Map from 'ol/Map';
+import View from 'ol/View';
+import TileLayer from 'ol/layer/Tile';
+import OSM from 'ol/source/OSM';
+import ContextMenu from 'ol-contextmenu';
+```
+
+**CommonJS:**
+
+```javascript
+const ContextMenu = require('ol-contextmenu');
+```
+
+**CDN (Global):**
+
+```javascript
+// Available as global: ContextMenu
+const contextmenu = new ContextMenu();
+```
+
+### 2. Create Your Map
+
+```javascript
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new OSM(),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+});
+```
+
+### 3. Initialize Context Menu
+
+```javascript
+const contextmenu = new ContextMenu({
+ width: 170,
+ defaultItems: true,
+ items: [
+ {
+ text: 'Center map here',
+ callback: (obj) => {
+ map.getView().animate({
+ center: obj.coordinate,
+ duration: 700,
+ });
+ },
+ },
+ '-', // Separator
+ {
+ text: 'Some Action',
+ callback: () => {
+ alert('Action clicked!');
+ },
+ },
+ ],
+});
+```
+
+### 4. Add to Map
+
+```javascript
+map.addControl(contextmenu);
+```
+
+## Complete Example
+
+Here's a complete working example:
+
+```html
+
+
+
+ ol-contextmenu Example
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Next Steps
+
+- **[API Reference](./api-reference.md)** - Learn about all available methods and options
+- **[Examples & Recipes](./examples.md)** - See common patterns and use cases
+- **[TypeScript Guide](./typescript.md)** - Add type safety to your project
+
+## Common Patterns
+
+### Adding Icons to Menu Items
+
+```javascript
+{
+ text: 'Add Marker',
+ icon: 'path/to/icon.png', // URL or data URI
+ callback: addMarker,
+}
+```
+
+### Creating Submenus
+
+```javascript
+{
+ text: 'Actions',
+ items: [
+ {
+ text: 'Action 1',
+ callback: action1,
+ },
+ {
+ text: 'Action 2',
+ callback: action2,
+ },
+ ],
+}
+```
+
+### Using Separators
+
+```javascript
+const items = [
+ { text: 'Item 1', callback: fn1 },
+ '-', // Separator
+ { text: 'Item 2', callback: fn2 },
+];
+```
+
+## Troubleshooting
+
+**Menu doesn't appear:** Make sure you've added the control to the map with `map.addControl(contextmenu)`.
+
+**Styles look wrong:** The CSS is bundled automatically since v6.0. No need to import separately.
+
+**TypeScript errors:** Make sure you're using the correct import syntax (see [TypeScript Guide](./typescript.md)).
+
+For more help, check the [Troubleshooting Guide](./troubleshooting.md).
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 0000000..cf8128b
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,425 @@
+# Troubleshooting
+
+Common issues and solutions for `ol-contextmenu`.
+
+## Installation Issues
+
+### Cannot find module 'ol-contextmenu'
+
+**Symptoms:**
+```
+Error: Cannot find module 'ol-contextmenu'
+```
+
+**Solutions:**
+
+1. **Verify installation:**
+ ```bash
+ npm list ol-contextmenu
+ ```
+
+2. **Reinstall the package:**
+ ```bash
+ npm install ol-contextmenu
+ ```
+
+3. **Clear cache and reinstall:**
+ ```bash
+ rm -rf node_modules package-lock.json
+ npm install
+ ```
+
+---
+
+### TypeScript cannot find types
+
+**Symptoms:**
+```
+Could not find a declaration file for module 'ol-contextmenu'
+```
+
+**Solutions:**
+
+Type definitions are included since v6.0. Make sure you're on the latest version:
+
+```bash
+npm install ol-contextmenu@latest
+```
+
+If the issue persists, check your `tsconfig.json`:
+
+```json
+{
+ "compilerOptions": {
+ "moduleResolution": "node",
+ "esModuleInterop": true
+ }
+}
+```
+
+## Display Issues
+
+### Menu doesn't appear
+
+**Symptoms:**
+- Right-clicking does nothing
+- No context menu visible
+
+**Solutions:**
+
+1. **Verify the control is added to the map:**
+ ```javascript
+ map.addControl(contextmenu);
+ ```
+
+2. **Check if menu is disabled:**
+ ```javascript
+ contextmenu.enable(); // Re-enable if needed
+ ```
+
+3. **Verify event type:**
+ ```javascript
+ const contextmenu = new ContextMenu({
+ eventType: 'contextmenu', // Default
+ });
+ ```
+
+4. **Check browser console for errors**
+
+5. **Ensure OpenLayers version compatibility:**
+ ```bash
+ npm list ol
+ # Should be 7.0.0 or higher
+ ```
+
+---
+
+### Menu appears cut off at viewport edges
+
+**Symptoms:**
+- Menu is partially hidden at screen edges
+- Bottom or right side of menu is cut off
+
+**Solution:**
+
+Update to v5.5.0 or higher (automatic viewport positioning):
+
+```bash
+npm install ol-contextmenu@latest
+```
+
+The menu now automatically repositions to stay within viewport bounds.
+
+---
+
+### Styles are missing or incorrect
+
+**Symptoms:**
+- Menu appears unstyled
+- CSS classes not applied
+
+**Solutions:**
+
+1. **Verify you're on v6.0+** (CSS is bundled):
+ ```bash
+ npm list ol-contextmenu
+ ```
+
+2. **Remove old CSS imports:**
+ ```diff
+ - import 'ol-contextmenu/dist/ol-contextmenu.css'; // Remove this
+ ```
+
+3. **Clear build cache:**
+ ```bash
+ rm -rf dist/
+ npm run build
+ ```
+
+4. **Check for CSS conflicts** in browser DevTools
+
+## Functional Issues
+
+### Callbacks not firing
+
+**Symptoms:**
+- Menu items don't respond to clicks
+- Callback functions never execute
+
+**Solutions:**
+
+1. **Verify callback syntax:**
+ ```javascript
+ // โ Correct
+ {
+ text: 'Test',
+ callback: (obj, map) => {
+ console.log('Clicked!');
+ }
+ }
+
+ // โ Wrong - missing parameters
+ {
+ text: 'Test',
+ callback: () => {
+ console.log('Clicked!');
+ }
+ }
+ ```
+
+2. **Check for JavaScript errors** in console
+
+3. **Ensure item has callback:**
+ ```javascript
+ // Separators don't have callbacks
+ '-' // This is correct
+
+ // Items need callbacks
+ { text: 'Test', callback: fn } // Needs callback
+ ```
+
+---
+
+### Menu closes immediately
+
+**Symptoms:**
+- Menu opens and closes instantly
+- Can't click menu items
+
+**Solutions:**
+
+1. **Check for conflicting click handlers:**
+ ```javascript
+ // Avoid stopPropagation on map clicks
+ map.on('click', (evt) => {
+ // Don't stopPropagation here
+ });
+ ```
+
+2. **Verify beforeopen handler:**
+ ```javascript
+ contextmenu.on('beforeopen', (evt) => {
+ // Don't call closeMenu() here
+ // Don't disable() immediately
+ });
+ ```
+
+---
+
+### Dynamic menus not updating
+
+**Symptoms:**
+- Menu items don't change based on context
+- Same items always show
+
+**Solutions:**
+
+1. **Clear menu before updating:**
+ ```javascript
+ contextmenu.on('beforeopen', (evt) => {
+ contextmenu.clear(); // Important!
+ contextmenu.extend(newItems);
+ });
+ ```
+
+2. **Use correct event:**
+ ```javascript
+ // โ Use beforeopen for menu changes
+ contextmenu.on('beforeopen', updateMenu);
+
+ // โ Don't use open (too late)
+ // contextmenu.on('open', updateMenu);
+ ```
+
+## TypeScript Issues
+
+### Import errors
+
+**Symptoms:**
+```typescript
+Module '"ol-contextmenu"' has no exported member 'ContextMenu'
+```
+
+**Solution:**
+
+Use correct import syntax:
+
+```typescript
+// โ Correct - default import
+import ContextMenu from 'ol-contextmenu';
+
+// โ Wrong - named import
+// import { ContextMenu } from 'ol-contextmenu';
+```
+
+---
+
+### Type errors with items
+
+**Symptoms:**
+```typescript
+Type '{ text: string; }' is missing the following properties: callback
+```
+
+**Solution:**
+
+Provide type annotations:
+
+```typescript
+import { type Item, type SingleItem } from 'ol-contextmenu';
+
+// Option 1: Type the array
+const items: Item[] = [
+ { text: 'Test', callback: (obj, map) => {} }
+];
+
+// Option 2: Type individual items
+const item: SingleItem = {
+ text: 'Test',
+ callback: (obj, map) => {}
+};
+```
+
+## Performance Issues
+
+### Menu opens slowly
+
+**Symptoms:**
+- Delay before menu appears
+- Laggy interaction
+
+**Solutions:**
+
+1. **Optimize beforeopen handler:**
+ ```javascript
+ // โ Slow - recalculating every time
+ contextmenu.on('beforeopen', (evt) => {
+ const items = expensiveCalculation();
+ contextmenu.extend(items);
+ });
+
+ // โ Fast - calculate once
+ const items = expensiveCalculation();
+ contextmenu.on('beforeopen', (evt) => {
+ contextmenu.extend(items);
+ });
+ ```
+
+2. **Reduce number of items** (use submenus for large lists)
+
+3. **Avoid DOM operations in callbacks:**
+ ```javascript
+ // โ Slow
+ callback: () => {
+ document.querySelectorAll('.heavy-selector').forEach(/* ... */);
+ }
+
+ // โ Fast
+ callback: () => {
+ cachedElements.forEach(/* ... */);
+ }
+ ```
+
+## Browser-Specific Issues
+
+### Menu doesn't work in Safari
+
+**Symptoms:**
+- Works in Chrome/Firefox but not Safari
+
+**Solutions:**
+
+1. **Check Safari version** (modern versions only)
+2. **Verify ES6 support** (Safari 10+)
+3. **Check for polyfills needed**
+
+---
+
+### Browser context menu still appears
+
+**Symptoms:**
+- Both custom and browser context menus show
+
+**Solution:**
+
+This is expected behavior. The library prevents default for `contextmenu` events, but some browsers may still show their menu. This is handled correctly by the library.
+
+## Build Issues
+
+### Webpack build errors
+
+**Symptoms:**
+```
+Module parse failed: Unexpected token
+```
+
+**Solution:**
+
+Update Webpack config to handle ES modules:
+
+```javascript
+module.exports = {
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ include: /node_modules\/ol-contextmenu/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: ['@babel/preset-env']
+ }
+ }
+ }
+ ]
+ }
+};
+```
+
+---
+
+### Vite build errors
+
+**Solution:**
+
+Vite should work out of the box. If you encounter issues:
+
+```javascript
+// vite.config.js
+export default {
+ optimizeDeps: {
+ include: ['ol-contextmenu']
+ }
+};
+```
+
+## Getting More Help
+
+If your issue isn't covered here:
+
+1. **Search existing issues:**
+ [GitHub Issues](https://github.com/jonataswalker/ol-contextmenu/issues)
+
+2. **Check the examples:**
+ [Examples Directory](../examples)
+
+3. **Review the API documentation:**
+ [API Reference](./api-reference.md)
+
+4. **Create a new issue:**
+
+ When creating an issue, please include:
+ - ol-contextmenu version
+ - OpenLayers version
+ - Browser and version
+ - Minimal reproduction example
+ - Error messages
+ - What you've tried
+
+ [Create Issue](https://github.com/jonataswalker/ol-contextmenu/issues/new)
+
+## See Also
+
+- [Getting Started](./getting-started.md)
+- [API Reference](./api-reference.md)
+- [Migration Guide](./migration.md)
diff --git a/docs/typescript.md b/docs/typescript.md
new file mode 100644
index 0000000..803a4f8
--- /dev/null
+++ b/docs/typescript.md
@@ -0,0 +1,440 @@
+# TypeScript Guide
+
+`ol-contextmenu` includes full TypeScript support with comprehensive type definitions.
+
+## Installation
+
+```bash
+npm install ol ol-contextmenu
+```
+
+Type definitions are included automaticallyโno need for `@types` packages.
+
+๐ For detailed installation instructions, see [Getting Started](./getting-started.md).
+
+## Basic Usage
+
+### Importing
+
+```typescript
+import ContextMenu, {
+ type Item,
+ type Options,
+ type SingleItem,
+ type CallbackObject,
+ type ContextMenuEvent,
+} from 'ol-contextmenu';
+import type Map from 'ol/Map';
+import type { Coordinate } from 'ol/coordinate';
+```
+
+**Note:** Use `type` imports for better tree-shaking and to avoid runtime imports.
+
+### Creating a Context Menu
+
+```typescript
+import ContextMenu, { type Item } from 'ol-contextmenu';
+import type Map from 'ol/Map';
+
+const items: Item[] = [
+ {
+ text: 'Center map here',
+ callback: (obj, map: Map) => {
+ map.getView().animate({
+ center: obj.coordinate,
+ duration: 700,
+ });
+ },
+ },
+ '-', // Separator (type-safe)
+ {
+ text: 'Zoom In',
+ callback: (obj, map: Map) => {
+ const view = map.getView();
+ view.animate({
+ zoom: view.getZoom()! + 1,
+ duration: 500,
+ });
+ },
+ },
+];
+
+const contextmenu = new ContextMenu({
+ width: 170,
+ defaultItems: true,
+ items,
+});
+```
+
+## Available Types
+
+### `Item`
+
+Union type representing any menu item:
+
+```typescript
+type Item = SingleItem | ItemWithNested | ItemSeparator;
+```
+
+### `SingleItem`
+
+A standard menu item with a callback:
+
+```typescript
+interface SingleItem {
+ text: string;
+ callback: (obj: CallbackObject, map: Map) => void;
+ icon?: string;
+ classname?: string;
+ data?: any;
+}
+```
+
+### `ItemWithNested`
+
+A menu item containing nested subitems:
+
+```typescript
+interface ItemWithNested {
+ text: string;
+ items: Item[];
+ icon?: string;
+ classname?: string;
+}
+```
+
+### `ItemSeparator`
+
+Menu separator (literal type):
+
+```typescript
+type ItemSeparator = '-';
+```
+
+### `Options`
+
+Constructor options:
+
+```typescript
+interface Options {
+ width?: number;
+ defaultItems?: boolean;
+ items?: Item[];
+ eventType?: EventTypes;
+}
+```
+
+### `CallbackObject`
+
+Data passed to callbacks:
+
+```typescript
+interface CallbackObject {
+ coordinate: Coordinate;
+ data?: any;
+}
+```
+
+### `ContextMenuEvent`
+
+Event data for `beforeopen` and `open` events:
+
+```typescript
+class ContextMenuEvent extends BaseEvent {
+ map: Map;
+ coordinate: Coordinate;
+ pixel: Pixel;
+ originalEvent: MouseEvent;
+}
+```
+
+### `EventTypes`
+
+Enum for event trigger types:
+
+```typescript
+enum EventTypes {
+ CONTEXTMENU = 'contextmenu',
+ CLICK = 'click',
+ DBLCLICK = 'dblclick',
+}
+```
+
+### `CustomEventTypes`
+
+Enum for menu events:
+
+```typescript
+enum CustomEventTypes {
+ BEFOREOPEN = 'beforeopen',
+ OPEN = 'open',
+ CLOSE = 'close',
+}
+```
+
+## Type-Safe Patterns
+
+### Typed Callback Functions
+
+```typescript
+import type { CallbackObject } from 'ol-contextmenu';
+import type Map from 'ol/Map';
+
+const handleCenter = (obj: CallbackObject, map: Map): void => {
+ map.getView().setCenter(obj.coordinate);
+};
+
+const handleZoom = (obj: CallbackObject, map: Map): void => {
+ const view = map.getView();
+ const currentZoom = view.getZoom();
+
+ if (currentZoom !== undefined) {
+ view.setZoom(currentZoom + 1);
+ }
+};
+
+const items: Item[] = [
+ { text: 'Center', callback: handleCenter },
+ { text: 'Zoom In', callback: handleZoom },
+];
+```
+
+### Custom Data with Type Safety
+
+```typescript
+interface MarkerData {
+ id: number;
+ name: string;
+ type: 'marker' | 'poi';
+}
+
+const markerItem: SingleItem = {
+ text: 'Delete Marker',
+ data: {
+ id: 1,
+ name: 'My Marker',
+ type: 'marker',
+ } as MarkerData,
+ callback: (obj) => {
+ const data = obj.data as MarkerData;
+ console.log(`Deleting ${data.type}: ${data.name}`);
+ },
+};
+```
+
+### Event Handlers
+
+```typescript
+import { CustomEventTypes, type ContextMenuEvent } from 'ol-contextmenu';
+
+contextmenu.on(CustomEventTypes.BEFOREOPEN, (evt: ContextMenuEvent) => {
+ console.log('Clicked at:', evt.coordinate);
+ console.log('Pixel:', evt.pixel);
+ console.log('Map:', evt.map);
+});
+
+contextmenu.on(CustomEventTypes.OPEN, (evt: ContextMenuEvent) => {
+ const feature = evt.map.forEachFeatureAtPixel(
+ evt.pixel,
+ (ft) => ft,
+ );
+
+ if (feature) {
+ console.log('Clicked on feature:', feature.getId());
+ }
+});
+```
+
+### Nested Submenus
+
+```typescript
+const nestedItems: Item[] = [
+ {
+ text: 'Tools',
+ items: [
+ {
+ text: 'Measure',
+ items: [
+ {
+ text: 'Distance',
+ callback: measureDistance,
+ },
+ {
+ text: 'Area',
+ callback: measureArea,
+ },
+ ],
+ },
+ '-',
+ {
+ text: 'Export',
+ callback: exportMap,
+ },
+ ],
+ },
+];
+```
+
+## Complete TypeScript Example
+
+```typescript
+import Map from 'ol/Map';
+import View from 'ol/View';
+import TileLayer from 'ol/layer/Tile';
+import OSM from 'ol/source/OSM';
+import VectorLayer from 'ol/layer/Vector';
+import VectorSource from 'ol/source/Vector';
+import Feature from 'ol/Feature';
+import Point from 'ol/geom/Point';
+import ContextMenu, {
+ type Item,
+ type CallbackObject,
+ CustomEventTypes,
+} from 'ol-contextmenu';
+
+// Initialize map
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new OSM(),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+});
+
+// Vector layer for markers
+const vectorSource = new VectorSource();
+const vectorLayer = new VectorLayer({
+ source: vectorSource,
+});
+map.addLayer(vectorLayer);
+
+// Type-safe callback functions
+const addMarker = (obj: CallbackObject, map: Map): void => {
+ const marker = new Feature({
+ geometry: new Point(obj.coordinate),
+ });
+ vectorSource.addFeature(marker);
+ console.log('Marker added at', obj.coordinate);
+};
+
+const centerMap = (obj: CallbackObject, map: Map): void => {
+ map.getView().animate({
+ center: obj.coordinate,
+ duration: 700,
+ });
+};
+
+// Define menu items
+const items: Item[] = [
+ {
+ text: 'Add Marker',
+ icon: './marker.png',
+ callback: addMarker,
+ },
+ '-',
+ {
+ text: 'Center Here',
+ callback: centerMap,
+ },
+];
+
+// Create context menu
+const contextmenu = new ContextMenu({
+ width: 180,
+ defaultItems: true,
+ items,
+});
+
+// Add to map
+map.addControl(contextmenu);
+
+// Event handlers
+contextmenu.on(CustomEventTypes.BEFOREOPEN, (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (ft) => ft);
+
+ if (feature) {
+ contextmenu.clear();
+ contextmenu.push({
+ text: 'Delete Marker',
+ callback: () => {
+ vectorSource.removeFeature(feature);
+ },
+ });
+ } else {
+ contextmenu.clear();
+ contextmenu.extend(items);
+ contextmenu.extend(contextmenu.getDefaultItems());
+ }
+});
+```
+
+## Strict Type Checking
+
+Enable strict mode in `tsconfig.json` for maximum type safety:
+
+```json
+{
+ "compilerOptions": {
+ "strict": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "noImplicitAny": true,
+ "noImplicitThis": true
+ }
+}
+```
+
+## Common Type Issues
+
+### Issue: Cannot find module 'ol-contextmenu'
+
+**Solution:** Make sure the package is installed:
+
+```bash
+npm install ol-contextmenu
+```
+
+### Issue: Type errors with `callback`
+
+**Problem:**
+```typescript
+// โ Error: Type '() => void' is not assignable...
+const item = {
+ text: 'Test',
+ callback: () => console.log('test'),
+};
+```
+
+**Solution:**
+```typescript
+// โ Correct: Include parameters
+const item: SingleItem = {
+ text: 'Test',
+ callback: (obj, map) => console.log('test'),
+};
+```
+
+### Issue: Cannot import types
+
+**Problem:**
+```typescript
+// โ Wrong
+import { ContextMenu, Item } from 'ol-contextmenu';
+```
+
+**Solution:**
+```typescript
+// โ Correct
+import ContextMenu, { type Item } from 'ol-contextmenu';
+```
+
+## See Also
+
+- [API Reference](./api-reference.md)
+- [Getting Started](./getting-started.md)
+- [Examples & Recipes](./examples.md)
diff --git a/package.json b/package.json
index 5b5f0a0..1a80199 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,29 @@
"name": "ol-contextmenu",
"version": "5.5.0",
"description": "Custom Context Menu for Openlayers",
+ "keywords": [
+ "openlayers",
+ "ol",
+ "contextmenu",
+ "context-menu",
+ "right-click",
+ "menu",
+ "map",
+ "gis",
+ "geospatial"
+ ],
"type": "module",
"main": "./dist/ol-contextmenu.umd.cjs",
"jsdelivr": "./dist/ol-contextmenu.iife.js",
"module": "./dist/ol-contextmenu.js",
+ "browser": "./dist/ol-contextmenu.js",
+ "sideEffects": ["./dist/ol-contextmenu.js", "./dist/ol-contextmenu.umd.cjs", "./dist/ol-contextmenu.iife.js"],
"exports": {
".": {
"types": "./dist/ol-contextmenu.d.ts",
- "import": "./dist/ol-contextmenu.js"
+ "import": "./dist/ol-contextmenu.js",
+ "require": "./dist/ol-contextmenu.umd.cjs",
+ "default": "./dist/ol-contextmenu.js"
}
},
"files": [
@@ -27,8 +42,8 @@
"url": "https://github.com/jonataswalker/ol-contextmenu/issues"
},
"engines": {
- "node": ">=16",
- "npm": ">=8"
+ "node": ">=24",
+ "npm": ">=11"
},
"scripts": {
"dev": "vite",
diff --git a/vite.config.ts b/vite.config.ts
index 9edbdf9..f723ac7 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -62,8 +62,8 @@ export default defineConfig(({ command }) =>
dts({
exclude: ['src/**/*.spec.ts', 'src/**/*.test.ts', 'tests/**', '**/*.config.ts'],
include: ['src/**/*.ts'],
- insertTypesEntry: true,
outDir: 'dist',
+ rollupTypes: true,
}),
bannerPlugin(banner),
cssInjectedByJsPlugin(),