├── tests ├── __init__.py ├── images │ └── django.png ├── forms.py ├── test_metadata.py ├── settings.py ├── test_basic_form.py ├── models.py ├── test_widgets.py ├── test_serializers.py └── test_fields.py ├── requirements.txt ├── .gitattributes ├── setup.py ├── colorfield ├── static │ └── colorfield │ │ ├── coloris │ │ ├── README.txt │ │ ├── coloris.min.css │ │ ├── coloris.css │ │ ├── coloris.min.js │ │ └── coloris.js │ │ └── colorfield.js ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── it │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── __init__.py ├── metadata.py ├── templates │ └── colorfield │ │ └── color.html ├── forms.py ├── utils.py ├── serializers.py ├── validators.py ├── widgets.py └── fields.py ├── requirements-test.txt ├── MANIFEST.in ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── dependabot.yml └── workflows │ ├── pre-commit-autoupdate.yml │ ├── create-release.yml │ └── test-package.yml ├── SECURITY.md ├── runtests.py ├── .pre-commit-config.yaml ├── .gitignore ├── LICENSE.txt ├── tox.ini ├── pyproject.toml ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django >= 2.2 2 | Pillow >= 9.0.0 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | colorfield/static/colorfield/coloris/* linguist-vendored=true 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/coloris/README.txt: -------------------------------------------------------------------------------- 1 | https://github.com/mdbassit/Coloris 2 | 3 | Release: v0.24.0 4 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | djangorestframework 2 | coverage == 7.10.* 3 | pre-commit == 4.3.* 4 | tox == 4.30.* 5 | -------------------------------------------------------------------------------- /tests/images/django.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiocaccamo/django-colorfield/HEAD/tests/images/django.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | recursive-include colorfield * 4 | recursive-exclude * *.pyc __pycache__ .DS_Store 5 | -------------------------------------------------------------------------------- /colorfield/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiocaccamo/django-colorfield/HEAD/colorfield/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /colorfield/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiocaccamo/django-colorfield/HEAD/colorfield/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /colorfield/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiocaccamo/django-colorfield/HEAD/colorfield/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fabiocaccamo] 2 | ko_fi: fabiocaccamo 3 | custom: ["https://www.buymeacoffee.com/fabiocaccamo", "https://www.paypal.me/fabiocaccamo"] 4 | -------------------------------------------------------------------------------- /tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from colorfield.forms import ColorField 3 | 4 | 5 | class BasicForm(forms.Form): 6 | color = ColorField(initial="#FF0000") 7 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Keep this library updated to the latest version. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | latest | :white_check_mark: | 10 | | oldest | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Open an issue. 15 | -------------------------------------------------------------------------------- /colorfield/__init__.py: -------------------------------------------------------------------------------- 1 | from colorfield.metadata import ( 2 | __author__, 3 | __copyright__, 4 | __description__, 5 | __license__, 6 | __title__, 7 | __version__, 8 | ) 9 | 10 | __all__ = [ 11 | "__author__", 12 | "__copyright__", 13 | "__description__", 14 | "__license__", 15 | "__title__", 16 | "__version__", 17 | ] 18 | -------------------------------------------------------------------------------- /colorfield/metadata.py: -------------------------------------------------------------------------------- 1 | __author__ = "Jared Forsyth, Fabio Caccamo" 2 | __copyright__ = "Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo" 3 | __description__ = "color field for django models with a nice color-picker in the admin." 4 | __email__ = "jared@jaredforsyth.com, fabio.caccamo@gmail.com" 5 | __license__ = "MIT" 6 | __title__ = "django-colorfield" 7 | __version__ = "0.14.0" 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Bug 6 | assignees: fabiocaccamo 7 | 8 | --- 9 | 10 | **Python version** 11 | ? 12 | 13 | **Django version** 14 | ? 15 | 16 | **Package version** 17 | ? 18 | 19 | **Current behavior (bug description)** 20 | ? 21 | 22 | **Expected behavior** 23 | ? 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Submit a pull request for this project 4 | assignees: fabiocaccamo 5 | 6 | --- 7 | 8 | **Describe your changes** 9 | ? 10 | 11 | **Related issue** 12 | ? 13 | 14 | **Checklist before requesting a review** 15 | - [ ] I have performed a self-review of my code. 16 | - [ ] I have added tests for the proposed changes. 17 | - [ ] I have run the tests and there are not errors. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | groups: 9 | python-requirements: 10 | patterns: 11 | - "*" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | from django.test.utils import get_runner 7 | 8 | 9 | def runtests(): 10 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" 11 | django.setup() 12 | TestRunner = get_runner(settings) 13 | test_runner = TestRunner() 14 | failures = test_runner.run_tests(["tests"]) 15 | return failures 16 | 17 | 18 | if __name__ == "__main__": 19 | failures = runtests() 20 | sys.exit(bool(failures)) 21 | -------------------------------------------------------------------------------- /colorfield/templates/colorfield/color.html: -------------------------------------------------------------------------------- 1 | 12 | {{ data_coloris_options|json_script:data_coloris_id }} 13 | -------------------------------------------------------------------------------- /colorfield/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from colorfield.validators import color_hex_validator 4 | from colorfield.widgets import ColorWidget 5 | 6 | 7 | class ColorField(forms.CharField): 8 | default_validators = [ 9 | color_hex_validator, 10 | ] 11 | 12 | def __init__(self, *args, **kwargs): 13 | validator = kwargs.pop("validator", color_hex_validator) 14 | self.default_validators = [validator] 15 | kwargs.setdefault("widget", ColorWidget()) 16 | super().__init__(*args, **kwargs) 17 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/colorfield.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function () { 2 | const inputs = document.getElementsByClassName('colorfield_field coloris'); 3 | for (const input of inputs) { 4 | const colorisId = input.getAttribute('data-coloris-options-json-script-id'); 5 | const script = document.querySelector(`script[id='${colorisId}']`); 6 | const options = JSON.parse(script.textContent); 7 | 8 | const id = input.getAttribute('id'); 9 | Coloris.setInstance(`.colorfield_field.coloris.${id}`, options); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Feature 6 | assignees: fabiocaccamo 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-autoupdate.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit auto-update 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: write 6 | 7 | on: 8 | # every month 9 | schedule: 10 | - cron: "0 0 1 * *" 11 | # on demand 12 | workflow_dispatch: 13 | 14 | jobs: 15 | auto-update: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v6 19 | - uses: actions/setup-python@v6 20 | with: 21 | python-version: '3.x' 22 | - uses: browniebroke/pre-commit-autoupdate-action@main 23 | - uses: peter-evans/create-pull-request@v7 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | branch: update/pre-commit-hooks 27 | title: Update pre-commit hooks 28 | commit-message: "Update pre-commit hooks." 29 | body: Update versions of pre-commit hooks to latest version. 30 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | 5 | - repo: https://github.com/asottile/pyupgrade 6 | rev: v3.21.2 7 | hooks: 8 | - id: pyupgrade 9 | args: ["--py38-plus"] 10 | 11 | - repo: https://github.com/adamchainz/django-upgrade 12 | rev: 1.29.1 13 | hooks: 14 | - id: django-upgrade 15 | args: ["--target-version", "3.0"] 16 | 17 | - repo: https://github.com/astral-sh/ruff-pre-commit 18 | rev: v0.14.9 19 | hooks: 20 | - id: ruff 21 | args: [--fix, --exit-non-zero-on-fix] 22 | - id: ruff-format 23 | 24 | - repo: https://github.com/pre-commit/pre-commit-hooks 25 | rev: v6.0.0 26 | hooks: 27 | - id: trailing-whitespace 28 | - id: end-of-file-fixer 29 | - id: check-yaml 30 | - id: check-added-large-files 31 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.test import TestCase 4 | 5 | from colorfield.metadata import ( 6 | __author__, 7 | __copyright__, 8 | __description__, 9 | __email__, 10 | __license__, 11 | __title__, 12 | __version__, 13 | ) 14 | 15 | 16 | class MetadataTestCase(TestCase): 17 | """ 18 | This class describes a metadata test case. 19 | """ 20 | 21 | def test_metadata(self): 22 | self.assertTrue(isinstance(__author__, str)) 23 | self.assertTrue(isinstance(__copyright__, str)) 24 | self.assertTrue(isinstance(__description__, str)) 25 | self.assertTrue(isinstance(__email__, str)) 26 | self.assertTrue(isinstance(__license__, str)) 27 | self.assertTrue(isinstance(__title__, str)) 28 | self.assertTrue(isinstance(__version__, str)) 29 | 30 | def test_version(self): 31 | v = __version__ 32 | v_re = re.compile(r"^([0-9]+)(\.([0-9]+)){1,2}$") 33 | v_match = v_re.match(v) 34 | self.assertTrue(v_match is not None) 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | ## Local setup 9 | .vscode/ 10 | .venv/ 11 | venv/ 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | screenshots 63 | TODO.txt 64 | 65 | .idea/ 66 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /colorfield/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django ColorField. 2 | # Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Fabio Caccamo , 2023. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-colorfield\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-03-14 00:20+0100\n" 12 | "PO-Revision-Date: 2025-05-01 11:24+0200\n" 13 | "Last-Translator: Fabio Caccamo \n" 14 | "Language-Team: English \n" 15 | "Language: English \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: colorfield/validators.py:9 22 | msgid "Enter a valid hex color, eg. #000000" 23 | msgstr "" 24 | 25 | #: colorfield/validators.py:17 26 | msgid "Enter a valid hexa color, eg. #00000000" 27 | msgstr "" 28 | 29 | #: colorfield/validators.py:39 30 | msgid "Enter a valid rgb color, eg. rgb(128, 128, 128)" 31 | msgstr "" 32 | 33 | #: colorfield/validators.py:64 34 | msgid "Enter a valid rgba color, eg. rgba(128, 128, 128, 0.5)" 35 | msgstr "" 36 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | # environment: release 12 | permissions: 13 | id-token: write 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | 19 | - name: Extract release notes 20 | id: extract-release-notes 21 | uses: ffurrer2/extract-release-notes@v3 22 | 23 | - name: Create release 24 | uses: ncipollo/release-action@v1 25 | with: 26 | body: ${{ steps.extract-release-notes.outputs.release_notes }} 27 | token: ${{ secrets.WORKFLOWS_CREATE_RELEASE_TOKEN }} 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v6 31 | with: 32 | python-version: '3.x' 33 | cache: 'pip' 34 | 35 | - name: Build Package 36 | run: | 37 | pip install pip --upgrade 38 | pip install build 39 | python -m build 40 | 41 | - name: Publish on PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | with: 44 | packages-dir: dist/ 45 | # password: ${{ secrets.WORKFLOWS_PUBLISH_TO_PYPI_TOKEN }} 46 | -------------------------------------------------------------------------------- /colorfield/locale/it/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django ColorField. 2 | # Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Fabio Caccamo , 2023. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-colorfield\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2023-03-14 00:20+0100\n" 12 | "PO-Revision-Date: 2025-05-01 11:24+0200\n" 13 | "Last-Translator: Fabio Caccamo \n" 14 | "Language-Team: Italian \n" 15 | "Language: Italian \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: colorfield/validators.py:9 22 | msgid "Enter a valid hex color, eg. #000000" 23 | msgstr "Inserisci un colore esadecimale valido, ad es. #000000" 24 | 25 | #: colorfield/validators.py:17 26 | msgid "Enter a valid hexa color, eg. #00000000" 27 | msgstr "Inserisci un colore esadecimale valido, ad es. #00000000" 28 | 29 | #: colorfield/validators.py:39 30 | msgid "Enter a valid rgb color, eg. rgb(128, 128, 128)" 31 | msgstr "Inserisci un colore rgb valido, ad es. rgb(128, 128, 128)" 32 | 33 | #: colorfield/validators.py:64 34 | msgid "Enter a valid rgba color, eg. rgba(128, 128, 128, 0.5)" 35 | msgstr "Inserisci un colore rgba valido, ad es. rgba(128, 128, 128, 0.5)" 36 | -------------------------------------------------------------------------------- /colorfield/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django ColorField. 2 | # Copyright (c) 2013-present Jared Forsyth / Fabio Caccamo 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # Obdulia Losantos , 2025. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: django-colorfield\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-05-01 11:24+0200\n" 12 | "PO-Revision-Date: 2025-05-01 11:24+0200\n" 13 | "Last-Translator: Obdulia Losantos \n" 14 | "Language-Team: Spanish \n" 15 | "Language: Spanish \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: colorfield/validators.py:9 22 | msgid "Enter a valid hex color, eg. #000000" 23 | msgstr "Introduzca un color en hexadecimal válido, ej. #000000" 24 | 25 | #: colorfield/validators.py:17 26 | msgid "Enter a valid hexa color, eg. #00000000" 27 | msgstr "" 28 | "Introduzca un color en hexadecimal con transparencia válido, ej. #00000000" 29 | 30 | #: colorfield/validators.py:39 31 | msgid "Enter a valid rgb color, eg. rgb(128, 128, 128)" 32 | msgstr "Introduzca un color en RGB válido, ej. rgb(128, 128, 128)" 33 | 34 | #: colorfield/validators.py:64 35 | msgid "Enter a valid rgba color, eg. rgba(128, 128, 128, 0.5)" 36 | msgstr "Introduzca un color en RGB con transparencia válido, ej. rgba(128, 128, 128, 0.5)" 37 | -------------------------------------------------------------------------------- /colorfield/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import string 3 | 4 | from PIL import Image, UnidentifiedImageError 5 | 6 | from django.utils.crypto import get_random_string as dj_get_random_string 7 | 8 | 9 | def get_image_background_color(img, img_format: str): 10 | has_alpha = img_format in {"hexa", "rgba"} 11 | img = img.convert("RGBA" if has_alpha else "RGB") 12 | pixel_color = img.getpixel((1, 1)) 13 | if img_format in {"hex", "hexa"}: 14 | color_format = "#" + "%02x" * len(pixel_color) 15 | color = color_format % pixel_color 16 | color = color.upper() 17 | elif img_format in {"rgb", "rgba"}: 18 | if has_alpha: 19 | # Normalize alpha channel to be between 0 and 1 20 | pixel_color = ( 21 | *pixel_color[:3], 22 | round(pixel_color[3] / 255, 2), 23 | ) 24 | # Should look like `rgb(1, 2, 3) or rgba(1, 2, 3, 1.0) 25 | color = f"{img_format}{pixel_color}" 26 | else: # pragma: no cover 27 | raise NotImplementedError(f"Unsupported color format: {img_format}") 28 | return color 29 | 30 | 31 | def get_image_file_background_color(img_file, img_format: str): 32 | color = "" 33 | with contextlib.suppress(UnidentifiedImageError): 34 | with Image.open(img_file) as image: 35 | color = get_image_background_color(image, img_format) 36 | return color 37 | 38 | 39 | def get_random_string(): 40 | return dj_get_random_string( 41 | length=32, allowed_chars=string.ascii_lowercase + string.digits 42 | ) 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py310-{dj40,dj41,dj42,dj50,dj51,dj52}-{sqlite}, 4 | py311-{dj41,dj42,dj50,dj51,dj52}-{sqlite}, 5 | py312-{dj42,dj50,dj51,dj52,dj60}-{sqlite}, 6 | py313-{dj51,dj52,dj60}-{sqlite}, 7 | py314-{dj52,dj60}-{sqlite}, 8 | 9 | [gh-actions] 10 | python = 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | 3.13: py313 15 | 3.14: py314 16 | 17 | [testenv] 18 | passenv = CI,GITHUB_WORKFLOW 19 | 20 | setenv = 21 | sqlite: DATABASE_ENGINE=sqlite 22 | 23 | deps = 24 | dj40: Django == 4.0.* 25 | dj41: Django == 4.1.* 26 | dj42: Django == 4.2.* 27 | dj50: Django == 5.0.* 28 | dj51: Django == 5.1.* 29 | dj52: Django == 5.2.* 30 | dj60: Django == 6.0.* 31 | -r requirements.txt 32 | -r requirements-test.txt 33 | 34 | commands = 35 | pre-commit run --all-files 36 | coverage run --append --source=colorfield runtests.py 37 | coverage report --show-missing --ignore-errors 38 | 39 | [testenv:migrations] 40 | setenv = 41 | DJANGO_SETTINGS_MODULE=tests.settings 42 | DATABASE_ENGINE=sqlite 43 | deps = 44 | -r requirements.txt 45 | commands = 46 | python -m django makemigrations --check 47 | 48 | [testenv:translations] 49 | setenv = 50 | DJANGO_SETTINGS_MODULE=tests.settings 51 | DATABASE_ENGINE=sqlite 52 | deps = 53 | -r requirements.txt 54 | allowlist_externals = git 55 | commands = 56 | python -m django makemessages --ignore ".tox" --ignore "venv" --all --add-location "file" --extension "html,py" 57 | python -m django compilemessages --ignore ".tox" --ignore "venv" 58 | git diff colorfield/locale/ 59 | git diff-index --quiet HEAD colorfield/locale/ 60 | -------------------------------------------------------------------------------- /colorfield/serializers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError as DjangoValidationError 2 | 3 | try: 4 | from rest_framework.serializers import CharField 5 | from rest_framework.serializers import ValidationError as DRFValidationError 6 | except ImportError: 7 | ModuleNotFoundError("Django REST Framework is not installed.") 8 | 9 | from colorfield.validators import ( 10 | color_hex_validator, 11 | color_hexa_validator, 12 | color_rgb_validator, 13 | color_rgba_validator, 14 | ) 15 | 16 | 17 | class ColorField(CharField): 18 | default_error_messages = { 19 | "invalid": [ 20 | color_hex_validator.message, 21 | color_hexa_validator.message, 22 | color_rgb_validator.message, 23 | color_rgba_validator.message, 24 | ] 25 | } 26 | 27 | def to_internal_value(self, data): 28 | errors = { 29 | "hex": False, 30 | "hexa": False, 31 | "rgb": False, 32 | "rgba": False, 33 | } 34 | try: 35 | color_hex_validator(data) 36 | except DjangoValidationError: 37 | errors["hex"] = True 38 | 39 | try: 40 | color_hexa_validator(data) 41 | except DjangoValidationError: 42 | errors["hexa"] = True 43 | 44 | try: 45 | color_rgb_validator(data) 46 | except DjangoValidationError: 47 | errors["rgb"] = True 48 | 49 | try: 50 | color_rgba_validator(data) 51 | except DjangoValidationError: 52 | errors["rgba"] = True 53 | 54 | if all(errors.values()): 55 | raise DRFValidationError(self.default_error_messages.get("invalid")) 56 | 57 | return data 58 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = "django-colorfield" 6 | 7 | ALLOWED_HOSTS = ["*"] 8 | 9 | # Application definition 10 | INSTALLED_APPS = [ 11 | "tests", 12 | "colorfield", 13 | ] 14 | 15 | INSTALLED_APPS += [ 16 | "django.contrib.admin", 17 | "django.contrib.auth", 18 | "django.contrib.contenttypes", 19 | "django.contrib.messages", 20 | "django.contrib.sessions", 21 | ] 22 | 23 | MIDDLEWARE = [ 24 | "django.contrib.auth.middleware.AuthenticationMiddleware", 25 | "django.contrib.messages.middleware.MessageMiddleware", 26 | "django.contrib.sessions.middleware.SessionMiddleware", 27 | "django.middleware.common.CommonMiddleware", 28 | ] 29 | 30 | TEMPLATES = [ 31 | { 32 | "BACKEND": "django.template.backends.django.DjangoTemplates", 33 | "DIRS": [], 34 | "APP_DIRS": True, 35 | "OPTIONS": { 36 | "context_processors": [ 37 | "django.template.context_processors.request", 38 | "django.contrib.auth.context_processors.auth", 39 | "django.contrib.messages.context_processors.messages", 40 | ] 41 | }, 42 | }, 43 | ] 44 | 45 | database_engine = os.environ.get("DATABASE_ENGINE", "sqlite") 46 | database_config = { 47 | "sqlite": { 48 | "ENGINE": "django.db.backends.sqlite3", 49 | "NAME": ":memory:", 50 | }, 51 | } 52 | 53 | DATABASES = { 54 | "default": database_config.get(database_engine), 55 | } 56 | 57 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 58 | 59 | MEDIA_ROOT = os.path.join(BASE_DIR, "tests/media/") 60 | MEDIA_URL = "/media/" 61 | 62 | STATIC_ROOT = os.path.join(BASE_DIR, "tests/static/") 63 | STATIC_URL = "/static/" 64 | -------------------------------------------------------------------------------- /colorfield/validators.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.core.validators import RegexValidator 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | COLOR_HEX_RE = re.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") 7 | color_hex_validator = RegexValidator( 8 | COLOR_HEX_RE, 9 | _("Enter a valid hex color, eg. #000000"), 10 | "invalid", 11 | ) 12 | 13 | 14 | COLOR_HEXA_RE = re.compile("#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{4})$") 15 | color_hexa_validator = RegexValidator( 16 | COLOR_HEXA_RE, 17 | _("Enter a valid hexa color, eg. #00000000"), 18 | "invalid", 19 | ) 20 | 21 | COLOR_RGB_RE = re.compile( 22 | # prefix and opening parenthesis 23 | r"^rgb\(" 24 | # first number: red channel 25 | r"(\d{1,3})" 26 | # comma and optional space 27 | r",\s?" 28 | # second number: green channel 29 | r"(\d{1,3})" 30 | # comma and optional space 31 | r",\s?" 32 | # third number: blue channel 33 | r"(\d{1,3})" 34 | # closing parenthesis 35 | r"\)$" 36 | ) 37 | color_rgb_validator = RegexValidator( 38 | COLOR_RGB_RE, 39 | _("Enter a valid rgb color, eg. rgb(128, 128, 128)"), 40 | "invalid", 41 | ) 42 | COLOR_RGBA_RE = re.compile( 43 | # prefix and opening parenthesis 44 | r"^rgba\(" 45 | # first number: red channel 46 | r"(\d{1,3})" 47 | # comma and optional space 48 | r",\s?" 49 | # second number: green channel 50 | r"(\d{1,3})" 51 | # comma and optional space 52 | r",\s?" 53 | # third number: blue channel 54 | r"(\d{1,3})" 55 | # comma and optional space 56 | r",\s?" 57 | # alpha channel: decimal number between 0 and 1 58 | r"(0(\.\d{1,2})?|1(\.0)?)" 59 | # closing parenthesis 60 | r"\)$" 61 | ) 62 | color_rgba_validator = RegexValidator( 63 | COLOR_RGBA_RE, 64 | _("Enter a valid rgba color, eg. rgba(128, 128, 128, 0.5)"), 65 | "invalid", 66 | ) 67 | -------------------------------------------------------------------------------- /tests/test_basic_form.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from tests.forms import BasicForm 3 | 4 | 5 | class BasicFormTest(TestCase): 6 | def test_color_field_initial_value(self): 7 | """ 8 | Test that the color field has the correct initial value. 9 | """ 10 | form = BasicForm() 11 | self.assertEqual(form.fields["color"].initial, "#FF0000") 12 | 13 | def test_color_field_valid_data(self): 14 | """ 15 | Test that the form is valid with correct HEX color data. 16 | """ 17 | data = {"color": "#00FF00"} 18 | form = BasicForm(data=data) 19 | self.assertTrue(form.is_valid()) 20 | self.assertEqual(form.cleaned_data["color"], "#00FF00") 21 | 22 | def test_color_field_invalid_data(self): 23 | """ 24 | Test that the form is invalid with incorrect HEX color data. 25 | """ 26 | # Test invalid HEX color (missing #) 27 | data = {"color": "00FF00"} 28 | form = BasicForm(data=data) 29 | self.assertFalse(form.is_valid()) 30 | self.assertIn("color", form.errors) 31 | 32 | # Test invalid HEX color (invalid characters) 33 | data = {"color": "#ZZZZZZ"} 34 | form = BasicForm(data=data) 35 | self.assertFalse(form.is_valid()) 36 | self.assertIn("color", form.errors) 37 | 38 | def test_color_field_empty_data(self): 39 | """ 40 | Test that the form is invalid when the color field is empty. 41 | """ 42 | data = {"color": ""} 43 | form = BasicForm(data=data) 44 | self.assertFalse(form.is_valid()) 45 | self.assertIn("color", form.errors) 46 | 47 | def test_color_field_widget_rendering(self): 48 | """ 49 | Test that the color field widget renders correctly. 50 | """ 51 | form = BasicForm() 52 | rendered_form = form.as_p() 53 | self.assertIn('type="text"', rendered_form) 54 | self.assertIn('value="#FF0000"', rendered_form) 55 | -------------------------------------------------------------------------------- /colorfield/widgets.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.forms import TextInput 3 | from django.template.loader import render_to_string 4 | 5 | from colorfield.utils import get_random_string 6 | 7 | 8 | class ColorWidget(TextInput): 9 | template_name = "colorfield/color.html" 10 | 11 | class Media: 12 | if settings.DEBUG: 13 | css = {"all": ["colorfield/coloris/coloris.css"]} 14 | js = [ 15 | "colorfield/coloris/coloris.js", 16 | "colorfield/colorfield.js", 17 | ] 18 | else: 19 | css = {"all": ["colorfield/coloris/coloris.min.css"]} 20 | js = [ 21 | "colorfield/coloris/coloris.min.js", 22 | "colorfield/colorfield.js", 23 | ] 24 | 25 | def get_context(self, name, value, attrs=None): 26 | context = {} 27 | context.update(self.attrs.copy() or {}) 28 | context.update(attrs or {}) 29 | context.update( 30 | { 31 | "widget": self, 32 | "name": name, 33 | "value": value, 34 | # ensure that there is an id 35 | "data_coloris_id": "coloris-" + context.get("id", get_random_string()), 36 | # data-coloris options 37 | "data_coloris_options": { 38 | "format": context.get("format", "hex"), 39 | "required": context.get("required", False), 40 | "clearButton": not bool(context.get("required")), 41 | "alpha": bool(context.get("alpha")), 42 | "forceAlpha": bool(context.get("alpha")), 43 | "swatches": context.get("swatches", []), 44 | "swatchesOnly": bool(context.get("swatches", [])) 45 | and bool(context.get("swatches_only")), 46 | }, 47 | } 48 | ) 49 | return context 50 | 51 | def render(self, name, value, attrs=None, renderer=None): 52 | return render_to_string( 53 | self.template_name, self.get_context(name, value, attrs) 54 | ) 55 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from colorfield.fields import ColorField 4 | 5 | COLOR_PALETTE = [ 6 | ("#FFFFFF", "white"), 7 | ("#000000", "black"), 8 | ] 9 | 10 | 11 | class Color(models.Model): 12 | color = ColorField(blank=True) 13 | 14 | class Meta: 15 | app_label = "tests" 16 | 17 | 18 | class ColorNull(models.Model): 19 | color = ColorField(null=True) 20 | 21 | class Meta: 22 | app_label = "tests" 23 | 24 | 25 | class ColorChoices(models.Model): 26 | COLOR_CHOICES = COLOR_PALETTE 27 | 28 | color = ColorField(blank=True, choices=COLOR_CHOICES) 29 | 30 | class Meta: 31 | app_label = "tests" 32 | 33 | 34 | class ColorSamples(models.Model): 35 | COLOR_SAMPLES = COLOR_PALETTE 36 | 37 | color = ColorField(blank=True, samples=COLOR_SAMPLES) 38 | 39 | class Meta: 40 | app_label = "tests" 41 | 42 | 43 | class ColorNoImageField(models.Model): 44 | color = ColorField(image_field="image") 45 | 46 | class Meta: 47 | app_label = "tests" 48 | 49 | 50 | class ColorInvalidImageField(models.Model): 51 | image = models.CharField(blank=True, max_length=10) 52 | color = ColorField(image_field="image") 53 | 54 | class Meta: 55 | app_label = "tests" 56 | 57 | 58 | class ColorImageField(models.Model): 59 | image = models.ImageField(blank=True, upload_to="temp") 60 | color = ColorField(image_field="image") 61 | 62 | class Meta: 63 | app_label = "tests" 64 | 65 | 66 | class ColorImageFieldAndDefault(models.Model): 67 | image = models.ImageField(blank=True, upload_to="temp") 68 | color = ColorField(image_field="image", default="#FF0000") 69 | 70 | class Meta: 71 | app_label = "tests" 72 | 73 | 74 | class ColorImageFieldAndFormat(models.Model): 75 | image = models.ImageField(blank=True, upload_to="temp") 76 | color = ColorField(image_field="image", format="hexa") 77 | 78 | class Meta: 79 | app_label = "tests" 80 | 81 | 82 | class ColorFieldRGBFormat(models.Model): 83 | color_rgb = ColorField(format="rgb") 84 | color_rgba = ColorField(format="rgba") 85 | 86 | class Meta: 87 | app_label = "tests" 88 | 89 | 90 | class ColorImageFieldRGBFormat(models.Model): 91 | image = models.ImageField(blank=True, upload_to="temp") 92 | color_rgb = ColorField(image_field="image", format="rgb") 93 | color_rgba = ColorField(image_field="image", format="rgba") 94 | 95 | class Meta: 96 | app_label = "tests" 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-colorfield" 7 | description = "color field for django models with a nice color-picker in the admin." 8 | authors = [ 9 | { name = "Jared Forsyth", email = "jared@jaredforsyth.com" }, 10 | { name = "Fabio Caccamo", email = "fabio.caccamo@gmail.com" }, 11 | ] 12 | keywords = [ 13 | "django", 14 | "colorfield", 15 | "colorpicker", 16 | "color", 17 | "field", 18 | "picker", 19 | "chooser", 20 | "admin", 21 | "python", 22 | ] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Environment :: Web Environment", 26 | "Framework :: Django", 27 | "Framework :: Django :: 4.0", 28 | "Framework :: Django :: 4.1", 29 | "Framework :: Django :: 4.2", 30 | "Framework :: Django :: 5.0", 31 | "Framework :: Django :: 5.1", 32 | "Framework :: Django :: 5.2", 33 | "Framework :: Django :: 6.0", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: MIT License", 36 | "Natural Language :: English", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.10", 40 | "Programming Language :: Python :: 3.11", 41 | "Programming Language :: Python :: 3.12", 42 | "Programming Language :: Python :: 3.13", 43 | "Programming Language :: Python :: 3.14", 44 | "Topic :: Software Development :: Build Tools", 45 | ] 46 | dependencies = [ 47 | "Pillow (>= 9.0.0)", 48 | ] 49 | dynamic = ["version"] 50 | maintainers = [ 51 | { name = "Fabio Caccamo", email = "fabio.caccamo@gmail.com" }, 52 | ] 53 | 54 | [project.readme] 55 | file = "README.md" 56 | content-type = "text/markdown" 57 | 58 | [project.license] 59 | file = "LICENSE.txt" 60 | content-type = "text/plain" 61 | 62 | [project.urls] 63 | Homepage = "https://github.com/fabiocaccamo/django-colorfield" 64 | Download = "https://github.com/fabiocaccamo/django-colorfield/releases" 65 | Documentation = "https://github.com/fabiocaccamo/django-colorfield#readme" 66 | Issues = "https://github.com/fabiocaccamo/django-colorfield/issues" 67 | Funding = "https://github.com/sponsors/fabiocaccamo/" 68 | Twitter = "https://twitter.com/fabiocaccamo" 69 | 70 | [tool.black] 71 | line-length = 88 72 | include = '\.pyi?$' 73 | exclude = ''' 74 | /( 75 | \.git 76 | | \.hg 77 | | \.mypy_cache 78 | | \.tox 79 | | \.venv 80 | | _build 81 | | buck-out 82 | | build 83 | | dist 84 | | venv 85 | )/ 86 | ''' 87 | 88 | [tool.ruff] 89 | line-length = 88 90 | 91 | [tool.ruff.lint] 92 | ignore = [] 93 | select = ["B", "B9", "C", "E", "F", "W"] 94 | 95 | [tool.ruff.lint.mccabe] 96 | max-complexity = 10 97 | 98 | [tool.setuptools.packages.find] 99 | include = ["colorfield*"] 100 | 101 | [tool.setuptools.dynamic.version] 102 | attr = "colorfield.metadata.__version__" 103 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Test package 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | 14 | prepare: 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v6 20 | 21 | - name: Create matrix 22 | uses: fabiocaccamo/create-matrix-action@v6 23 | id: create_matrix 24 | with: 25 | matrix: | 26 | python-version {3.10}, django-version {4.0, 4.1, 4.2, 5.0, 5.1, 5.2} 27 | python-version {3.11}, django-version {4.1, 4.2, 5.0, 5.1, 5.2} 28 | python-version {3.12}, django-version {4.2, 5.0, 5.1, 5.2, 6.0} 29 | python-version {3.13}, django-version {5.1, 5.2, 6.0} 30 | python-version {3.14}, django-version {5.2, 6.0} 31 | 32 | outputs: 33 | matrix: ${{ steps.create_matrix.outputs.matrix }} 34 | 35 | lint: 36 | 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v6 41 | 42 | - name: Set up Python 43 | uses: actions/setup-python@v6 44 | with: 45 | python-version: '3.x' 46 | cache: 'pip' 47 | 48 | - name: Install tools needed 49 | run: | 50 | sudo apt install gettext 51 | python -m pip install --upgrade pip 52 | pip install tox 53 | 54 | - name: Check migrations 55 | run: | 56 | tox -e migrations 57 | 58 | - name: Check translations 59 | run: | 60 | tox -e translations 61 | 62 | test: 63 | 64 | needs: prepare 65 | runs-on: ubuntu-latest 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | include: ${{fromJson(needs.prepare.outputs.matrix)}} 70 | 71 | steps: 72 | 73 | - uses: actions/checkout@v6 74 | 75 | - name: Set up Python ${{ matrix.python-version }} 76 | uses: actions/setup-python@v6 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | cache: 'pip' 80 | 81 | - name: Upgrade pip version 82 | run: | 83 | python -m pip install --upgrade pip 84 | 85 | - name: Install django 86 | run: | 87 | pip install "Django == ${{ matrix.django-version }}.*" 88 | 89 | - name: Install requirements 90 | run: | 91 | pip install -r requirements.txt 92 | pip install -r requirements-test.txt 93 | 94 | - name: Run pre-commit 95 | run: | 96 | pre-commit run --all-files --show-diff-on-failure --verbose 97 | 98 | - name: Run tests 99 | run: | 100 | coverage run --append --source=colorfield runtests.py 101 | coverage report --show-missing 102 | coverage xml -o ./coverage.xml 103 | 104 | - name: Upload coverage to Codecov 105 | uses: codecov/codecov-action@v5 106 | with: 107 | token: ${{ secrets.CODECOV_TOKEN }} 108 | fail_ci_if_error: false 109 | files: ./coverage.xml 110 | flags: unittests 111 | verbose: true 112 | -------------------------------------------------------------------------------- /tests/test_widgets.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | 5 | from colorfield.widgets import ColorWidget 6 | from tests.models import COLOR_PALETTE 7 | 8 | 9 | CHOICES = [choice[0] for choice in COLOR_PALETTE] 10 | 11 | 12 | class ColorWidgetTestCase(TestCase): 13 | def test_basic(self): 14 | widget = ColorWidget(attrs={"id": "id_color"}) 15 | 16 | expected = { 17 | "format": "hex", 18 | "required": False, 19 | "clearButton": True, 20 | "alpha": False, 21 | "forceAlpha": False, 22 | "swatches": [], 23 | "swatchesOnly": False, 24 | } 25 | 26 | context = widget.get_context("color", "#FFFFFF") 27 | self.assertIn("data_coloris_id", context) 28 | self.assertIsNotNone(context["data_coloris_id"]) 29 | self.assertIn("data_coloris_options", context) 30 | self.assertDictEqual(context["data_coloris_options"], expected) 31 | 32 | text = widget.render("color", "#FFFFFF") 33 | self.assertIn('= 3.8`. 45 | - Set max line length to `88`. 46 | - Switch from `setup.py` to `pyproject.toml`. 47 | - Replace `flake8` with `Ruff`. 48 | - Add locales (`en` and `it`). 49 | - Add `metadata` module. 50 | - Set max line length to `88`. 51 | - Run `pre-commit` also with `tox`. 52 | - Bump requirements. 53 | - Pin test requirements. 54 | - Rename default branch from `master` to `main`. 55 | 56 | ## [0.8.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.8.0) - 2022-12-02 57 | - Drop `Python < 3.8` and `Django < 2.2` support. 58 | 59 | ## [0.7.3](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.7.3) - 2022-12-02 60 | - Handle possible corrupted image when opening image. #98 61 | 62 | ## [0.7.2](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.7.2) - 2022-07-19 63 | - Fixed options not working when not using `palette` (choices/samples). #80 (by [@jan-szejko-steelseries](https://github.com/jan-szejko-steelseries) in #81) 64 | 65 | ## [0.7.1](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.7.1) - 2022-06-08 66 | - Fixed `ColorField` widget classes. #43 #78 (thanks to [@N1K1TAS95](https://github.com/N1K1TAS95)) 67 | 68 | ## [0.7.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.7.0) - 2022-05-13 69 | - Added `ColorField` serializer. #77 (thanks to [@hugofer93](https://github.com/hugofer93)) 70 | 71 | ## [0.6.3](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.6.3) - 2022-01-03 72 | - Fixed django < 2.0 compatibility. 73 | 74 | ## [0.6.2](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.6.2) - 2022-01-03 75 | - Fixed possible memory leak. 76 | 77 | ## [0.6.1](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.6.1) - 2022-01-03 78 | - Fixed `ValueError: seek of closed file`. #75 79 | 80 | ## [0.6.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.6.0) - 2021-12-22 81 | - Added `image_field` option. 82 | - Added `python 3.10` / `django 4.0` support. 83 | - Added more tests and increased coverage. 84 | - Fixed tests warnings. 85 | - Replaced Travis CI with GitHub actions workflow. 86 | 87 | ## [0.5.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.5.0) - 2021-12-06 88 | - Added `samples` option support. 89 | 90 | ## [0.4.5](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.5) - 2021-10-12 91 | - Fixed widget backward-compatibility with older django versions. 92 | 93 | ## [0.4.4](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.4) - 2021-10-08 94 | - Fixed widget backward-compatibility with older django versions. 95 | 96 | ## [0.4.3](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.3) - 2021-09-16 97 | - Fixed subclasses of `forms.Widget` must provide a `render()` method. #70 98 | 99 | ## [0.4.2](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.2) - 2021-07-12 100 | - Fixed disable colorfield in `ModelForm` (thanks to [@rcatajar](https://github.com/rcatajar)). #67 #69 101 | 102 | ## [0.4.1](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.1) - 2021-01-19 103 | - Fixed 500 error caused by palette `choices`. #65 104 | 105 | ## [0.4.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.4.0) - 2021-01-14 106 | - Added `hex` (default) and `hexa` color format support. #58 #59 107 | - Added palette support using field `choices`. #19 108 | - Updated `jscolor` library version to `2.4.5`. 109 | 110 | ## [0.3.2](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.3.2) - 2020-07-07 111 | - Used `load` event instead of `window.onload` callback. 112 | 113 | ## [0.3.1](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.3.1) - 2020-06-17 114 | - Updated jscolor to 2.1.1 version. #57 115 | - Fixed self invoking anonymous function expression. 116 | 117 | ## [0.3.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.3.0) - 2020-04-07 118 | - Fixed `default`, `blank` and `null` attrs support. #53 #54 119 | - Fixed `jscolor` not working on inlines added dynamically (only when `extra=0`) 120 | 121 | ## [0.2.2](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.2.2) - 2020-04-02 122 | - Fixed colopicker not working on inlines added dynamically (only when `jquery` is loaded by the browser after `colorfield`). #52 123 | 124 | ## [0.2.1](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.2.1) - 2020-02-21 125 | - Fixed colopicker not working on inlines added dynamically. 126 | - Fixed failed lookup for key [class]. #7 127 | 128 | ## [0.2.0](https://github.com/fabiocaccamo/django-colorfield/releases/tag/0.2.0) - 2020-02-17 129 | - Fixed whole inline model required. #7 130 | - Fixed `README.md` missing in package. #46 131 | - Refactored `ColorField` and `ColorWidget`. #39, #43 132 | - Updated `jscolor` version to `2.0.5`. 133 | - Bumped min `django` version to `1.7`. 134 | - Added test suite *(not tests)* with `tox` and `travis`. 135 | 136 | ## 0.1.16 137 | - Remove warnings about `ugettext_lazy` usage. 138 | 139 | ## 0.1.13 140 | - Use not minified jscolor when `DEBUG=true` 141 | - Fix rendering when `value is None` 142 | - Use `{required: false}` js color option when the form field is not required. This forces to stop using the `{hash: true}` option for jscolor. To make this change retrocompatible, anchor is appended at the `Widget` level. 143 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.core.exceptions import ImproperlyConfigured, ValidationError 6 | from django.core.files import File 7 | from django.forms import fields_for_model 8 | from django.test import TestCase 9 | 10 | from colorfield.fields import ColorField 11 | from tests.models import ( 12 | COLOR_PALETTE, 13 | Color, 14 | ColorChoices, 15 | ColorFieldRGBFormat, 16 | ColorImageField, 17 | ColorImageFieldAndDefault, 18 | ColorImageFieldAndFormat, 19 | ColorImageFieldRGBFormat, 20 | ColorInvalidImageField, 21 | ColorNoImageField, 22 | ColorNull, 23 | ColorSamples, 24 | ) 25 | 26 | 27 | class ColorFieldTestCase(TestCase): 28 | def setUp(self): 29 | self.delete_media() 30 | 31 | def tearDown(self): 32 | self.delete_media() 33 | 34 | @classmethod 35 | def delete_media(cls): 36 | path = settings.MEDIA_ROOT 37 | if os.path.exists(path): 38 | shutil.rmtree(path) 39 | 40 | @classmethod 41 | def get_images_input_dir(cls): 42 | return os.path.join(os.path.dirname(os.path.realpath(__file__)), "images") 43 | 44 | @classmethod 45 | def save_image_to_field_from_path(cls, field, path, save=True): 46 | path = os.path.join(cls.get_images_input_dir(), path) 47 | with open(path, "rb") as image: 48 | name = os.path.basename(path) 49 | field.save(name, content=File(image), save=save) 50 | 51 | def test_model_formfield_doesnt_raise(self): 52 | """ 53 | Adding a ColorField to a model should not fail in 2.2LTS. 54 | """ 55 | try: 56 | fields_for_model(Color()) 57 | except AttributeError: 58 | self.fail("Raised Attribute Error") 59 | 60 | def test_model_formfield_with_samples_and_choices_fails(self): 61 | """ 62 | Checks that supplying a ColorField with both samples 63 | and choices options fails (mutually exclusive). 64 | """ 65 | with self.assertRaises(ImproperlyConfigured): 66 | ColorField(choices=COLOR_PALETTE, samples=COLOR_PALETTE) 67 | 68 | def test_clean_field_choices(self): 69 | """ 70 | Checks that supplying a ColorField with the samples kwarg works, 71 | and that it accepts valid values outside the predefined choices. 72 | """ 73 | # 1. Test with predefined choice 74 | obj = ColorChoices() 75 | obj.color = ColorChoices.COLOR_CHOICES[0][0] 76 | try: 77 | obj.full_clean() 78 | except ValidationError as e: 79 | self.fail( 80 | "Failed to assign predefined palette choice " 81 | f"to ColorField model instance. Message: {e}" 82 | ) 83 | 84 | # 2. Test with value outside of the choices 85 | other_value = "#35B6A3" 86 | obj.color = other_value 87 | with self.assertRaises(ValidationError): 88 | obj.full_clean() 89 | 90 | # 3. Test with predefined choice with different case 91 | obj.color = "#fFfFfF" 92 | try: 93 | obj.full_clean() 94 | except ValidationError as e: 95 | self.fail( 96 | "Failed to assign predefined palette choice " 97 | f"to ColorField model instance. Message: {e}" 98 | ) 99 | 100 | def test_clean_field_samples(self): 101 | """ 102 | Checks that supplying a ColorField with the samples kwarg works, 103 | and that it accepts valid values outside the predefined choices. 104 | """ 105 | # 1. Test with predefined choice 106 | obj = ColorSamples() 107 | obj.color = ColorSamples.COLOR_SAMPLES[0][0] 108 | try: 109 | obj.full_clean() 110 | except ValidationError as e: 111 | self.fail( 112 | "Failed to assign predefined palette choice " 113 | f"to ColorField model instance. Message: {e}" 114 | ) 115 | 116 | # 2. Test with value outside of the choices 117 | other_value = "#35B6A3" 118 | obj.color = other_value 119 | try: 120 | obj.full_clean() 121 | except ValidationError as e: 122 | self.fail( 123 | "Failed to assign value outside palette choices " 124 | f"to ColorField model instance. Message: {e}" 125 | ) 126 | 127 | def test_model_with_null(self): 128 | obj = ColorNull() 129 | obj.save() 130 | self.assertEqual(obj.color, None) 131 | 132 | def test_model_with_invalid_image_field_type(self): 133 | obj = ColorInvalidImageField() 134 | with self.assertRaises(ImproperlyConfigured): 135 | obj.save() 136 | 137 | def test_model_with_not_existing_image_field(self): 138 | obj = ColorNoImageField() 139 | with self.assertRaises(ImproperlyConfigured): 140 | obj.save() 141 | 142 | def test_model_with_image_field_empty(self): 143 | obj = ColorImageField() 144 | obj.save() 145 | self.assertEqual(obj.color, "") 146 | 147 | def test_model_with_image_field_empty_and_default(self): 148 | obj = ColorImageFieldAndDefault() 149 | obj.save() 150 | self.assertEqual(obj.color, "#FF0000") 151 | 152 | def test_model_with_image(self): 153 | model_class = ColorImageField 154 | obj = model_class() 155 | filename = "django.png" 156 | self.save_image_to_field_from_path(obj.image, filename) 157 | obj.save() 158 | # ensure the image has been saved correctly 159 | self.assertTrue(obj.image.path.endswith(filename)) 160 | expected_color = "#082D20" 161 | # check in-memory value 162 | self.assertEqual(obj.color, expected_color) 163 | # check stored value 164 | obj_saved = model_class.objects.get(pk=obj.pk) 165 | self.assertEqual(obj_saved.color, expected_color) 166 | 167 | def test_model_with_image_and_format(self): 168 | model_class = ColorImageFieldAndFormat 169 | obj = model_class() 170 | filename = "django.png" 171 | self.save_image_to_field_from_path(obj.image, filename) 172 | obj.save() 173 | # ensure the image has been saved correctly 174 | self.assertTrue(obj.image.path.endswith(filename)) 175 | expected_color = "#082D20FF" 176 | # check in-memory value 177 | self.assertEqual(obj.color, expected_color) 178 | # check stored value 179 | obj_saved = model_class.objects.get(pk=obj.pk) 180 | self.assertEqual(obj_saved.color, expected_color) 181 | 182 | def test_model_rgb_formats(self): 183 | obj = ColorFieldRGBFormat( 184 | color_rgb="rgb(123, 123, 123)", 185 | color_rgba="rgba(128, 199, 255, 0.55)", 186 | ) 187 | obj.save() 188 | # check in-memory values 189 | self.assertEqual(obj.color_rgb, "rgb(123, 123, 123)") 190 | self.assertEqual(obj.color_rgba, "rgba(128, 199, 255, 0.55)") 191 | # check stored value 192 | obj_saved = ColorFieldRGBFormat.objects.get(pk=obj.pk) 193 | self.assertEqual(obj_saved.color_rgb, "rgb(123, 123, 123)") 194 | self.assertEqual(obj_saved.color_rgba, "rgba(128, 199, 255, 0.55)") 195 | 196 | def test_model_with_image_rgb_format(self): 197 | obj = ColorImageFieldRGBFormat() 198 | filename = "django.png" 199 | self.save_image_to_field_from_path(obj.image, filename) 200 | obj.save() 201 | # ensure the image has been saved correctly 202 | self.assertTrue(obj.image.path.endswith(filename)) 203 | # check in-memory value 204 | self.assertEqual(obj.color_rgb, "rgb(8, 45, 32)") 205 | self.assertEqual(obj.color_rgba, "rgba(8, 45, 32, 1.0)") 206 | # check stored value 207 | obj_saved = ColorImageFieldRGBFormat.objects.get(pk=obj.pk) 208 | self.assertEqual(obj_saved.color_rgb, "rgb(8, 45, 32)") 209 | self.assertEqual(obj_saved.color_rgba, "rgba(8, 45, 32, 1.0)") 210 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/coloris/coloris.min.css: -------------------------------------------------------------------------------- 1 | .clr-picker{display:none;flex-wrap:wrap;position:absolute;width:200px;z-index:1000;border-radius:10px;background-color:#fff;justify-content:flex-end;direction:ltr;box-shadow:0 0 5px rgba(0,0,0,.05),0 5px 20px rgba(0,0,0,.1);-moz-user-select:none;-webkit-user-select:none;user-select:none}.clr-picker.clr-open,.clr-picker[data-inline=true]{display:flex}.clr-picker[data-inline=true]{position:relative}.clr-gradient{position:relative;width:100%;height:100px;margin-bottom:15px;border-radius:3px 3px 0 0;background-image:linear-gradient(rgba(0,0,0,0),#000),linear-gradient(90deg,#fff,currentColor);cursor:pointer}.clr-marker{position:absolute;width:12px;height:12px;margin:-6px 0 0 -6px;border:1px solid #fff;border-radius:50%;background-color:currentColor;cursor:pointer}.clr-picker input[type=range]::-webkit-slider-runnable-track{width:100%;height:16px}.clr-picker input[type=range]::-webkit-slider-thumb{width:16px;height:16px;-webkit-appearance:none}.clr-picker input[type=range]::-moz-range-track{width:100%;height:16px;border:0}.clr-picker input[type=range]::-moz-range-thumb{width:16px;height:16px;border:0}.clr-hue{background-image:linear-gradient(to right,red 0,#ff0 16.66%,#0f0 33.33%,#0ff 50%,#00f 66.66%,#f0f 83.33%,red 100%)}.clr-alpha,.clr-hue{position:relative;width:calc(100% - 40px);height:8px;margin:5px 20px;border-radius:4px}.clr-alpha span{display:block;height:100%;width:100%;border-radius:inherit;background-image:linear-gradient(90deg,rgba(0,0,0,0),currentColor)}.clr-alpha input[type=range],.clr-hue input[type=range]{position:absolute;width:calc(100% + 32px);height:16px;left:-16px;top:-4px;margin:0;background-color:transparent;opacity:0;cursor:pointer;appearance:none;-webkit-appearance:none}.clr-alpha div,.clr-hue div{position:absolute;width:16px;height:16px;left:0;top:50%;margin-left:-8px;transform:translateY(-50%);border:2px solid #fff;border-radius:50%;background-color:currentColor;box-shadow:0 0 1px #888;pointer-events:none}.clr-alpha div:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border-radius:50%;background-color:currentColor}.clr-format{display:none;order:1;width:calc(100% - 40px);margin:0 20px 20px}.clr-segmented{display:flex;position:relative;width:100%;margin:0;padding:0;border:1px solid #ddd;border-radius:15px;box-sizing:border-box;color:#999;font-size:12px}.clr-segmented input,.clr-segmented legend{position:absolute;width:100%;height:100%;margin:0;padding:0;border:0;left:0;top:0;opacity:0;pointer-events:none}.clr-segmented label{flex-grow:1;margin:0;padding:4px 0;font-size:inherit;font-weight:400;line-height:initial;text-align:center;cursor:pointer}.clr-segmented label:first-of-type{border-radius:10px 0 0 10px}.clr-segmented label:last-of-type{border-radius:0 10px 10px 0}.clr-segmented input:checked+label{color:#fff;background-color:#666}.clr-swatches{order:2;width:calc(100% - 32px);margin:0 16px}.clr-swatches div{display:flex;flex-wrap:wrap;padding-bottom:12px;justify-content:center}.clr-swatches button{position:relative;width:20px;height:20px;margin:0 4px 6px 4px;padding:0;border:0;border-radius:50%;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;cursor:pointer}.clr-swatches button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}input.clr-color{order:1;width:calc(100% - 80px);height:32px;margin:15px 20px 20px auto;padding:0 10px;border:1px solid #ddd;border-radius:16px;color:#444;background-color:#fff;font-family:sans-serif;font-size:14px;text-align:center;box-shadow:none}input.clr-color:focus{outline:0;border:1px solid #1e90ff}.clr-clear,.clr-close{display:none;order:2;height:24px;margin:0 20px 20px;padding:0 20px;border:0;border-radius:12px;color:#fff;background-color:#666;font-family:inherit;font-size:12px;font-weight:400;cursor:pointer}.clr-close{display:block;margin:0 20px 20px auto}.clr-preview{position:relative;width:32px;height:32px;margin:15px 0 20px 20px;border-radius:50%;overflow:hidden}.clr-preview:after,.clr-preview:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border:1px solid #fff;border-radius:50%}.clr-preview:after{border:0;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.clr-preview button{position:absolute;width:100%;height:100%;z-index:1;margin:0;padding:0;border:0;border-radius:50%;outline-offset:-2px;background-color:transparent;text-indent:-9999px;cursor:pointer;overflow:hidden}.clr-alpha div,.clr-color,.clr-hue div,.clr-marker{box-sizing:border-box}.clr-field{display:inline-block;position:relative;color:transparent}.clr-field input{margin:0;direction:ltr}.clr-field.clr-rtl input{text-align:right}.clr-field button{position:absolute;width:30px;height:100%;right:0;top:50%;transform:translateY(-50%);margin:0;padding:0;border:0;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;pointer-events:none}.clr-field.clr-rtl button{right:auto;left:0}.clr-field button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 1px rgba(0,0,0,.5)}.clr-alpha,.clr-alpha div,.clr-field button,.clr-preview:before,.clr-swatches button{background-image:repeating-linear-gradient(45deg,#aaa 25%,transparent 25%,transparent 75%,#aaa 75%,#aaa),repeating-linear-gradient(45deg,#aaa 25%,#fff 25%,#fff 75%,#aaa 75%,#aaa);background-position:0 0,4px 4px;background-size:8px 8px}.clr-marker:focus{outline:0}.clr-keyboard-nav .clr-alpha input:focus+div,.clr-keyboard-nav .clr-hue input:focus+div,.clr-keyboard-nav .clr-marker:focus,.clr-keyboard-nav .clr-segmented input:focus+label{outline:0;box-shadow:0 0 0 2px #1e90ff,0 0 2px 2px #fff}.clr-picker[data-alpha=false] .clr-alpha{display:none}.clr-picker[data-minimal=true]{padding-top:16px}.clr-picker[data-minimal=true] .clr-alpha,.clr-picker[data-minimal=true] .clr-color,.clr-picker[data-minimal=true] .clr-gradient,.clr-picker[data-minimal=true] .clr-hue,.clr-picker[data-minimal=true] .clr-preview{display:none}.clr-dark{background-color:#444}.clr-dark .clr-segmented{border-color:#777}.clr-dark .clr-swatches button:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}.clr-dark input.clr-color{color:#fff;border-color:#777;background-color:#555}.clr-dark input.clr-color:focus{border-color:#1e90ff}.clr-dark .clr-preview:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.5)}.clr-dark .clr-alpha,.clr-dark .clr-alpha div,.clr-dark .clr-preview:before,.clr-dark .clr-swatches button{background-image:repeating-linear-gradient(45deg,#666 25%,transparent 25%,transparent 75%,#888 75%,#888),repeating-linear-gradient(45deg,#888 25%,#444 25%,#444 75%,#888 75%,#888)}.clr-picker.clr-polaroid{border-radius:6px;box-shadow:0 0 5px rgba(0,0,0,.1),0 5px 30px rgba(0,0,0,.2)}.clr-picker.clr-polaroid:before{content:'';display:block;position:absolute;width:16px;height:10px;left:20px;top:-10px;border:solid transparent;border-width:0 8px 10px 8px;border-bottom-color:currentColor;box-sizing:border-box;color:#fff;filter:drop-shadow(0 -4px 3px rgba(0,0,0,.1));pointer-events:none}.clr-picker.clr-polaroid.clr-dark:before{color:#444}.clr-picker.clr-polaroid.clr-left:before{left:auto;right:20px}.clr-picker.clr-polaroid.clr-top:before{top:auto;bottom:-10px;transform:rotateZ(180deg)}.clr-polaroid .clr-gradient{width:calc(100% - 20px);height:120px;margin:10px;border-radius:3px}.clr-polaroid .clr-alpha,.clr-polaroid .clr-hue{width:calc(100% - 30px);height:10px;margin:6px 15px;border-radius:5px}.clr-polaroid .clr-alpha div,.clr-polaroid .clr-hue div{box-shadow:0 0 5px rgba(0,0,0,.2)}.clr-polaroid .clr-format{width:calc(100% - 20px);margin:0 10px 15px}.clr-polaroid .clr-swatches{width:calc(100% - 12px);margin:0 6px}.clr-polaroid .clr-swatches div{padding-bottom:10px}.clr-polaroid .clr-swatches button{width:22px;height:22px}.clr-polaroid input.clr-color{width:calc(100% - 60px);margin:10px 10px 15px auto}.clr-polaroid .clr-clear{margin:0 10px 15px 10px}.clr-polaroid .clr-close{margin:0 10px 15px auto}.clr-polaroid .clr-preview{margin:10px 0 15px 10px}.clr-picker.clr-large{width:275px}.clr-large .clr-gradient{height:150px}.clr-large .clr-swatches button{width:22px;height:22px}.clr-picker.clr-pill{width:380px;padding-left:180px;box-sizing:border-box}.clr-pill .clr-gradient{position:absolute;width:180px;height:100%;left:0;top:0;margin-bottom:0;border-radius:3px 0 0 3px}.clr-pill .clr-hue{margin-top:20px} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/pypi/pyversions/django-colorfield.svg?color=3776AB&logo=python&logoColor=white)](https://www.python.org/) 2 | [![](https://img.shields.io/pypi/djversions/django-colorfield?color=0C4B33&logo=django&logoColor=white&label=django)](https://www.djangoproject.com/) 3 | 4 | [![](https://img.shields.io/pypi/v/django-colorfield.svg?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/django-colorfield/) 5 | [![](https://static.pepy.tech/badge/django-colorfield/month)](https://pepy.tech/project/django-colorfield) 6 | [![](https://img.shields.io/github/stars/fabiocaccamo/django-colorfield?logo=github&style=flat)](https://github.com/fabiocaccamo/django-colorfield/stargazers) 7 | [![](https://img.shields.io/pypi/l/django-colorfield.svg?color=blue)](https://github.com/fabiocaccamo/django-colorfield/blob/main/LICENSE.txt) 8 | 9 | [![](https://results.pre-commit.ci/badge/github/fabiocaccamo/django-colorfield/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/django-colorfield/main) 10 | [![](https://img.shields.io/github/actions/workflow/status/fabiocaccamo/django-colorfield/test-package.yml?branch=main&label=build&logo=github)](https://github.com/fabiocaccamo/django-colorfield) 11 | [![](https://img.shields.io/codecov/c/gh/fabiocaccamo/django-colorfield?logo=codecov)](https://codecov.io/gh/fabiocaccamo/django-colorfield) 12 | [![](https://img.shields.io/codacy/grade/194566618f424a819ce43450ea0af081?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/django-colorfield) 13 | [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black) 14 | [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) 15 | 16 | # django-colorfield 17 | simple color field for your models with a nice color-picker in the admin-interface. 18 | 19 | ![django-colorfield-hex](https://user-images.githubusercontent.com/7900305/104512324-51ed0f80-55ee-11eb-9144-de03d922c2ce.png) 20 | ![django-colorfield-hexa](https://user-images.githubusercontent.com/7900305/104512063-ec991e80-55ed-11eb-95b6-9174ac3f4f38.png) 21 | 22 | --- 23 | 24 | ## Installation 25 | - Run `pip install django-colorfield` 26 | - Add `colorfield` to `settings.INSTALLED_APPS` 27 | - Run `python manage.py collectstatic` 28 | - Restart your application server 29 | 30 | --- 31 | 32 | ## Usage 33 | 34 | ### Settings 35 | This package doesn't need any setting. 36 | 37 | ### Models 38 | Just add color field(s) to your models like this: 39 | 40 | ```python 41 | from colorfield.fields import ColorField 42 | from django.db import models 43 | 44 | class MyModel(models.Model): 45 | color = ColorField(default='#FF0000') 46 | ``` 47 | 48 | ### Field Options 49 | These are the supported custom options: [`format`](#format), [`image_field`](#image_field), [`samples`](#samples) 50 | 51 | #### format 52 | 53 | The following formats are supported: `hex` *(default)*, `hexa`, `rgb`, `rgba`. 54 | 55 | ```python 56 | from colorfield.fields import ColorField 57 | from django.db import models 58 | 59 | class MyModel(models.Model): 60 | color = ColorField(format="hexa") 61 | ``` 62 | 63 | #### image_field 64 | 65 | It is possible to auto-populate the field value getting the color from an image using the `image_field` option. 66 | 67 | The color will be calculated from the **top-left pixel** color of the image each time the model instance is saved. 68 | 69 | ```python 70 | from colorfield.fields import ColorField 71 | from django.db import models 72 | 73 | class MyModel(models.Model): 74 | image = models.ImageField(upload_to="images") 75 | color = ColorField(image_field="image") 76 | ``` 77 | 78 | #### samples 79 | 80 | It is possible to provide a palette of colors to choose from to the widget using the `samples` option. 81 | 82 | This option **is not restrictive** (on the contrary of `choices` option), it is also possible to choose another color from the spectrum. 83 | 84 | ![django-colorfield-samples](https://user-images.githubusercontent.com/7900305/104512178-194d3600-55ee-11eb-8cba-91cca156da06.png) 85 | 86 | ```python 87 | from colorfield.fields import ColorField 88 | from django.db import models 89 | 90 | class MyModel(models.Model): 91 | 92 | COLOR_PALETTE = [ 93 | ("#FFFFFF", "white", ), 94 | ("#000000", "black", ), 95 | ] 96 | 97 | # not restrictive, allows the selection of another color from the spectrum. 98 | color = ColorField(samples=COLOR_PALETTE) 99 | 100 | # restrictive, it is mandatory to choose a color from the palette 101 | color = ColorField(choices=COLOR_PALETTE) 102 | ``` 103 | 104 | ### Forms 105 | 106 | #### Model forms 107 | 108 | The `colorfield.fields.ColorField` can be used in Django model forms (`django.forms.ModelForm`) to provide a color picker widget. 109 | When used in a model form, the field automatically validates the color format based on the specified `format` (e.g., `hex`, `rgb`, `rgba`). 110 | 111 | ```python 112 | from django import forms 113 | 114 | class MyModelForm(forms.ModelForm): 115 | class Meta: 116 | model = MyModel 117 | fields = ["color"] 118 | ``` 119 | 120 | #### Plain forms 121 | 122 | The `colorfield.forms.ColorField` can be used in plain Django forms (`django.forms.Form`) to provide a color-picker widget. 123 | This is useful when you need a color input outside of a model context. 124 | 125 | ```python 126 | from django import forms 127 | from colorfield.forms import ColorField 128 | 129 | class MyForm(forms.Form): 130 | color = ColorField(initial="#FF0000", format="hex") 131 | ``` 132 | 133 | ### Admin 134 | The admin will kindly provide a simple [color picker](https://coloris.js.org/) for all color fields. :) 135 | 136 | --- 137 | 138 | ## Testing 139 | ```bash 140 | # clone repository 141 | git clone https://github.com/fabiocaccamo/django-colorfield.git && cd django-colorfield 142 | 143 | # create virtualenv and activate it 144 | python -m venv venv && . venv/bin/activate 145 | 146 | # upgrade pip 147 | python -m pip install --upgrade pip 148 | 149 | # install requirements 150 | pip install -r requirements.txt -r requirements-test.txt 151 | 152 | # install pre-commit to run formatters and linters 153 | pre-commit install --install-hooks 154 | 155 | # run tests 156 | tox 157 | # or 158 | python runtests.py 159 | # or 160 | python -m django test --settings "tests.settings" 161 | ``` 162 | --- 163 | 164 | ## Credits 165 | Originally developed by [Jared Forsyth](https://github.com/jaredly) 166 | 167 | --- 168 | 169 | ## License 170 | Released under [MIT License](LICENSE.txt). 171 | 172 | --- 173 | 174 | ## Supporting 175 | 176 | - :star: Star this project on [GitHub](https://github.com/fabiocaccamo/django-colorfield) 177 | - :octocat: Follow me on [GitHub](https://github.com/fabiocaccamo) 178 | - :blue_heart: Follow me on [Bluesky](https://bsky.app/profile/fabiocaccamo.bsky.social) 179 | - :moneybag: Sponsor me on [Github](https://github.com/sponsors/fabiocaccamo) 180 | 181 | ## See also 182 | 183 | - [`django-admin-interface`](https://github.com/fabiocaccamo/django-admin-interface) - the default admin interface made customizable by the admin itself. popup windows replaced by modals. 🧙 ⚡ 184 | 185 | - [`django-cache-cleaner`](https://github.com/fabiocaccamo/django-cache-cleaner) - clear the entire cache or individual caches easily using the admin panel or management command. 🧹✨ 186 | 187 | - [`django-email-validators`](https://github.com/fabiocaccamo/django-email-validators) - no more invalid or disposable emails in your database. ✉️ ✅ 188 | 189 | - [`django-extra-settings`](https://github.com/fabiocaccamo/django-extra-settings) - config and manage typed extra settings using just the django admin. ⚙️ 190 | 191 | - [`django-maintenance-mode`](https://github.com/fabiocaccamo/django-maintenance-mode) - shows a 503 error page when maintenance-mode is on. 🚧 🛠️ 192 | 193 | - [`django-redirects`](https://github.com/fabiocaccamo/django-redirects) - redirects with full control. ↪️ 194 | 195 | - [`django-treenode`](https://github.com/fabiocaccamo/django-treenode) - probably the best abstract model / admin for your tree based stuff. 🌳 196 | 197 | - [`python-benedict`](https://github.com/fabiocaccamo/python-benedict) - dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, json, pickle, plist, query-string, toml, xml, yaml) and many utilities. 📘 198 | 199 | - [`python-codicefiscale`](https://github.com/fabiocaccamo/python-codicefiscale) - encode/decode Italian fiscal codes - codifica/decodifica del Codice Fiscale. 🇮🇹 💳 200 | 201 | - [`python-fontbro`](https://github.com/fabiocaccamo/python-fontbro) - friendly font operations. 🧢 202 | 203 | - [`python-fsutil`](https://github.com/fabiocaccamo/python-fsutil) - file-system utilities for lazy devs. 🧟‍♂️ 204 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/coloris/coloris.css: -------------------------------------------------------------------------------- 1 | .clr-picker { 2 | display: none; 3 | flex-wrap: wrap; 4 | position: absolute; 5 | width: 200px; 6 | z-index: 1000; 7 | border-radius: 10px; 8 | background-color: #fff; 9 | justify-content: flex-end; 10 | direction: ltr; 11 | box-shadow: 0 0 5px rgba(0,0,0,.05), 0 5px 20px rgba(0,0,0,.1); 12 | -moz-user-select: none; 13 | -webkit-user-select: none; 14 | user-select: none; 15 | } 16 | 17 | .clr-picker.clr-open, 18 | .clr-picker[data-inline="true"] { 19 | display: flex; 20 | } 21 | 22 | .clr-picker[data-inline="true"] { 23 | position: relative; 24 | } 25 | 26 | .clr-gradient { 27 | position: relative; 28 | width: 100%; 29 | height: 100px; 30 | margin-bottom: 15px; 31 | border-radius: 3px 3px 0 0; 32 | background-image: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentColor); 33 | cursor: pointer; 34 | } 35 | 36 | .clr-marker { 37 | position: absolute; 38 | width: 12px; 39 | height: 12px; 40 | margin: -6px 0 0 -6px; 41 | border: 1px solid #fff; 42 | border-radius: 50%; 43 | background-color: currentColor; 44 | cursor: pointer; 45 | } 46 | 47 | .clr-picker input[type="range"]::-webkit-slider-runnable-track { 48 | width: 100%; 49 | height: 16px; 50 | } 51 | 52 | .clr-picker input[type="range"]::-webkit-slider-thumb { 53 | width: 16px; 54 | height: 16px; 55 | -webkit-appearance: none; 56 | } 57 | 58 | .clr-picker input[type="range"]::-moz-range-track { 59 | width: 100%; 60 | height: 16px; 61 | border: 0; 62 | } 63 | 64 | .clr-picker input[type="range"]::-moz-range-thumb { 65 | width: 16px; 66 | height: 16px; 67 | border: 0; 68 | } 69 | 70 | .clr-hue { 71 | background-image: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); 72 | } 73 | 74 | .clr-hue, 75 | .clr-alpha { 76 | position: relative; 77 | width: calc(100% - 40px); 78 | height: 8px; 79 | margin: 5px 20px; 80 | border-radius: 4px; 81 | } 82 | 83 | .clr-alpha span { 84 | display: block; 85 | height: 100%; 86 | width: 100%; 87 | border-radius: inherit; 88 | background-image: linear-gradient(90deg, rgba(0,0,0,0), currentColor); 89 | } 90 | 91 | .clr-hue input[type="range"], 92 | .clr-alpha input[type="range"] { 93 | position: absolute; 94 | width: calc(100% + 32px); 95 | height: 16px; 96 | left: -16px; 97 | top: -4px; 98 | margin: 0; 99 | background-color: transparent; 100 | opacity: 0; 101 | cursor: pointer; 102 | appearance: none; 103 | -webkit-appearance: none; 104 | } 105 | 106 | .clr-hue div, 107 | .clr-alpha div { 108 | position: absolute; 109 | width: 16px; 110 | height: 16px; 111 | left: 0; 112 | top: 50%; 113 | margin-left: -8px; 114 | transform: translateY(-50%); 115 | border: 2px solid #fff; 116 | border-radius: 50%; 117 | background-color: currentColor; 118 | box-shadow: 0 0 1px #888; 119 | pointer-events: none; 120 | } 121 | 122 | .clr-alpha div:before { 123 | content: ''; 124 | position: absolute; 125 | height: 100%; 126 | width: 100%; 127 | left: 0; 128 | top: 0; 129 | border-radius: 50%; 130 | background-color: currentColor; 131 | } 132 | 133 | .clr-format { 134 | display: none; 135 | order: 1; 136 | width: calc(100% - 40px); 137 | margin: 0 20px 20px; 138 | } 139 | 140 | .clr-segmented { 141 | display: flex; 142 | position: relative; 143 | width: 100%; 144 | margin: 0; 145 | padding: 0; 146 | border: 1px solid #ddd; 147 | border-radius: 15px; 148 | box-sizing: border-box; 149 | color: #999; 150 | font-size: 12px; 151 | } 152 | 153 | .clr-segmented input, 154 | .clr-segmented legend { 155 | position: absolute; 156 | width: 100%; 157 | height: 100%; 158 | margin: 0; 159 | padding: 0; 160 | border: 0; 161 | left: 0; 162 | top: 0; 163 | opacity: 0; 164 | pointer-events: none; 165 | } 166 | 167 | .clr-segmented label { 168 | flex-grow: 1; 169 | margin: 0; 170 | padding: 4px 0; 171 | font-size: inherit; 172 | font-weight: normal; 173 | line-height: initial; 174 | text-align: center; 175 | cursor: pointer; 176 | } 177 | 178 | .clr-segmented label:first-of-type { 179 | border-radius: 10px 0 0 10px; 180 | } 181 | 182 | .clr-segmented label:last-of-type { 183 | border-radius: 0 10px 10px 0; 184 | } 185 | 186 | .clr-segmented input:checked + label { 187 | color: #fff; 188 | background-color: #666; 189 | } 190 | 191 | .clr-swatches { 192 | order: 2; 193 | width: calc(100% - 32px); 194 | margin: 0 16px; 195 | } 196 | 197 | .clr-swatches div { 198 | display: flex; 199 | flex-wrap: wrap; 200 | padding-bottom: 12px; 201 | justify-content: center; 202 | } 203 | 204 | .clr-swatches button { 205 | position: relative; 206 | width: 20px; 207 | height: 20px; 208 | margin: 0 4px 6px 4px; 209 | padding: 0; 210 | border: 0; 211 | border-radius: 50%; 212 | color: inherit; 213 | text-indent: -1000px; 214 | white-space: nowrap; 215 | overflow: hidden; 216 | cursor: pointer; 217 | } 218 | 219 | .clr-swatches button:after { 220 | content: ''; 221 | display: block; 222 | position: absolute; 223 | width: 100%; 224 | height: 100%; 225 | left: 0; 226 | top: 0; 227 | border-radius: inherit; 228 | background-color: currentColor; 229 | box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); 230 | } 231 | 232 | input.clr-color { 233 | order: 1; 234 | width: calc(100% - 80px); 235 | height: 32px; 236 | margin: 15px 20px 20px auto; 237 | padding: 0 10px; 238 | border: 1px solid #ddd; 239 | border-radius: 16px; 240 | color: #444; 241 | background-color: #fff; 242 | font-family: sans-serif; 243 | font-size: 14px; 244 | text-align: center; 245 | box-shadow: none; 246 | } 247 | 248 | input.clr-color:focus { 249 | outline: none; 250 | border: 1px solid #1e90ff; 251 | } 252 | 253 | .clr-close, 254 | .clr-clear { 255 | display: none; 256 | order: 2; 257 | height: 24px; 258 | margin: 0 20px 20px; 259 | padding: 0 20px; 260 | border: 0; 261 | border-radius: 12px; 262 | color: #fff; 263 | background-color: #666; 264 | font-family: inherit; 265 | font-size: 12px; 266 | font-weight: 400; 267 | cursor: pointer; 268 | } 269 | 270 | .clr-close { 271 | display: block; 272 | margin: 0 20px 20px auto; 273 | } 274 | 275 | .clr-preview { 276 | position: relative; 277 | width: 32px; 278 | height: 32px; 279 | margin: 15px 0 20px 20px; 280 | border-radius: 50%; 281 | overflow: hidden; 282 | } 283 | 284 | .clr-preview:before, 285 | .clr-preview:after { 286 | content: ''; 287 | position: absolute; 288 | height: 100%; 289 | width: 100%; 290 | left: 0; 291 | top: 0; 292 | border: 1px solid #fff; 293 | border-radius: 50%; 294 | } 295 | 296 | .clr-preview:after { 297 | border: 0; 298 | background-color: currentColor; 299 | box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); 300 | } 301 | 302 | .clr-preview button { 303 | position: absolute; 304 | width: 100%; 305 | height: 100%; 306 | z-index: 1; 307 | margin: 0; 308 | padding: 0; 309 | border: 0; 310 | border-radius: 50%; 311 | outline-offset: -2px; 312 | background-color: transparent; 313 | text-indent: -9999px; 314 | cursor: pointer; 315 | overflow: hidden; 316 | } 317 | 318 | .clr-marker, 319 | .clr-hue div, 320 | .clr-alpha div, 321 | .clr-color { 322 | box-sizing: border-box; 323 | } 324 | 325 | .clr-field { 326 | display: inline-block; 327 | position: relative; 328 | color: transparent; 329 | } 330 | 331 | .clr-field input { 332 | margin: 0; 333 | direction: ltr; 334 | } 335 | 336 | .clr-field.clr-rtl input { 337 | text-align: right; 338 | } 339 | 340 | .clr-field button { 341 | position: absolute; 342 | width: 30px; 343 | height: 100%; 344 | right: 0; 345 | top: 50%; 346 | transform: translateY(-50%); 347 | margin: 0; 348 | padding: 0; 349 | border: 0; 350 | color: inherit; 351 | text-indent: -1000px; 352 | white-space: nowrap; 353 | overflow: hidden; 354 | pointer-events: none; 355 | } 356 | 357 | .clr-field.clr-rtl button { 358 | right: auto; 359 | left: 0; 360 | } 361 | 362 | .clr-field button:after { 363 | content: ''; 364 | display: block; 365 | position: absolute; 366 | width: 100%; 367 | height: 100%; 368 | left: 0; 369 | top: 0; 370 | border-radius: inherit; 371 | background-color: currentColor; 372 | box-shadow: inset 0 0 1px rgba(0,0,0,.5); 373 | } 374 | 375 | .clr-alpha, 376 | .clr-alpha div, 377 | .clr-swatches button, 378 | .clr-preview:before, 379 | .clr-field button { 380 | background-image: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); 381 | background-position: 0 0, 4px 4px; 382 | background-size: 8px 8px; 383 | } 384 | 385 | .clr-marker:focus { 386 | outline: none; 387 | } 388 | 389 | .clr-keyboard-nav .clr-marker:focus, 390 | .clr-keyboard-nav .clr-hue input:focus + div, 391 | .clr-keyboard-nav .clr-alpha input:focus + div, 392 | .clr-keyboard-nav .clr-segmented input:focus + label { 393 | outline: none; 394 | box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; 395 | } 396 | 397 | .clr-picker[data-alpha="false"] .clr-alpha { 398 | display: none; 399 | } 400 | 401 | .clr-picker[data-minimal="true"] { 402 | padding-top: 16px; 403 | } 404 | 405 | .clr-picker[data-minimal="true"] .clr-gradient, 406 | .clr-picker[data-minimal="true"] .clr-hue, 407 | .clr-picker[data-minimal="true"] .clr-alpha, 408 | .clr-picker[data-minimal="true"] .clr-color, 409 | .clr-picker[data-minimal="true"] .clr-preview { 410 | display: none; 411 | } 412 | 413 | /** Dark theme **/ 414 | 415 | .clr-dark { 416 | background-color: #444; 417 | } 418 | 419 | .clr-dark .clr-segmented { 420 | border-color: #777; 421 | } 422 | 423 | .clr-dark .clr-swatches button:after { 424 | box-shadow: inset 0 0 0 1px rgba(255,255,255,.3); 425 | } 426 | 427 | .clr-dark input.clr-color { 428 | color: #fff; 429 | border-color: #777; 430 | background-color: #555; 431 | } 432 | 433 | .clr-dark input.clr-color:focus { 434 | border-color: #1e90ff; 435 | } 436 | 437 | .clr-dark .clr-preview:after { 438 | box-shadow: inset 0 0 0 1px rgba(255,255,255,.5); 439 | } 440 | 441 | .clr-dark .clr-alpha, 442 | .clr-dark .clr-alpha div, 443 | .clr-dark .clr-swatches button, 444 | .clr-dark .clr-preview:before { 445 | background-image: repeating-linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #888 75%, #888), repeating-linear-gradient(45deg, #888 25%, #444 25%, #444 75%, #888 75%, #888); 446 | } 447 | 448 | /** Polaroid theme **/ 449 | 450 | .clr-picker.clr-polaroid { 451 | border-radius: 6px; 452 | box-shadow: 0 0 5px rgba(0,0,0,.1), 0 5px 30px rgba(0,0,0,.2); 453 | } 454 | 455 | .clr-picker.clr-polaroid:before { 456 | content: ''; 457 | display: block; 458 | position: absolute; 459 | width: 16px; 460 | height: 10px; 461 | left: 20px; 462 | top: -10px; 463 | border: solid transparent; 464 | border-width: 0 8px 10px 8px; 465 | border-bottom-color: currentColor; 466 | box-sizing: border-box; 467 | color: #fff; 468 | filter: drop-shadow(0 -4px 3px rgba(0,0,0,.1)); 469 | pointer-events: none; 470 | } 471 | 472 | .clr-picker.clr-polaroid.clr-dark:before { 473 | color: #444; 474 | } 475 | 476 | .clr-picker.clr-polaroid.clr-left:before { 477 | left: auto; 478 | right: 20px; 479 | } 480 | 481 | .clr-picker.clr-polaroid.clr-top:before { 482 | top: auto; 483 | bottom: -10px; 484 | transform: rotateZ(180deg); 485 | } 486 | 487 | .clr-polaroid .clr-gradient { 488 | width: calc(100% - 20px); 489 | height: 120px; 490 | margin: 10px; 491 | border-radius: 3px; 492 | } 493 | 494 | .clr-polaroid .clr-hue, 495 | .clr-polaroid .clr-alpha { 496 | width: calc(100% - 30px); 497 | height: 10px; 498 | margin: 6px 15px; 499 | border-radius: 5px; 500 | } 501 | 502 | .clr-polaroid .clr-hue div, 503 | .clr-polaroid .clr-alpha div { 504 | box-shadow: 0 0 5px rgba(0,0,0,.2); 505 | } 506 | 507 | .clr-polaroid .clr-format { 508 | width: calc(100% - 20px); 509 | margin: 0 10px 15px; 510 | } 511 | 512 | .clr-polaroid .clr-swatches { 513 | width: calc(100% - 12px); 514 | margin: 0 6px; 515 | } 516 | .clr-polaroid .clr-swatches div { 517 | padding-bottom: 10px; 518 | } 519 | 520 | .clr-polaroid .clr-swatches button { 521 | width: 22px; 522 | height: 22px; 523 | } 524 | 525 | .clr-polaroid input.clr-color { 526 | width: calc(100% - 60px); 527 | margin: 10px 10px 15px auto; 528 | } 529 | 530 | .clr-polaroid .clr-clear { 531 | margin: 0 10px 15px 10px; 532 | } 533 | 534 | .clr-polaroid .clr-close { 535 | margin: 0 10px 15px auto; 536 | } 537 | 538 | .clr-polaroid .clr-preview { 539 | margin: 10px 0 15px 10px; 540 | } 541 | 542 | /** Large theme **/ 543 | 544 | .clr-picker.clr-large { 545 | width: 275px; 546 | } 547 | 548 | .clr-large .clr-gradient { 549 | height: 150px; 550 | } 551 | 552 | .clr-large .clr-swatches button { 553 | width: 22px; 554 | height: 22px; 555 | } 556 | 557 | /** Pill (horizontal) theme **/ 558 | 559 | .clr-picker.clr-pill { 560 | width: 380px; 561 | padding-left: 180px; 562 | box-sizing: border-box; 563 | } 564 | 565 | .clr-pill .clr-gradient { 566 | position: absolute; 567 | width: 180px; 568 | height: 100%; 569 | left: 0; 570 | top: 0; 571 | margin-bottom: 0; 572 | border-radius: 3px 0 0 3px; 573 | } 574 | 575 | .clr-pill .clr-hue { 576 | margin-top: 20px; 577 | } 578 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/coloris/coloris.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Momo Bassit. 3 | * Licensed under the MIT License (MIT) 4 | * https://github.com/mdbassit/Coloris 5 | */ 6 | !function(u,p,s,c){var d,f,h,i,b,y,v,m,g,l,w,k,L,E,a,n,r=p.createElement("canvas").getContext("2d"),x={r:0,g:0,b:0,h:0,s:0,v:0,a:1},A={},C={el:"[data-coloris]",parent:"body",theme:"default",themeMode:"light",rtl:!1,wrap:!0,margin:2,format:"hex",formatToggle:!1,swatches:[],swatchesOnly:!1,alpha:!0,forceAlpha:!1,focusInput:!0,selectInput:!1,inline:!1,defaultColor:"#000000",clearButton:!1,clearLabel:"Clear",closeButton:!1,closeLabel:"Close",onChange:function(){return c},a11y:{open:"Open color picker",close:"Close color picker",clear:"Clear the selected color",marker:"Saturation: {s}. Brightness: {v}.",hueSlider:"Hue slider",alphaSlider:"Opacity slider",input:"Color value field",format:"Color format",swatch:"Color swatch",instruction:"Saturation and brightness selector. Use up, down, left and right arrow keys to select."}},o={},S="",T={},B=!1;function M(t){if("object"==typeof t)for(var e in t)switch(e){case"el":D(t.el),!1!==t.wrap&&R(t.el);break;case"parent":(d=t.parent instanceof HTMLElement?t.parent:p.querySelector(t.parent))&&(d.appendChild(f),C.parent=t.parent,d===p.body&&(d=c));break;case"themeMode":C.themeMode=t.themeMode,"auto"===t.themeMode&&u.matchMedia&&u.matchMedia("(prefers-color-scheme: dark)").matches&&(C.themeMode="dark");case"theme":t.theme&&(C.theme=t.theme),f.className="clr-picker clr-"+C.theme+" clr-"+C.themeMode,C.inline&&j();break;case"rtl":C.rtl=!!t.rtl,Array.from(p.getElementsByClassName("clr-field")).forEach(function(e){return e.classList.toggle("clr-rtl",C.rtl)});break;case"margin":t.margin*=1,C.margin=(isNaN(t.margin)?C:t).margin;break;case"wrap":t.el&&t.wrap&&R(t.el);break;case"formatToggle":C.formatToggle=!!t.formatToggle,V("clr-format").style.display=C.formatToggle?"block":"none",C.formatToggle&&(C.format="auto");break;case"swatches":Array.isArray(t.swatches)&&function(){var e=V("clr-swatches"),l=p.createElement("div");e.textContent="",t.swatches.forEach(function(e,t){var a=p.createElement("button");a.setAttribute("type","button"),a.setAttribute("id","clr-swatch-"+t),a.setAttribute("aria-labelledby","clr-swatch-label clr-swatch-"+t),a.style.color=e,a.textContent=e,l.appendChild(a)}),t.swatches.length&&e.appendChild(l),C.swatches=t.swatches.slice()}();break;case"swatchesOnly":C.swatchesOnly=!!t.swatchesOnly,f.setAttribute("data-minimal",C.swatchesOnly);break;case"alpha":C.alpha=!!t.alpha,f.setAttribute("data-alpha",C.alpha);break;case"inline":C.inline=!!t.inline,f.setAttribute("data-inline",C.inline),C.inline&&(l=t.defaultColor||C.defaultColor,E=P(l),j(),Y(l));break;case"clearButton":"object"==typeof t.clearButton&&(t.clearButton.label&&(C.clearLabel=t.clearButton.label,v.innerHTML=C.clearLabel),t.clearButton=t.clearButton.show),C.clearButton=!!t.clearButton,v.style.display=C.clearButton?"block":"none";break;case"clearLabel":C.clearLabel=t.clearLabel,v.innerHTML=C.clearLabel;break;case"closeButton":C.closeButton=!!t.closeButton,C.closeButton?f.insertBefore(m,b):b.appendChild(m);break;case"closeLabel":C.closeLabel=t.closeLabel,m.innerHTML=C.closeLabel;break;case"a11y":var a,l,r=t.a11y,n=!1;if("object"==typeof r)for(var o in r)r[o]&&C.a11y[o]&&(C.a11y[o]=r[o],n=!0);n&&(a=V("clr-open-label"),l=V("clr-swatch-label"),a.innerHTML=C.a11y.open,l.innerHTML=C.a11y.swatch,m.setAttribute("aria-label",C.a11y.close),v.setAttribute("aria-label",C.a11y.clear),g.setAttribute("aria-label",C.a11y.hueSlider),w.setAttribute("aria-label",C.a11y.alphaSlider),y.setAttribute("aria-label",C.a11y.input),h.setAttribute("aria-label",C.a11y.instruction));break;default:C[e]=t[e]}}function H(e,t){"string"==typeof e&&"object"==typeof t&&(o[e]=t,B=!0)}function N(e){delete o[e],0===Object.keys(o).length&&(B=!1,e===S&&O())}function t(l){if(B){var e,r=["el","wrap","rtl","inline","defaultColor","a11y"];for(e in o)if("break"===function(e){var t=o[e];if(l.matches(e)){for(var a in S=e,T={},r.forEach(function(e){return delete t[e]}),t)T[a]=Array.isArray(C[a])?C[a].slice():C[a];return M(t),"break"}}(e))break}}function O(){0r.clientWidth&&(a+=t.width-o,i.left=!0),l+c>r.clientHeight-e&&c+C.margin<=t.top-(s.y-n)&&(l-=t.height+c+2*C.margin,i.top=!0),l+=r.scrollTop):(a+o>p.documentElement.clientWidth&&(a+=t.width-o,i.left=!0),l+c-n>p.documentElement.clientHeight&&c+C.margin<=t.top&&(l=n+t.y-c-C.margin,i.top=!0)),f.classList.toggle("clr-left",i.left),f.classList.toggle("clr-top",i.top),f.style.left=a+"px",f.style.top=l+"px",s.x+=f.offsetLeft,s.y+=f.offsetTop),A={width:h.offsetWidth,height:h.offsetHeight,x:h.offsetLeft+s.x,y:h.offsetTop+s.y}}function R(e){e instanceof HTMLElement?W(e):(Array.isArray(e)?e:p.querySelectorAll(e)).forEach(W)}function W(e){var t,a,l=e.parentNode;l.classList.contains("clr-field")||(t=p.createElement("div"),a="clr-field",(C.rtl||e.classList.contains("clr-rtl"))&&(a+=" clr-rtl"),t.innerHTML='',l.insertBefore(t,e),t.className=a,t.style.color=e.value,t.appendChild(e))}function q(e){var t=e.target.parentNode;t.classList.contains("clr-field")&&(t.style.color=e.target.value)}function F(e){var t;L&&!C.inline&&(t=L,e&&(L=c,a!==t.value&&(t.value=a,t.dispatchEvent(new Event("input",{bubbles:!0})))),setTimeout(function(){a!==t.value&&t.dispatchEvent(new Event("change",{bubbles:!0}))}),f.classList.remove("clr-open"),B&&O(),t.dispatchEvent(new Event("close",{bubbles:!0})),C.focusInput&&t.focus({preventScroll:!0}),L=c)}function Y(e){var t=function(e){r.fillStyle="#000",r.fillStyle=e,e=(e=/^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i.exec(r.fillStyle))?{r:+e[3],g:+e[4],b:+e[5],a:+e[6]}:(e=r.fillStyle.replace("#","").match(/.{2}/g).map(function(e){return parseInt(e,16)}),{r:e[0],g:e[1],b:e[2],a:1});return e}(e),e=function(e){var t=e.r/255,a=e.g/255,l=e.b/255,r=s.max(t,a,l),n=s.min(t,a,l),o=r-n,c=r,i=0,n=0;o&&(r===t&&(i=(a-l)/o),r===a&&(i=2+(l-t)/o),r===l&&(i=4+(t-a)/o),r&&(n=o/r));return{h:(i=s.floor(60*i))<0?i+360:i,s:s.round(100*n),v:s.round(100*c),a:e.a}}(t);G(e.s,e.v),z(t,e),g.value=e.h,f.style.color="hsl("+e.h+", 100%, 50%)",l.style.left=e.h/360*100+"%",i.style.left=A.width*e.s/100+"px",i.style.top=A.height-A.height*e.v/100+"px",w.value=100*e.a,k.style.left=100*e.a+"%"}function P(e){e=e.substring(0,3).toLowerCase();return"rgb"===e||"hsl"===e?e:"hex"}function U(e){e=e!==c?e:y.value,L&&(L.value=e,L.dispatchEvent(new Event("input",{bubbles:!0}))),C.onChange&&C.onChange.call(u,e,L),p.dispatchEvent(new CustomEvent("coloris:pick",{detail:{color:e,currentEl:L}}))}function X(e,t){e={h:+g.value,s:e/A.width*100,v:100-t/A.height*100,a:w.value/100},t=function(e){var t=e.s/100,a=e.v/100,l=t*a,r=e.h/60,n=l*(1-s.abs(r%2-1)),o=a-l;l+=o,n+=o;t=s.floor(r)%6,a=[l,n,o,o,n,l][t],r=[n,l,l,n,o,o][t],t=[o,o,n,l,l,n][t];return{r:s.round(255*a),g:s.round(255*r),b:s.round(255*t),a:e.a}}(e);G(e.s,e.v),z(t,e),U()}function G(e,t){var a=C.a11y.marker;e=+e.toFixed(1),t=+t.toFixed(1),a=(a=a.replace("{s}",e)).replace("{v}",t),i.setAttribute("aria-label",a)}function K(e){var t={pageX:((a=e).changedTouches?a.changedTouches[0]:a).pageX,pageY:(a.changedTouches?a.changedTouches[0]:a).pageY},a=t.pageX-A.x,t=t.pageY-A.y;d&&(t+=d.scrollTop),$(a,t),e.preventDefault(),e.stopPropagation()}function $(e,t){e=e<0?0:e>A.width?A.width:e,t=t<0?0:t>A.height?A.height:t,i.style.left=e+"px",i.style.top=t+"px",X(e,t),i.focus()}function z(e,t){void 0===t&&(t={});var a,l,r=C.format;for(a in e=void 0===e?{}:e)x[a]=e[a];for(l in t)x[l]=t[l];var n,o=function(e){var t=e.r.toString(16),a=e.g.toString(16),l=e.b.toString(16),r="";e.r<16&&(t="0"+t);e.g<16&&(a="0"+a);e.b<16&&(l="0"+l);C.alpha&&(e.a<1||C.forceAlpha)&&(e=255*e.a|0,r=e.toString(16),e<16&&(r="0"+r));return"#"+t+a+l+r}(x),c=o.substring(0,7);switch(i.style.color=c,k.parentNode.style.color=c,k.style.color=o,b.style.color=o,h.style.display="none",h.offsetHeight,h.style.display="",k.nextElementSibling.style.display="none",k.nextElementSibling.offsetHeight,k.nextElementSibling.style.display="","mixed"===r?r=1===x.a?"hex":"rgb":"auto"===r&&(r=E),r){case"hex":y.value=o;break;case"rgb":y.value=(n=x,!C.alpha||1===n.a&&!C.forceAlpha?"rgb("+n.r+", "+n.g+", "+n.b+")":"rgba("+n.r+", "+n.g+", "+n.b+", "+n.a+")");break;case"hsl":y.value=(n=function(e){var t,a=e.v/100,l=a*(1-e.s/100/2);0
'+C.a11y.format+'
",p.body.appendChild(f),h=V("clr-color-area"),i=V("clr-color-marker"),v=V("clr-clear"),m=V("clr-close"),b=V("clr-color-preview"),y=V("clr-color-value"),g=V("clr-hue-slider"),l=V("clr-hue-marker"),w=V("clr-alpha-slider"),k=V("clr-alpha-marker"),D(C.el),R(C.el),Z(f,"mousedown",function(e){f.classList.remove("clr-keyboard-nav"),e.stopPropagation()}),Z(h,"mousedown",function(e){Z(p,"mousemove",K)}),Z(h,"contextmenu",function(e){e.preventDefault()}),Z(h,"touchstart",function(e){p.addEventListener("touchmove",K,{passive:!1})}),Z(i,"mousedown",function(e){Z(p,"mousemove",K)}),Z(i,"touchstart",function(e){p.addEventListener("touchmove",K,{passive:!1})}),Z(y,"change",function(e){var t=y.value;(L||C.inline)&&U(""===t?t:Y(t))}),Z(v,"click",function(e){U(""),F()}),Z(m,"click",function(e){U(),F()}),Z(V("clr-format"),"click",".clr-format input",function(e){E=e.target.value,z(),U()}),Z(f,"click",".clr-swatches button",function(e){Y(e.target.textContent),U(),C.swatchesOnly&&F()}),Z(p,"mouseup",function(e){p.removeEventListener("mousemove",K)}),Z(p,"touchend",function(e){p.removeEventListener("touchmove",K)}),Z(p,"mousedown",function(e){n=!1,f.classList.remove("clr-keyboard-nav"),F()}),Z(p,"keydown",function(e){var t,a=e.key,l=e.target,r=e.shiftKey;"Escape"===a?F(!0):["Tab","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(a)&&(n=!0,f.classList.add("clr-keyboard-nav")),"Tab"===a&&l.matches(".clr-picker *")&&(a=(t=Q()).shift(),t=t.pop(),r&&l===a?(t.focus(),e.preventDefault()):r||l!==t||(a.focus(),e.preventDefault()))}),Z(p,"click",".clr-field button",function(e){B&&O(),e.target.nextElementSibling.dispatchEvent(new Event("click",{bubbles:!0}))}),Z(i,"keydown",function(e){var t={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]};Object.keys(t).includes(e.key)&&(!function(e,t){$(+i.style.left.replace("px","")+e,+i.style.top.replace("px","")+t)}.apply(void 0,t[e.key]),e.preventDefault())}),Z(h,"click",K),Z(g,"input",e),Z(w,"input",J)})}(window,document,Math); 7 | -------------------------------------------------------------------------------- /colorfield/static/colorfield/coloris/coloris.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Momo Bassit. 3 | * Licensed under the MIT License (MIT) 4 | * https://github.com/mdbassit/Coloris 5 | */ 6 | 7 | (function (window, document, Math, undefined) { 8 | var ctx = document.createElement('canvas').getContext('2d'); 9 | var currentColor = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0, a: 1 }; 10 | var container,picker,colorArea,colorMarker,colorPreview,colorValue,clearButton,closeButton, 11 | hueSlider,hueMarker,alphaSlider,alphaMarker,currentEl,currentFormat,oldColor,keyboardNav, 12 | colorAreaDims = {}; 13 | 14 | // Default settings 15 | var settings = { 16 | el: '[data-coloris]', 17 | parent: 'body', 18 | theme: 'default', 19 | themeMode: 'light', 20 | rtl: false, 21 | wrap: true, 22 | margin: 2, 23 | format: 'hex', 24 | formatToggle: false, 25 | swatches: [], 26 | swatchesOnly: false, 27 | alpha: true, 28 | forceAlpha: false, 29 | focusInput: true, 30 | selectInput: false, 31 | inline: false, 32 | defaultColor: '#000000', 33 | clearButton: false, 34 | clearLabel: 'Clear', 35 | closeButton: false, 36 | closeLabel: 'Close', 37 | onChange: function onChange() {return undefined;}, 38 | a11y: { 39 | open: 'Open color picker', 40 | close: 'Close color picker', 41 | clear: 'Clear the selected color', 42 | marker: 'Saturation: {s}. Brightness: {v}.', 43 | hueSlider: 'Hue slider', 44 | alphaSlider: 'Opacity slider', 45 | input: 'Color value field', 46 | format: 'Color format', 47 | swatch: 'Color swatch', 48 | instruction: 'Saturation and brightness selector. Use up, down, left and right arrow keys to select.' } }; 49 | 50 | 51 | 52 | // Virtual instances cache 53 | var instances = {}; 54 | var currentInstanceId = ''; 55 | var defaultInstance = {}; 56 | var hasInstance = false; 57 | 58 | /** 59 | * Configure the color picker. 60 | * @param {object} options Configuration options. 61 | */ 62 | function configure(options) { 63 | if (typeof options !== 'object') { 64 | return; 65 | } 66 | 67 | for (var key in options) { 68 | switch (key) { 69 | case 'el': 70 | bindFields(options.el); 71 | if (options.wrap !== false) { 72 | wrapFields(options.el); 73 | } 74 | break; 75 | case 'parent': 76 | container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent); 77 | if (container) { 78 | container.appendChild(picker); 79 | settings.parent = options.parent; 80 | 81 | // document.body is special 82 | if (container === document.body) { 83 | container = undefined; 84 | } 85 | } 86 | break; 87 | case 'themeMode': 88 | settings.themeMode = options.themeMode; 89 | if (options.themeMode === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 90 | settings.themeMode = 'dark'; 91 | } 92 | // The lack of a break statement is intentional 93 | case 'theme': 94 | if (options.theme) { 95 | settings.theme = options.theme; 96 | } 97 | 98 | // Set the theme and color scheme 99 | picker.className = "clr-picker clr-" + settings.theme + " clr-" + settings.themeMode; 100 | 101 | // Update the color picker's position if inline mode is in use 102 | if (settings.inline) { 103 | updatePickerPosition(); 104 | } 105 | break; 106 | case 'rtl': 107 | settings.rtl = !!options.rtl; 108 | Array.from(document.getElementsByClassName('clr-field')).forEach(function (field) {return field.classList.toggle('clr-rtl', settings.rtl);}); 109 | break; 110 | case 'margin': 111 | options.margin *= 1; 112 | settings.margin = !isNaN(options.margin) ? options.margin : settings.margin; 113 | break; 114 | case 'wrap': 115 | if (options.el && options.wrap) { 116 | wrapFields(options.el); 117 | } 118 | break; 119 | case 'formatToggle': 120 | settings.formatToggle = !!options.formatToggle; 121 | getEl('clr-format').style.display = settings.formatToggle ? 'block' : 'none'; 122 | if (settings.formatToggle) { 123 | settings.format = 'auto'; 124 | } 125 | break; 126 | case 'swatches': 127 | if (Array.isArray(options.swatches)) {(function () { 128 | var swatchesContainer = getEl('clr-swatches'); 129 | var swatches = document.createElement('div'); 130 | 131 | // Clear current swatches 132 | swatchesContainer.textContent = ''; 133 | 134 | // Build new swatches 135 | options.swatches.forEach(function (swatch, i) { 136 | var button = document.createElement('button'); 137 | 138 | button.setAttribute('type', "button"); 139 | button.setAttribute('id', "clr-swatch-" + i); 140 | button.setAttribute('aria-labelledby', "clr-swatch-label clr-swatch-" + i); 141 | button.style.color = swatch; 142 | button.textContent = swatch; 143 | 144 | swatches.appendChild(button); 145 | }); 146 | 147 | // Append new swatches if any 148 | if (options.swatches.length) { 149 | swatchesContainer.appendChild(swatches); 150 | } 151 | 152 | settings.swatches = options.swatches.slice();})(); 153 | } 154 | break; 155 | case 'swatchesOnly': 156 | settings.swatchesOnly = !!options.swatchesOnly; 157 | picker.setAttribute('data-minimal', settings.swatchesOnly); 158 | break; 159 | case 'alpha': 160 | settings.alpha = !!options.alpha; 161 | picker.setAttribute('data-alpha', settings.alpha); 162 | break; 163 | case 'inline': 164 | settings.inline = !!options.inline; 165 | picker.setAttribute('data-inline', settings.inline); 166 | 167 | if (settings.inline) { 168 | var defaultColor = options.defaultColor || settings.defaultColor; 169 | 170 | currentFormat = getColorFormatFromStr(defaultColor); 171 | updatePickerPosition(); 172 | setColorFromStr(defaultColor); 173 | } 174 | break; 175 | case 'clearButton': 176 | // Backward compatibility 177 | if (typeof options.clearButton === 'object') { 178 | if (options.clearButton.label) { 179 | settings.clearLabel = options.clearButton.label; 180 | clearButton.innerHTML = settings.clearLabel; 181 | } 182 | 183 | options.clearButton = options.clearButton.show; 184 | } 185 | 186 | settings.clearButton = !!options.clearButton; 187 | clearButton.style.display = settings.clearButton ? 'block' : 'none'; 188 | break; 189 | case 'clearLabel': 190 | settings.clearLabel = options.clearLabel; 191 | clearButton.innerHTML = settings.clearLabel; 192 | break; 193 | case 'closeButton': 194 | settings.closeButton = !!options.closeButton; 195 | 196 | if (settings.closeButton) { 197 | picker.insertBefore(closeButton, colorPreview); 198 | } else { 199 | colorPreview.appendChild(closeButton); 200 | } 201 | 202 | break; 203 | case 'closeLabel': 204 | settings.closeLabel = options.closeLabel; 205 | closeButton.innerHTML = settings.closeLabel; 206 | break; 207 | case 'a11y': 208 | var labels = options.a11y; 209 | var update = false; 210 | 211 | if (typeof labels === 'object') { 212 | for (var label in labels) { 213 | if (labels[label] && settings.a11y[label]) { 214 | settings.a11y[label] = labels[label]; 215 | update = true; 216 | } 217 | } 218 | } 219 | 220 | if (update) { 221 | var openLabel = getEl('clr-open-label'); 222 | var swatchLabel = getEl('clr-swatch-label'); 223 | 224 | openLabel.innerHTML = settings.a11y.open; 225 | swatchLabel.innerHTML = settings.a11y.swatch; 226 | closeButton.setAttribute('aria-label', settings.a11y.close); 227 | clearButton.setAttribute('aria-label', settings.a11y.clear); 228 | hueSlider.setAttribute('aria-label', settings.a11y.hueSlider); 229 | alphaSlider.setAttribute('aria-label', settings.a11y.alphaSlider); 230 | colorValue.setAttribute('aria-label', settings.a11y.input); 231 | colorArea.setAttribute('aria-label', settings.a11y.instruction); 232 | } 233 | break; 234 | default: 235 | settings[key] = options[key];} 236 | 237 | } 238 | } 239 | 240 | /** 241 | * Add or update a virtual instance. 242 | * @param {String} selector The CSS selector of the elements to which the instance is attached. 243 | * @param {Object} options Per-instance options to apply. 244 | */ 245 | function setVirtualInstance(selector, options) { 246 | if (typeof selector === 'string' && typeof options === 'object') { 247 | instances[selector] = options; 248 | hasInstance = true; 249 | } 250 | } 251 | 252 | /** 253 | * Remove a virtual instance. 254 | * @param {String} selector The CSS selector of the elements to which the instance is attached. 255 | */ 256 | function removeVirtualInstance(selector) { 257 | delete instances[selector]; 258 | 259 | if (Object.keys(instances).length === 0) { 260 | hasInstance = false; 261 | 262 | if (selector === currentInstanceId) { 263 | resetVirtualInstance(); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Attach a virtual instance to an element if it matches a selector. 270 | * @param {Object} element Target element that will receive a virtual instance if applicable. 271 | */ 272 | function attachVirtualInstance(element) { 273 | if (hasInstance) { 274 | // These options can only be set globally, not per instance 275 | var unsupportedOptions = ['el', 'wrap', 'rtl', 'inline', 'defaultColor', 'a11y'];var _loop = function _loop( 276 | 277 | selector) { 278 | var options = instances[selector]; 279 | 280 | // If the element matches an instance's CSS selector 281 | if (element.matches(selector)) { 282 | currentInstanceId = selector; 283 | defaultInstance = {}; 284 | 285 | // Delete unsupported options 286 | unsupportedOptions.forEach(function (option) {return delete options[option];}); 287 | 288 | // Back up the default options so we can restore them later 289 | for (var option in options) { 290 | defaultInstance[option] = Array.isArray(settings[option]) ? settings[option].slice() : settings[option]; 291 | } 292 | 293 | // Set the instance's options 294 | configure(options); 295 | return "break"; 296 | }};for (var selector in instances) {var _ret = _loop(selector);if (_ret === "break") break; 297 | } 298 | } 299 | } 300 | 301 | /** 302 | * Revert any per-instance options that were previously applied. 303 | */ 304 | function resetVirtualInstance() { 305 | if (Object.keys(defaultInstance).length > 0) { 306 | configure(defaultInstance); 307 | currentInstanceId = ''; 308 | defaultInstance = {}; 309 | } 310 | } 311 | 312 | /** 313 | * Bind the color picker to input fields that match the selector. 314 | * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. 315 | */ 316 | function bindFields(selector) { 317 | if (selector instanceof HTMLElement) { 318 | selector = [selector]; 319 | } 320 | 321 | if (Array.isArray(selector)) { 322 | selector.forEach(function (field) { 323 | addListener(field, 'click', openPicker); 324 | addListener(field, 'input', updateColorPreview); 325 | }); 326 | } else { 327 | addListener(document, 'click', selector, openPicker); 328 | addListener(document, 'input', selector, updateColorPreview); 329 | } 330 | } 331 | 332 | /** 333 | * Open the color picker. 334 | * @param {object} event The event that opens the color picker. 335 | */ 336 | function openPicker(event) { 337 | // Skip if inline mode is in use 338 | if (settings.inline) { 339 | return; 340 | } 341 | 342 | // Apply any per-instance options first 343 | attachVirtualInstance(event.target); 344 | 345 | currentEl = event.target; 346 | oldColor = currentEl.value; 347 | currentFormat = getColorFormatFromStr(oldColor); 348 | picker.classList.add('clr-open'); 349 | 350 | updatePickerPosition(); 351 | setColorFromStr(oldColor); 352 | 353 | if (settings.focusInput || settings.selectInput) { 354 | colorValue.focus({ preventScroll: true }); 355 | colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd); 356 | } 357 | 358 | if (settings.selectInput) { 359 | colorValue.select(); 360 | } 361 | 362 | // Always focus the first element when using keyboard navigation 363 | if (keyboardNav || settings.swatchesOnly) { 364 | getFocusableElements().shift().focus(); 365 | } 366 | 367 | // Trigger an "open" event 368 | currentEl.dispatchEvent(new Event('open', { bubbles: true })); 369 | } 370 | 371 | /** 372 | * Update the color picker's position and the color gradient's offset 373 | */ 374 | function updatePickerPosition() { 375 | var parent = container; 376 | var scrollY = window.scrollY; 377 | var pickerWidth = picker.offsetWidth; 378 | var pickerHeight = picker.offsetHeight; 379 | var reposition = { left: false, top: false }; 380 | var parentStyle, parentMarginTop, parentBorderTop; 381 | var offset = { x: 0, y: 0 }; 382 | 383 | if (parent) { 384 | parentStyle = window.getComputedStyle(parent); 385 | parentMarginTop = parseFloat(parentStyle.marginTop); 386 | parentBorderTop = parseFloat(parentStyle.borderTopWidth); 387 | 388 | offset = parent.getBoundingClientRect(); 389 | offset.y += parentBorderTop + scrollY; 390 | } 391 | 392 | if (!settings.inline) { 393 | var coords = currentEl.getBoundingClientRect(); 394 | var left = coords.x; 395 | var top = scrollY + coords.y + coords.height + settings.margin; 396 | 397 | // If the color picker is inside a custom container 398 | // set the position relative to it 399 | if (parent) { 400 | left -= offset.x; 401 | top -= offset.y; 402 | 403 | if (left + pickerWidth > parent.clientWidth) { 404 | left += coords.width - pickerWidth; 405 | reposition.left = true; 406 | } 407 | 408 | if (top + pickerHeight > parent.clientHeight - parentMarginTop) { 409 | if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) { 410 | top -= coords.height + pickerHeight + settings.margin * 2; 411 | reposition.top = true; 412 | } 413 | } 414 | 415 | top += parent.scrollTop; 416 | 417 | // Otherwise set the position relative to the whole document 418 | } else { 419 | if (left + pickerWidth > document.documentElement.clientWidth) { 420 | left += coords.width - pickerWidth; 421 | reposition.left = true; 422 | } 423 | 424 | if (top + pickerHeight - scrollY > document.documentElement.clientHeight) { 425 | if (pickerHeight + settings.margin <= coords.top) { 426 | top = scrollY + coords.y - pickerHeight - settings.margin; 427 | reposition.top = true; 428 | } 429 | } 430 | } 431 | 432 | picker.classList.toggle('clr-left', reposition.left); 433 | picker.classList.toggle('clr-top', reposition.top); 434 | picker.style.left = left + "px"; 435 | picker.style.top = top + "px"; 436 | offset.x += picker.offsetLeft; 437 | offset.y += picker.offsetTop; 438 | } 439 | 440 | colorAreaDims = { 441 | width: colorArea.offsetWidth, 442 | height: colorArea.offsetHeight, 443 | x: colorArea.offsetLeft + offset.x, 444 | y: colorArea.offsetTop + offset.y }; 445 | 446 | } 447 | 448 | /** 449 | * Wrap the linked input fields in a div that adds a color preview. 450 | * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. 451 | */ 452 | function wrapFields(selector) { 453 | if (selector instanceof HTMLElement) { 454 | wrapColorField(selector); 455 | } else if (Array.isArray(selector)) { 456 | selector.forEach(wrapColorField); 457 | } else { 458 | document.querySelectorAll(selector).forEach(wrapColorField); 459 | } 460 | } 461 | 462 | /** 463 | * Wrap an input field in a div that adds a color preview. 464 | * @param {object} field The input field. 465 | */ 466 | function wrapColorField(field) { 467 | var parentNode = field.parentNode; 468 | 469 | if (!parentNode.classList.contains('clr-field')) { 470 | var wrapper = document.createElement('div'); 471 | var classes = 'clr-field'; 472 | 473 | if (settings.rtl || field.classList.contains('clr-rtl')) { 474 | classes += ' clr-rtl'; 475 | } 476 | 477 | wrapper.innerHTML = ''; 478 | parentNode.insertBefore(wrapper, field); 479 | wrapper.className = classes; 480 | wrapper.style.color = field.value; 481 | wrapper.appendChild(field); 482 | } 483 | } 484 | 485 | /** 486 | * Update the color preview of an input field 487 | * @param {object} event The "input" event that triggers the color change. 488 | */ 489 | function updateColorPreview(event) { 490 | var parent = event.target.parentNode; 491 | 492 | // Only update the preview if the field has been previously wrapped 493 | if (parent.classList.contains('clr-field')) { 494 | parent.style.color = event.target.value; 495 | } 496 | } 497 | 498 | /** 499 | * Close the color picker. 500 | * @param {boolean} [revert] If true, revert the color to the original value. 501 | */ 502 | function closePicker(revert) { 503 | if (currentEl && !settings.inline) { 504 | var prevEl = currentEl; 505 | 506 | // Revert the color to the original value if needed 507 | if (revert) { 508 | // This will prevent the "change" event on the colorValue input to execute its handler 509 | currentEl = undefined; 510 | 511 | if (oldColor !== prevEl.value) { 512 | prevEl.value = oldColor; 513 | 514 | // Trigger an "input" event to force update the thumbnail next to the input field 515 | prevEl.dispatchEvent(new Event('input', { bubbles: true })); 516 | } 517 | } 518 | 519 | // Trigger a "change" event if needed 520 | setTimeout(function () {// Add this to the end of the event loop 521 | if (oldColor !== prevEl.value) { 522 | prevEl.dispatchEvent(new Event('change', { bubbles: true })); 523 | } 524 | }); 525 | 526 | // Hide the picker dialog 527 | picker.classList.remove('clr-open'); 528 | 529 | // Reset any previously set per-instance options 530 | if (hasInstance) { 531 | resetVirtualInstance(); 532 | } 533 | 534 | // Trigger a "close" event 535 | prevEl.dispatchEvent(new Event('close', { bubbles: true })); 536 | 537 | if (settings.focusInput) { 538 | prevEl.focus({ preventScroll: true }); 539 | } 540 | 541 | // This essentially marks the picker as closed 542 | currentEl = undefined; 543 | } 544 | } 545 | 546 | /** 547 | * Set the active color from a string. 548 | * @param {string} str String representing a color. 549 | */ 550 | function setColorFromStr(str) { 551 | var rgba = strToRGBA(str); 552 | var hsva = RGBAtoHSVA(rgba); 553 | 554 | updateMarkerA11yLabel(hsva.s, hsva.v); 555 | updateColor(rgba, hsva); 556 | 557 | // Update the UI 558 | hueSlider.value = hsva.h; 559 | picker.style.color = "hsl(" + hsva.h + ", 100%, 50%)"; 560 | hueMarker.style.left = hsva.h / 360 * 100 + "%"; 561 | 562 | colorMarker.style.left = colorAreaDims.width * hsva.s / 100 + "px"; 563 | colorMarker.style.top = colorAreaDims.height - colorAreaDims.height * hsva.v / 100 + "px"; 564 | 565 | alphaSlider.value = hsva.a * 100; 566 | alphaMarker.style.left = hsva.a * 100 + "%"; 567 | } 568 | 569 | /** 570 | * Guess the color format from a string. 571 | * @param {string} str String representing a color. 572 | * @return {string} The color format. 573 | */ 574 | function getColorFormatFromStr(str) { 575 | var format = str.substring(0, 3).toLowerCase(); 576 | 577 | if (format === 'rgb' || format === 'hsl') { 578 | return format; 579 | } 580 | 581 | return 'hex'; 582 | } 583 | 584 | /** 585 | * Copy the active color to the linked input field. 586 | * @param {number} [color] Color value to override the active color. 587 | */ 588 | function pickColor(color) { 589 | color = color !== undefined ? color : colorValue.value; 590 | 591 | if (currentEl) { 592 | currentEl.value = color; 593 | currentEl.dispatchEvent(new Event('input', { bubbles: true })); 594 | } 595 | 596 | if (settings.onChange) { 597 | settings.onChange.call(window, color, currentEl); 598 | } 599 | 600 | document.dispatchEvent(new CustomEvent('coloris:pick', { detail: { color: color, currentEl: currentEl } })); 601 | } 602 | 603 | /** 604 | * Set the active color based on a specific point in the color gradient. 605 | * @param {number} x Left position. 606 | * @param {number} y Top position. 607 | */ 608 | function setColorAtPosition(x, y) { 609 | var hsva = { 610 | h: hueSlider.value * 1, 611 | s: x / colorAreaDims.width * 100, 612 | v: 100 - y / colorAreaDims.height * 100, 613 | a: alphaSlider.value / 100 }; 614 | 615 | var rgba = HSVAtoRGBA(hsva); 616 | 617 | updateMarkerA11yLabel(hsva.s, hsva.v); 618 | updateColor(rgba, hsva); 619 | pickColor(); 620 | } 621 | 622 | /** 623 | * Update the color marker's accessibility label. 624 | * @param {number} saturation 625 | * @param {number} value 626 | */ 627 | function updateMarkerA11yLabel(saturation, value) { 628 | var label = settings.a11y.marker; 629 | 630 | saturation = saturation.toFixed(1) * 1; 631 | value = value.toFixed(1) * 1; 632 | label = label.replace('{s}', saturation); 633 | label = label.replace('{v}', value); 634 | colorMarker.setAttribute('aria-label', label); 635 | } 636 | 637 | // 638 | /** 639 | * Get the pageX and pageY positions of the pointer. 640 | * @param {object} event The MouseEvent or TouchEvent object. 641 | * @return {object} The pageX and pageY positions. 642 | */ 643 | function getPointerPosition(event) { 644 | return { 645 | pageX: event.changedTouches ? event.changedTouches[0].pageX : event.pageX, 646 | pageY: event.changedTouches ? event.changedTouches[0].pageY : event.pageY }; 647 | 648 | } 649 | 650 | /** 651 | * Move the color marker when dragged. 652 | * @param {object} event The MouseEvent object. 653 | */ 654 | function moveMarker(event) { 655 | var pointer = getPointerPosition(event); 656 | var x = pointer.pageX - colorAreaDims.x; 657 | var y = pointer.pageY - colorAreaDims.y; 658 | 659 | if (container) { 660 | y += container.scrollTop; 661 | } 662 | 663 | setMarkerPosition(x, y); 664 | 665 | // Prevent scrolling while dragging the marker 666 | event.preventDefault(); 667 | event.stopPropagation(); 668 | } 669 | 670 | /** 671 | * Move the color marker when the arrow keys are pressed. 672 | * @param {number} offsetX The horizontal amount to move. 673 | * @param {number} offsetY The vertical amount to move. 674 | */ 675 | function moveMarkerOnKeydown(offsetX, offsetY) { 676 | var x = colorMarker.style.left.replace('px', '') * 1 + offsetX; 677 | var y = colorMarker.style.top.replace('px', '') * 1 + offsetY; 678 | 679 | setMarkerPosition(x, y); 680 | } 681 | 682 | /** 683 | * Set the color marker's position. 684 | * @param {number} x Left position. 685 | * @param {number} y Top position. 686 | */ 687 | function setMarkerPosition(x, y) { 688 | // Make sure the marker doesn't go out of bounds 689 | x = x < 0 ? 0 : x > colorAreaDims.width ? colorAreaDims.width : x; 690 | y = y < 0 ? 0 : y > colorAreaDims.height ? colorAreaDims.height : y; 691 | 692 | // Set the position 693 | colorMarker.style.left = x + "px"; 694 | colorMarker.style.top = y + "px"; 695 | 696 | // Update the color 697 | setColorAtPosition(x, y); 698 | 699 | // Make sure the marker is focused 700 | colorMarker.focus(); 701 | } 702 | 703 | /** 704 | * Update the color picker's input field and preview thumb. 705 | * @param {Object} rgba Red, green, blue and alpha values. 706 | * @param {Object} [hsva] Hue, saturation, value and alpha values. 707 | */ 708 | function updateColor(rgba, hsva) {if (rgba === void 0) {rgba = {};}if (hsva === void 0) {hsva = {};} 709 | var format = settings.format; 710 | 711 | for (var key in rgba) { 712 | currentColor[key] = rgba[key]; 713 | } 714 | 715 | for (var _key in hsva) { 716 | currentColor[_key] = hsva[_key]; 717 | } 718 | 719 | var hex = RGBAToHex(currentColor); 720 | var opaqueHex = hex.substring(0, 7); 721 | 722 | colorMarker.style.color = opaqueHex; 723 | alphaMarker.parentNode.style.color = opaqueHex; 724 | alphaMarker.style.color = hex; 725 | colorPreview.style.color = hex; 726 | 727 | // Force repaint the color and alpha gradients as a workaround for a Google Chrome bug 728 | colorArea.style.display = 'none'; 729 | colorArea.offsetHeight; 730 | colorArea.style.display = ''; 731 | alphaMarker.nextElementSibling.style.display = 'none'; 732 | alphaMarker.nextElementSibling.offsetHeight; 733 | alphaMarker.nextElementSibling.style.display = ''; 734 | 735 | if (format === 'mixed') { 736 | format = currentColor.a === 1 ? 'hex' : 'rgb'; 737 | } else if (format === 'auto') { 738 | format = currentFormat; 739 | } 740 | 741 | switch (format) { 742 | case 'hex': 743 | colorValue.value = hex; 744 | break; 745 | case 'rgb': 746 | colorValue.value = RGBAToStr(currentColor); 747 | break; 748 | case 'hsl': 749 | colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor)); 750 | break;} 751 | 752 | 753 | // Select the current format in the format switcher 754 | document.querySelector(".clr-format [value=\"" + format + "\"]").checked = true; 755 | } 756 | 757 | /** 758 | * Set the hue when its slider is moved. 759 | */ 760 | function setHue() { 761 | var hue = hueSlider.value * 1; 762 | var x = colorMarker.style.left.replace('px', '') * 1; 763 | var y = colorMarker.style.top.replace('px', '') * 1; 764 | 765 | picker.style.color = "hsl(" + hue + ", 100%, 50%)"; 766 | hueMarker.style.left = hue / 360 * 100 + "%"; 767 | 768 | setColorAtPosition(x, y); 769 | } 770 | 771 | /** 772 | * Set the alpha when its slider is moved. 773 | */ 774 | function setAlpha() { 775 | var alpha = alphaSlider.value / 100; 776 | 777 | alphaMarker.style.left = alpha * 100 + "%"; 778 | updateColor({ a: alpha }); 779 | pickColor(); 780 | } 781 | 782 | /** 783 | * Convert HSVA to RGBA. 784 | * @param {object} hsva Hue, saturation, value and alpha values. 785 | * @return {object} Red, green, blue and alpha values. 786 | */ 787 | function HSVAtoRGBA(hsva) { 788 | var saturation = hsva.s / 100; 789 | var value = hsva.v / 100; 790 | var chroma = saturation * value; 791 | var hueBy60 = hsva.h / 60; 792 | var x = chroma * (1 - Math.abs(hueBy60 % 2 - 1)); 793 | var m = value - chroma; 794 | 795 | chroma = chroma + m; 796 | x = x + m; 797 | 798 | var index = Math.floor(hueBy60) % 6; 799 | var red = [chroma, x, m, m, x, chroma][index]; 800 | var green = [x, chroma, chroma, x, m, m][index]; 801 | var blue = [m, m, x, chroma, chroma, x][index]; 802 | 803 | return { 804 | r: Math.round(red * 255), 805 | g: Math.round(green * 255), 806 | b: Math.round(blue * 255), 807 | a: hsva.a }; 808 | 809 | } 810 | 811 | /** 812 | * Convert HSVA to HSLA. 813 | * @param {object} hsva Hue, saturation, value and alpha values. 814 | * @return {object} Hue, saturation, lightness and alpha values. 815 | */ 816 | function HSVAtoHSLA(hsva) { 817 | var value = hsva.v / 100; 818 | var lightness = value * (1 - hsva.s / 100 / 2); 819 | var saturation; 820 | 821 | if (lightness > 0 && lightness < 1) { 822 | saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100); 823 | } 824 | 825 | return { 826 | h: hsva.h, 827 | s: saturation || 0, 828 | l: Math.round(lightness * 100), 829 | a: hsva.a }; 830 | 831 | } 832 | 833 | /** 834 | * Convert RGBA to HSVA. 835 | * @param {object} rgba Red, green, blue and alpha values. 836 | * @return {object} Hue, saturation, value and alpha values. 837 | */ 838 | function RGBAtoHSVA(rgba) { 839 | var red = rgba.r / 255; 840 | var green = rgba.g / 255; 841 | var blue = rgba.b / 255; 842 | var xmax = Math.max(red, green, blue); 843 | var xmin = Math.min(red, green, blue); 844 | var chroma = xmax - xmin; 845 | var value = xmax; 846 | var hue = 0; 847 | var saturation = 0; 848 | 849 | if (chroma) { 850 | if (xmax === red) {hue = (green - blue) / chroma;} 851 | if (xmax === green) {hue = 2 + (blue - red) / chroma;} 852 | if (xmax === blue) {hue = 4 + (red - green) / chroma;} 853 | if (xmax) {saturation = chroma / xmax;} 854 | } 855 | 856 | hue = Math.floor(hue * 60); 857 | 858 | return { 859 | h: hue < 0 ? hue + 360 : hue, 860 | s: Math.round(saturation * 100), 861 | v: Math.round(value * 100), 862 | a: rgba.a }; 863 | 864 | } 865 | 866 | /** 867 | * Parse a string to RGBA. 868 | * @param {string} str String representing a color. 869 | * @return {object} Red, green, blue and alpha values. 870 | */ 871 | function strToRGBA(str) { 872 | var regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i; 873 | var match, rgba; 874 | 875 | // Default to black for invalid color strings 876 | ctx.fillStyle = '#000'; 877 | 878 | // Use canvas to convert the string to a valid color string 879 | ctx.fillStyle = str; 880 | match = regex.exec(ctx.fillStyle); 881 | 882 | if (match) { 883 | rgba = { 884 | r: match[3] * 1, 885 | g: match[4] * 1, 886 | b: match[5] * 1, 887 | a: match[6] * 1 }; 888 | 889 | 890 | } else { 891 | match = ctx.fillStyle.replace('#', '').match(/.{2}/g).map(function (h) {return parseInt(h, 16);}); 892 | rgba = { 893 | r: match[0], 894 | g: match[1], 895 | b: match[2], 896 | a: 1 }; 897 | 898 | } 899 | 900 | return rgba; 901 | } 902 | 903 | /** 904 | * Convert RGBA to Hex. 905 | * @param {object} rgba Red, green, blue and alpha values. 906 | * @return {string} Hex color string. 907 | */ 908 | function RGBAToHex(rgba) { 909 | var R = rgba.r.toString(16); 910 | var G = rgba.g.toString(16); 911 | var B = rgba.b.toString(16); 912 | var A = ''; 913 | 914 | if (rgba.r < 16) { 915 | R = '0' + R; 916 | } 917 | 918 | if (rgba.g < 16) { 919 | G = '0' + G; 920 | } 921 | 922 | if (rgba.b < 16) { 923 | B = '0' + B; 924 | } 925 | 926 | if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) { 927 | var alpha = rgba.a * 255 | 0; 928 | A = alpha.toString(16); 929 | 930 | if (alpha < 16) { 931 | A = '0' + A; 932 | } 933 | } 934 | 935 | return '#' + R + G + B + A; 936 | } 937 | 938 | /** 939 | * Convert RGBA values to a CSS rgb/rgba string. 940 | * @param {object} rgba Red, green, blue and alpha values. 941 | * @return {string} CSS color string. 942 | */ 943 | function RGBAToStr(rgba) { 944 | if (!settings.alpha || rgba.a === 1 && !settings.forceAlpha) { 945 | return "rgb(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ")"; 946 | } else { 947 | return "rgba(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ", " + rgba.a + ")"; 948 | } 949 | } 950 | 951 | /** 952 | * Convert HSLA values to a CSS hsl/hsla string. 953 | * @param {object} hsla Hue, saturation, lightness and alpha values. 954 | * @return {string} CSS color string. 955 | */ 956 | function HSLAToStr(hsla) { 957 | if (!settings.alpha || hsla.a === 1 && !settings.forceAlpha) { 958 | return "hsl(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%)"; 959 | } else { 960 | return "hsla(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%, " + hsla.a + ")"; 961 | } 962 | } 963 | 964 | /** 965 | * Init the color picker. 966 | */ 967 | function init() { 968 | // Render the UI 969 | container = undefined; 970 | picker = document.createElement('div'); 971 | picker.setAttribute('id', 'clr-picker'); 972 | picker.className = 'clr-picker'; 973 | picker.innerHTML = 974 | "" + ("
") + 976 | '
' + 977 | '
' + 978 | '
' + ("") + 980 | '
' + 981 | '
' + 982 | '
' + ("") + 984 | '
' + 985 | '' + 986 | '
' + 987 | '
' + 988 | '
' + ("" + 989 | settings.a11y.format + "") + 990 | '' + 991 | '' + 992 | '' + 993 | '' + 994 | '' + 995 | '' + 996 | '' + 997 | '
' + 998 | '
' + 999 | '
' + ("") + 1001 | '
' + ("") + 1003 | '
' + ("") + (""); 1006 | 1007 | // Append the color picker to the DOM 1008 | document.body.appendChild(picker); 1009 | 1010 | // Reference the UI elements 1011 | colorArea = getEl('clr-color-area'); 1012 | colorMarker = getEl('clr-color-marker'); 1013 | clearButton = getEl('clr-clear'); 1014 | closeButton = getEl('clr-close'); 1015 | colorPreview = getEl('clr-color-preview'); 1016 | colorValue = getEl('clr-color-value'); 1017 | hueSlider = getEl('clr-hue-slider'); 1018 | hueMarker = getEl('clr-hue-marker'); 1019 | alphaSlider = getEl('clr-alpha-slider'); 1020 | alphaMarker = getEl('clr-alpha-marker'); 1021 | 1022 | // Bind the picker to the default selector 1023 | bindFields(settings.el); 1024 | wrapFields(settings.el); 1025 | 1026 | addListener(picker, 'mousedown', function (event) { 1027 | picker.classList.remove('clr-keyboard-nav'); 1028 | event.stopPropagation(); 1029 | }); 1030 | 1031 | addListener(colorArea, 'mousedown', function (event) { 1032 | addListener(document, 'mousemove', moveMarker); 1033 | }); 1034 | 1035 | addListener(colorArea, 'contextmenu', function (event) { 1036 | event.preventDefault(); 1037 | }); 1038 | 1039 | addListener(colorArea, 'touchstart', function (event) { 1040 | document.addEventListener('touchmove', moveMarker, { passive: false }); 1041 | }); 1042 | 1043 | addListener(colorMarker, 'mousedown', function (event) { 1044 | addListener(document, 'mousemove', moveMarker); 1045 | }); 1046 | 1047 | addListener(colorMarker, 'touchstart', function (event) { 1048 | document.addEventListener('touchmove', moveMarker, { passive: false }); 1049 | }); 1050 | 1051 | addListener(colorValue, 'change', function (event) { 1052 | var value = colorValue.value; 1053 | 1054 | if (currentEl || settings.inline) { 1055 | var color = value === '' ? value : setColorFromStr(value); 1056 | pickColor(color); 1057 | } 1058 | }); 1059 | 1060 | addListener(clearButton, 'click', function (event) { 1061 | pickColor(''); 1062 | closePicker(); 1063 | }); 1064 | 1065 | addListener(closeButton, 'click', function (event) { 1066 | pickColor(); 1067 | closePicker(); 1068 | }); 1069 | 1070 | addListener(getEl('clr-format'), 'click', '.clr-format input', function (event) { 1071 | currentFormat = event.target.value; 1072 | updateColor(); 1073 | pickColor(); 1074 | }); 1075 | 1076 | addListener(picker, 'click', '.clr-swatches button', function (event) { 1077 | setColorFromStr(event.target.textContent); 1078 | pickColor(); 1079 | 1080 | if (settings.swatchesOnly) { 1081 | closePicker(); 1082 | } 1083 | }); 1084 | 1085 | addListener(document, 'mouseup', function (event) { 1086 | document.removeEventListener('mousemove', moveMarker); 1087 | }); 1088 | 1089 | addListener(document, 'touchend', function (event) { 1090 | document.removeEventListener('touchmove', moveMarker); 1091 | }); 1092 | 1093 | addListener(document, 'mousedown', function (event) { 1094 | keyboardNav = false; 1095 | picker.classList.remove('clr-keyboard-nav'); 1096 | closePicker(); 1097 | }); 1098 | 1099 | addListener(document, 'keydown', function (event) { 1100 | var key = event.key; 1101 | var target = event.target; 1102 | var shiftKey = event.shiftKey; 1103 | var navKeys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; 1104 | 1105 | if (key === 'Escape') { 1106 | closePicker(true); 1107 | 1108 | // Display focus rings when using the keyboard 1109 | } else if (navKeys.includes(key)) { 1110 | keyboardNav = true; 1111 | picker.classList.add('clr-keyboard-nav'); 1112 | } 1113 | 1114 | // Trap the focus within the color picker while it's open 1115 | if (key === 'Tab' && target.matches('.clr-picker *')) { 1116 | var focusables = getFocusableElements(); 1117 | var firstFocusable = focusables.shift(); 1118 | var lastFocusable = focusables.pop(); 1119 | 1120 | if (shiftKey && target === firstFocusable) { 1121 | lastFocusable.focus(); 1122 | event.preventDefault(); 1123 | } else if (!shiftKey && target === lastFocusable) { 1124 | firstFocusable.focus(); 1125 | event.preventDefault(); 1126 | } 1127 | } 1128 | }); 1129 | 1130 | addListener(document, 'click', '.clr-field button', function (event) { 1131 | // Reset any previously set per-instance options 1132 | if (hasInstance) { 1133 | resetVirtualInstance(); 1134 | } 1135 | 1136 | // Open the color picker 1137 | event.target.nextElementSibling.dispatchEvent(new Event('click', { bubbles: true })); 1138 | }); 1139 | 1140 | addListener(colorMarker, 'keydown', function (event) { 1141 | var movements = { 1142 | ArrowUp: [0, -1], 1143 | ArrowDown: [0, 1], 1144 | ArrowLeft: [-1, 0], 1145 | ArrowRight: [1, 0] }; 1146 | 1147 | 1148 | if (Object.keys(movements).includes(event.key)) { 1149 | moveMarkerOnKeydown.apply(void 0, movements[event.key]); 1150 | event.preventDefault(); 1151 | } 1152 | }); 1153 | 1154 | addListener(colorArea, 'click', moveMarker); 1155 | addListener(hueSlider, 'input', setHue); 1156 | addListener(alphaSlider, 'input', setAlpha); 1157 | } 1158 | 1159 | /** 1160 | * Return a list of focusable elements within the color picker. 1161 | * @return {array} The list of focusable DOM elemnts. 1162 | */ 1163 | function getFocusableElements() { 1164 | var controls = Array.from(picker.querySelectorAll('input, button')); 1165 | var focusables = controls.filter(function (node) {return !!node.offsetWidth;}); 1166 | 1167 | return focusables; 1168 | } 1169 | 1170 | /** 1171 | * Shortcut for getElementById to optimize the minified JS. 1172 | * @param {string} id The element id. 1173 | * @return {object} The DOM element with the provided id. 1174 | */ 1175 | function getEl(id) { 1176 | return document.getElementById(id); 1177 | } 1178 | 1179 | /** 1180 | * Shortcut for addEventListener to optimize the minified JS. 1181 | * @param {object} context The context to which the listener is attached. 1182 | * @param {string} type Event type. 1183 | * @param {(string|function)} selector Event target if delegation is used, event handler if not. 1184 | * @param {function} [fn] Event handler if delegation is used. 1185 | */ 1186 | function addListener(context, type, selector, fn) { 1187 | var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; 1188 | 1189 | // Delegate event to the target of the selector 1190 | if (typeof selector === 'string') { 1191 | context.addEventListener(type, function (event) { 1192 | if (matches.call(event.target, selector)) { 1193 | fn.call(event.target, event); 1194 | } 1195 | }); 1196 | 1197 | // If the selector is not a string then it's a function 1198 | // in which case we need a regular event listener 1199 | } else { 1200 | fn = selector; 1201 | context.addEventListener(type, fn); 1202 | } 1203 | } 1204 | 1205 | /** 1206 | * Call a function only when the DOM is ready. 1207 | * @param {function} fn The function to call. 1208 | * @param {array} [args] Arguments to pass to the function. 1209 | */ 1210 | function DOMReady(fn, args) { 1211 | args = args !== undefined ? args : []; 1212 | 1213 | if (document.readyState !== 'loading') { 1214 | fn.apply(void 0, args); 1215 | } else { 1216 | document.addEventListener('DOMContentLoaded', function () { 1217 | fn.apply(void 0, args); 1218 | }); 1219 | } 1220 | } 1221 | 1222 | // Polyfill for Nodelist.forEach 1223 | if (NodeList !== undefined && NodeList.prototype && !NodeList.prototype.forEach) { 1224 | NodeList.prototype.forEach = Array.prototype.forEach; 1225 | } 1226 | 1227 | // Expose the color picker to the global scope 1228 | window.Coloris = function () { 1229 | var methods = { 1230 | set: configure, 1231 | wrap: wrapFields, 1232 | close: closePicker, 1233 | setInstance: setVirtualInstance, 1234 | removeInstance: removeVirtualInstance, 1235 | updatePosition: updatePickerPosition, 1236 | ready: DOMReady }; 1237 | 1238 | 1239 | function Coloris(options) { 1240 | DOMReady(function () { 1241 | if (options) { 1242 | if (typeof options === 'string') { 1243 | bindFields(options); 1244 | } else { 1245 | configure(options); 1246 | } 1247 | } 1248 | }); 1249 | }var _loop2 = function _loop2( 1250 | 1251 | key) { 1252 | Coloris[key] = function () {for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) {args[_key2] = arguments[_key2];} 1253 | DOMReady(methods[key], args); 1254 | };};for (var key in methods) {_loop2(key); 1255 | } 1256 | 1257 | return Coloris; 1258 | }(); 1259 | 1260 | // Init the color picker when the DOM is ready 1261 | DOMReady(init); 1262 | 1263 | })(window, document, Math); 1264 | --------------------------------------------------------------------------------